electron实现软件(热)更新(附带示例源码)
热更新指的是:electron 程序已经开启,在不关闭的情况下执行更新,需要我们把远程的app.asar文件下载到本地执行替换,然而 在electron应用程序开启状态是无法直接下载app.asar文件的,下载会检查出app.asar文件被占用,所以我们需要在本地将app.asar文件反编译,编译出一个app文件夹,里面有项目所需的所有源码文件,这时通过vue或react打包的渲染进程代码是经过压缩的,但是主进程代码会直接暴露,所以刚好,我们可以将主进程代码做压缩混淆,然后生成一个
app.zip压缩包,上传至服务器,然后下载这个压缩包,解压,将其编译成app.asar文件替换到resources目录中,从而实现electron软件的热更新。
- 主进程(nodejs)侧代码:
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
const fs = require("fs");
const http = require("http");
const asar = require('asar');
const AdmZip = require('adm-zip');
const fsExtra = require('fs-extra');const mainData = require("./mainData");
const txtConsole = require("./txtConsole");//当前环境
const production = mainData?.production;//Electron 安装根目录
const rootPath = production === 'dev' ? path.resolve('./public') : path.dirname(app.getPath('exe'));
mainData.rootPath = rootPath;// 软件更新配置信息(目前需要手动修改~~~)
const winUpdateConfig = {currentVersion: null, //当前版本updateVersionFilePath: 'http://103.117.121.53:8002/latest', //远程版本信息路径updateFilePath: 'http://103.117.121.53:8002/app.zip', //远程包路径localUpdateVersionFilePath: production === 'dev' ? `${rootPath}/latest` : `${rootPath}/resources/latest`, //本地版本信息路径localUpdateFilePath: production === 'dev' ? rootPath : `${rootPath}/resources`, //本地包路径updateSteps: [{id: 1, desc: '开始下载并解压更新文件,请勿重启!', active: 'active'},{id: 2, desc: '下载并解压完成, 开始覆盖安装!', active: 'wait'},{id: 3, desc: '更新完毕, 即将重启,请稍候!(第3步完成后也可以手动重启)', active: 'wait'},], //更新步骤 active:正在进行 wait:等待 success:执行成功 error: 执行失败
};let versionInfo = ''; //获取最新版本信息
let locallatest = ''; //本地版本号function appInit() {txtConsole.log('初始化');try {locallatest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');locallatest = JSON.parse(locallatest);//设置当前版本信息winUpdateConfig.currentVersion = locallatest?.version;txtConsole.log('已设置当前版本信息', locallatest?.version);//删除日志txtConsole.clearLog();} catch (err) {txtConsole.log(err);}
}//创建主窗口
function createWindow() {const mainWindow = new BrowserWindow({width: 800,height: 600,webPreferences: {// preload: path.join(__dirname, 'preload.js'),nodeIntegration: true,contextIsolation: false,webSecurity: false, //禁用同源策略}});mainWindow.loadFile('index.html').then();// 打开开发者工具 控制台if (mainData.winControl === 'dev') {mainWindow.webContents.openDevTools();}// 检查更新ipcMain.on('window-version', function (event) {try {txtConsole.log('检查更新', versionInfo);if (!versionInfo) {!event?.sender?.isDestroyed() &&event?.sender?.send('window-version-err-msg', '更新文件读取失败');return;}const v = {...(versionInfo || {})};//最新版本号let firNewVersion = versionInfo?.version?.split('.')?.[0]; //第一位let secNewVersion = versionInfo?.version?.split('.')?.[1]; //第二位let thiNewVersion = versionInfo?.version?.split('.')?.[2]; //第三位//当前版本号let firOldVersion = versionInfo?.currentVersion?.split('.')?.[0]; //第一位let secOldVersion = versionInfo?.currentVersion?.split('.')?.[1]; //第二位let thiOldVersion = versionInfo?.currentVersion?.split('.')?.[2]; //第三位//按位比较是否需要更新if (Number(firNewVersion || 10000) > Number(firOldVersion || 10000)) {v['versionVisible'] = true;}else if (Number(secNewVersion || 10000) > Number(secOldVersion || 10000)) {v['versionVisible'] = true;}else if (Number(thiNewVersion || 10000) > Number(thiOldVersion || 10000)) {v['versionVisible'] = true;}else {v['versionVisible'] = false;v['currentVersion'] = versionInfo?.version;}if (!v['versionVisible']) {let latest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');latest = JSON.parse(latest);latest.version = versionInfo?.version || latest?.version;latest.currentVersion = versionInfo?.version || latest?.version;fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath, JSON.stringify(latest));txtConsole.log('hot: ', latest.version);}txtConsole.log('versionVisible=> ', v['versionVisible']);!event?.sender?.isDestroyed() && event?.sender?.send('window-version-msg', v);} catch (err) {!event?.sender?.isDestroyed() &&event?.sender?.send('window-version-err-msg', '更新文件读取失败');txtConsole.log('检查更新err:', err);}});// 下载更新文件ipcMain.on('window-download-newfile', function (event) {txtConsole.log('开始下载并解压更新文件 热更新');event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);const file = fs.createWriteStream(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'),);let downloadedBytes = 0;let totalBytes = 0;http.get(winUpdateConfig.updateFilePath, (response) => {totalBytes = parseInt(response?.headers['content-length'], 10);let prevTimestamp = Date.now();response?.on('data', (chunk) => {downloadedBytes += chunk.length;const timestamp = Date.now();const timeDiff = timestamp - prevTimestamp;// 每1.5秒钟更新一次进度if (timeDiff >= 1500) {const progress = ((downloadedBytes / totalBytes) * 100).toFixed(2);txtConsole.log(`下载进度:${progress}% `, totalBytes);prevTimestamp = timestamp;event?.sender?.send('window-download-progress-msg', Math.min(Number(progress), 80));}});response?.pipe(file);}).on('error', (err) => {txtConsole.log(`下载错误: ${err.message}`);event?.sender?.send('window-download-newfile-err-msg', '更新文件下载失败');});file?.on('finish', function () {event.sender.send('window-download-progress-msg', 90);winUpdateConfig.updateSteps[0]['active'] = 'success';winUpdateConfig.updateSteps[1]['active'] = 'active';event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);// 文件已经完全写入磁盘,开始解压try {const zip = new AdmZip(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'), void 0);zip.extractAllTo(winUpdateConfig.localUpdateFilePath, true, void 0, void 0);} catch (err) {txtConsole.log('解压异常 error: ', err);!event?.sender?.isDestroyed() &&event?.sender?.send('window-download-newfile-err-msg', '解压异常');return;}winUpdateConfig.updateSteps[1]['active'] = 'success';winUpdateConfig.updateSteps[2]['active'] = 'active';event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);event?.sender?.send('window-download-progress-msg', 95);const sourceDir = path.join(winUpdateConfig.localUpdateFilePath, 'apps');const destPath = path.join(winUpdateConfig.localUpdateFilePath, 'app.asar');asar.createPackage(sourceDir, destPath).then(() => {if (fs.existsSync(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'))) {fs.unlinkSync(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'));}txtConsole.log('更新完毕');event.sender.send('window-download-progress-msg', 100);winUpdateConfig.updateSteps[2]['active'] = 'success';event?.sender?.send('window-download-newfile-msg',winUpdateConfig.updateSteps,'success',);//设置当前版本信息try {let latest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');latest = JSON.parse(latest);latest.version = versionInfo.version;fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath, JSON.stringify(latest));txtConsole.log('更新后已设置当前版本信息', latest?.version);//删除apps文件夹 防止执行文件夹内的代码deleteFolderRecursive(sourceDir);} catch (err) {txtConsole.log(err);}}).catch((err) => {txtConsole.log('创建asar文件失败: ', err);event.sender.send('window-download-newfile-err-msg', 'asar文件创建失败');});});file?.on('error', function (err) {txtConsole.log('更新asar=>Error: ', err);event.sender.send('window-download-newfile-err-msg', err);});});
}//检查更新
function checkUpdate(callback) {txtConsole.log('检查更新');http.get(winUpdateConfig.updateVersionFilePath, (res) => {res.on('data', (chunk) => {versionInfo += chunk;});res.on('end', () => {try {if (versionInfo && versionInfo?.indexOf('404 Not Found') < 0) {versionInfo = JSON.parse(versionInfo);winUpdateConfig.updateFilePath = versionInfo.updateFilePath;//热更最新信息let asarVersionInfo = {newVersionDesc: versionInfo.newVersionDesc,currentVersion: winUpdateConfig.currentVersion,};versionInfo.currentVersion = winUpdateConfig.currentVersion;let writeNewVersonInfo;//不存在则创建latest文件if (!fs.existsSync(winUpdateConfig.localUpdateVersionFilePath)) {writeNewVersonInfo = versionInfo;txtConsole.log('latest文件重新创建成功');}else {let currentVersion = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath,'utf8',);currentVersion = JSON.parse(currentVersion);currentVersion['updateFilePath'] = '';//只覆盖热更版本信息writeNewVersonInfo = {...currentVersion, ...asarVersionInfo};}//将整理好的配置文件信息写入fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath,JSON.stringify(writeNewVersonInfo),);// txtConsole.log('已将新的更新配置文件信息写入:', JSON.stringify(writeNewVersonInfo));txtConsole.log(`更新检查完毕:最新版本:${versionInfo.version}, 当前版本:${asarVersionInfo.currentVersion}`,);txtConsole.log('-------------------------------------------------------');callback?.(null, versionInfo);}else {txtConsole.log('更新配置文件读取失败');callback?.('更新配置文件读取失败');}} catch (err) {txtConsole.log('更新配置文件覆写失败');callback?.('更新配置文件覆写失败');}});}).on('error', (error) => {txtConsole.log(`更新配置文件下载失败: ${error.message}`);callback?.('更新配置文件下载失败');});
}//删除更新文件
function deleteFolderRecursive(folderPath) {if (fs.existsSync(folderPath)) {fs.readdirSync(folderPath).forEach((file) => {const curPath = path.join(folderPath, file);if (fs.lstatSync(curPath).isDirectory()) {// 递归删除子文件夹deleteFolderRecursive(curPath);}else {// 删除文件fs.unlinkSync(curPath);}});// 删除子文件夹后删除文件夹本身fs.rmdirSync(folderPath);}
}app.whenReady().then(() => {//初始化appInit();//检查更新checkUpdate(async (check, versionInfo = {}) => {if (check) {txtConsole.log('检查更新执行失败');}else {txtConsole.log('检查更新执行成功');}createWindow();app.on('activate', function () {if (BrowserWindow.getAllWindows().length === 0) createWindow()})});
})app.on('window-all-closed', function () {if (process.platform !== 'darwin') app.quit()
})
- 渲染进程侧代码(以原生为例):
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/><title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
<button onclick="onCheckUpdate()">检查更新</button>
<button onclick="onUpdateVersion()">测试更新</button><div class="updateInfo"></div>
<div class="descInfo"></div><script>const {ipcRenderer} = require("electron");const onVersion = {updateDsec: [],process: 0,};//检查更新function onCheckUpdate() {ipcRenderer?.send('window-version');ipcRenderer?.once('window-version-msg', (_, bool) => {document.querySelector('.updateInfo').innerHTML = JSON.stringify(bool);});ipcRenderer?.once('window-version-err-msg', (_, err) => {document.querySelector('.updateInfo').innerHTML = err;});}//测试更新function onUpdateVersion() {onVersion.updateDsec = [];ipcRenderer?.send('window-download-newfile');//监听下载版本信息ipcRenderer?.on('window-download-newfile-msg', (event, updateDsec, status) => {if (!event.handel) {event.handel = true;onVersion.isStartUpdate = true;onVersion.updateDsec = updateDsec;document.querySelector('.descInfo').innerHTML = updateDsec.map(item => `<span style="color:${item.active === 'success' ? 'green' : 'orangered'}">${item.desc}</span>`).join('</br>');if (status === 'success') {ipcRenderer?.send('window-restart-app');}onVersion.visible = true;}});//监听更新包下载进度ipcRenderer?.on('window-download-progress-msg', (event, process) => {if (!event.handel) {event.handel = true;document.querySelector('.updateInfo').innerHTML = String("完成进度:" + process + '%');}});//监听下载版本错误信息ipcRenderer?.once('window-download-newfile-err-msg', (event, res) => {console.log(res)});}
</script>
</body>
</html>
- 轮子(实现压缩混淆反编译):
//生成 反编译app.asar 并生成压缩包
const asar = require('asar');
const path = require('path');
const fs = require('fs');
const fsExtra = require('fs-extra');
const zlib = require('zlib');
const archiver = require('archiver');
const uglify = require('uglify-js');
const moment = require('moment');
const {exec} = require('child_process');
const JavaScriptObfuscator = require('javascript-obfuscator');const mainData = require('./mainData');const startTime = moment().unix(); //秒级时间戳const rootPath = path.resolve(__dirname); // 获取项目根路径
const asarPath = './build/win-ia32-unpacked/resources/app.asar'; // 获取 app.asar 文件路径
const sourceDir = './apps'; // 要压缩的文件夹路径
const asarAppPath = './apps/apps'; // asar反编译文件的存放路径
const buildPath = './build'; //electron 打包后的build文件夹
const destFile = './app.zip'; // 压缩后的文件路径
const publicLogPath = './public/log.txt';
const publicAsarPath = './public/app.asar';// 配置环境路径为项目根路径
const env = Object.assign({}, process.env, {PATH: rootPath + ';' + process.env.PATH,npm_config_prefix: 'C:\\Program Files\\nodejs\\npm', // 这里是你的 npm 安装路径
});const Console = {log(p1 = '', p2 = '', p3 = '', p4 = '', p5 = '') {console.log(`${moment().format('HH:mm:ss')} | ${p1}${p2}${p3}${p4}${p5}`);},
};//压缩主进程 main.js 相关代码
function zipMainJS() {try {const dir = path.resolve(asarAppPath, 'main.js');const dirJs = fs.readFileSync(dir, 'utf8');//压缩代码 mangle: true,const result = uglify.minify(dirJs, {mangle: {toplevel: true,},});// 混淆代码const obfuscationResult = JavaScriptObfuscator.obfuscate(result.code, {compact: true,controlFlowFlattening: true,controlFlowFlatteningThreshold: 0.75,numbersToExpressions: true,simplify: true,shuffleStringArray: true,splitStrings: true,stringArrayThreshold: 0.75,});fs.writeFileSync(dir, obfuscationResult.getObfuscatedCode());return true;} catch (err) {Console.log(err);return false;}
}//添加开始执行 app.asar反编译逻辑
async function init() {//执行app.asar 反编译、压缩、混淆Console.log('正在执行app.asar 反编译、压缩、混淆');// 将 app.asar 解压缩到指定文件夹中asar.extractAll(asarPath, asarAppPath);Console.log('正在压缩app文件夹到项目根目录');//压缩main.js相关代码let zipRes = zipMainJS();if (!zipRes) {Console.log('!!!压缩main.js主进程代码失败!');return;}Console.log('主进程相关代码压缩完毕');//再次生成 app.asarasar.createPackage(asarAppPath, asarPath).then(() => {Console.log('已再次生成 app.asar 文件(代码压缩后的asar文件)');onAppZip();});
}function onAppZip() {// 创建一个可写流,将压缩后的文件写入到目标文件中const destStream = fs.createWriteStream(destFile);// 创建一个 archiver 实例const archive = archiver('zip', {zlib: {level: zlib.constants.Z_BEST_COMPRESSION},});// 将可写流传递给 archiver 实例archive.pipe(destStream);// 将要压缩的文件夹添加到 archiver 实例中archive.directory(sourceDir, false, null);// 完成压缩并关闭可写流archive.finalize();// 监听可写流的 'close' 事件,表示压缩完成destStream.on('close', () => {Console.log(`压缩完毕,压缩包路径:【${path.resolve(__dirname, destFile)}】`);Console.log('共用时:' + (moment().unix() - startTime) + '秒');});
}try {if (mainData?.production === 'dev') {throw "请将环境切换为生产环境 mainData.js => 【const production = 'pro';】";}if (mainData?.winControl === 'dev') {throw '请关闭主窗口调试控制台!' + 'winControl';}fsExtra.removeSync(buildPath);Console.log('已删除build文件夹内容');fsExtra.removeSync(publicLogPath);Console.log('已删除public/log.txt');fsExtra.removeSync(publicAsarPath);Console.log('已删除public/app.asar');fsExtra.removeSync(asarAppPath);Console.log('已删除apps');//执行 打包命令Console.log('正在执行【npm run packager32】命令');exec('npm run packager32', env, (error, stdout, stderr) => {if (error) {Console.log(`执行出错: ${error}`);return;}stderr && Console.log('【npm run packager32】 stderr=>', stderr);//生成app.init();});} catch (err) {Console.log(err);
}
示例Demo: https://github.com/qglovehy/electron-updater.git
相关文章:
electron实现软件(热)更新(附带示例源码)
热更新指的是:electron 程序已经开启,在不关闭的情况下执行更新,需要我们把远程的app.asar文件下载到本地执行替换,然而 在electron应用程序开启状态是无法直接下载app.asar文件的,下载会检查出app.asar文件被占用&…...
飞天使-k8s知识点12-kubernetes散装知识点1-架构有状态资源对象分类
文章目录 k8s架构图有状态和无状态服务 资源和对象对象规约和状态 资源的对象-资源的分类元数据型与集群型资源命名空间 k8s架构图 有状态和无状态服务 区分有状态和无状态服务有利于维护yaml文件 因为配置不同资源和对象 命令行yaml来定义对象对象规约和状态 规约 spec 描述…...
mhz_c1f
信息收集 探测到存活主机的IP地址为 192.168.101.32 # nmap -sT --min-rate 10000 -p- 192.168.101.32 -oN port.nmap Starting Nmap 7.94 ( https://nmap.org ) at 2024-02-03 13:41 CST Nmap scan report for 192.168.101.32 Host is up (0.0020s latency). Not shown: 6553…...
Excel——高级筛选匹配条件提取数据
一、筛选多条件 Q:筛选多个条件,并将筛选出的内容复制到其他区域 点击任意一个单元格 点击【数据】——【筛选】——【高级筛选】 选择【将筛选结果复制到其他位置】——在【列表区域】 鼠标选择对应的区域位置,条件区域一定要单独写出来&a…...
Python初学者学习记录——python基础综合案例:数据可视化——动态柱状图
一、案例效果 通过pyecharts可以实现数据的动态显示,直观的感受1960~2019年世界各国GDP的变化趋势 二、通过Bar构建基础柱状图 反转x轴和y轴 标签数值在右侧 from pyecharts.charts import Bar from pyecharts.options import LabelOpts# 构建柱状图对象 bar Bar()…...
1.27马尔科夫链,抽样蒙特卡洛模拟(逆转化方法,接受拒绝矩阵),马尔科夫链蒙特卡洛MCMC,隐马尔科夫(HMM(V算法剪枝优化),NLP)
马尔科夫链 蒙特卡洛法模拟 抽样,逆转换方法 就是说由系统自带的随机函数RANDOM,通过下面这个方法,可以变为对应的随机模拟函数 就是说要实现蒙特卡洛模拟,是要先有一个概率表达式,然后基于这个概率表达式࿰…...
MC34063异常发热分析
问题描述: 工程现场反馈若干电源转换模块损坏,没有输出。拿到问题模块后,查看有一个MC34063周围的PCB有比较明显的高温痕迹,配套的电感也有明显的高温过热痕迹。 问题调查: MC34063的电路非常经典(虽然自…...
获取真实 IP 地址(一):判断是否使用 CDN(附链接)
一、介绍 CDN,全称为内容分发网络(Content Delivery Network),是一种网络架构,旨在提高用户对于网络上内容的访问速度和性能。CDN通过在全球各地部署分布式服务器节点来存储和分发静态和动态内容,从而减少…...
跨越财务困境,聚道云软件连接器如何助力企业轻松实现数字化转型?
客户介绍 某家庭服务科技有限公司是一家专注于提供高品质家庭服务的综合性企业。公司以“让家庭生活更美好”为使命,致力于为每一位客户提供专业、细致、周到的家庭服务。作为一家具有社会责任感的企业,该公司积极履行企业公民义务,关注家庭…...
Python接口自动化测试框架运行原理及流程
这篇文章主要介绍了Python接口自动化测试框架运行原理及流程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 本文总结分享介绍接口测试框架开发,环境使用python3selenium3unittestddtrequests测试框…...
strtok的使用
Strtok:原型char *strtok(char s[], const char *delim); s为要分解的字符,delim为分隔符字符(如果传入字符串,则传入的字符串中每个字符均为分割符)。首次调用时,s指向要分解的字符串,之后再次…...
0206作业
TCP(传输控制协议)和 UDP(用户数据报协议)是两种常用的网络传输协议。它们之间的主要区别在于: 可靠性:TCP 是一种可靠的传输协议,它提供了数据传输的确认、重传和排序功能。如果数据在传输过程…...
数据结构-栈
1.容器 容器用于容纳元素集合,并对元素集合进行管理和维护. 传统意义上的管理和维护就是:增,删,改,查. 我们分析每种类型容器时,主要分析其增,删,改ÿ…...
CentOS7搭建k8s-v1.28.6集群详情
文章目录 1.灌装集群节点操作系统1.1 设置hosts1.2 设置nameserver1.3 关闭防火墙1.4 关闭Selinux1.5 关闭Swap分区1.6 时间同步1.7 调整内核参数1.8 系统内核升级 2.安装Docker2.1 卸载旧Docker2.2 配置Docker软件源2.3 安装Docker 3.部署Kubernets集群3.1 设置 K8s 软件源3.2…...
Android实现底部导航栏方法(Navigation篇)
Navigation实现底部导航栏 前言导入和基本使用导入基础使用创建nav文件编辑Nav文件添加页面(代码版)添加页面(图解版) 创建导航动作 action创建action(代码版)创建action(图解版) 编…...
python 爬虫篇(1)---->re正则的详细讲解(附带演示代码)
re正则的详细讲解 文章目录 re正则的详细讲解前言4.re正则表达式(1)e正则的匹配模式(2) re.search 的使用(3)re.findall()的使用(4)re.sub()的使用结语前言 大家好,今天我将开始更新python爬虫篇,陆续更新几种解析数据的方法,例如 re正则表达式beautifulsoup xpath lxml 等等,…...
(超详细)10-YOLOV5改进-替换CIou为Wise-IoU
yolov5中box_iou其默认用的是CIoU,其中代码还带有GIoU,DIoU,文件路径:utils/metrics.py,函数名为:bbox_iou 将下面代码放到metrics.py文件里面,原来的bbox_iou函数删掉 class WIoU_Scale: mon…...
Java-并发高频面试题-2
接着之前的Java-并发高频面试题 7. synchronized的实现原理是怎么样的? 首先我们要知道synchronized它是解决线程安全问题的一种方式,而具体是怎么解决的呢?主要是通过加锁的方式来解决 在底层实现上来看 是通过 monitorenter、monitorexit…...
Windows安装Redis
安装Redis是一个比较简单的过程,以下是在Windows上安装Redis的基本步骤: 下载Redis:首先,你需要从Redis官方网站(https://redis.io/download)下载适合Windows的Redis安装包。你可以选择稳定版本或者开发版本…...
Nicn的刷题日常之 有序序列判断
目录 1.题目描述 描述 输入描述: 输出描述: 示例1 示例2 示例3 2.解题 1.题目描述 描述 输入一个整数序列,判断是否是有序序列,有序,指序列中的整数从小到大排序或者从大到小排序(相同元素也视为有序)。 数据…...
IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...
【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...
