# dhh-upgrade-app **Repository Path**: GreatNavigation/dhh-upgrade-app ## Basic Information - **Project Name**: dhh-upgrade-app - **Description**: uniapp 应用升级(包含本地后台模拟) - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-08-17 - **Last Updated**: 2025-08-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## App整包升级和热更新 - 因为自用,本插件后期可能不会维护,追求长期稳定的可以跳转原作者[赵永强](https://ext.dcloud.net.cn/plugin?id=7286)。 - 目前只测试了安卓-vue3的升级流程(其他环境下应该也没问题)。 - 采用uni_modules版本,带完整本地升级测试后台。 ## 安装指引 ### 1. pages.json 添加路径 ```json "pages": [ { "path": "uni_modules/dhh-upgrade-app/components/dhh-upgrade-app/dhh-upgrade-app", "style": { "app-plus": { "animationDuration": 200, // 窗口的显示动画的持续时间 "animationType": "fade-in", // 窗口的显示的动画类型 "background": "transparent", // 窗体背景色 "backgroundColorTop": "transparent", "popGesture": "none", // 侧滑返回功能,可选值:"close"(启用侧滑返回)、"none"(禁用侧滑返回) "scrollIndicator": false, // 是否显示滚动条,设置为 "none" 时不显示滚动条。 "titleNView": false, // 导航栏设置 }, "disableScroll": true // 设置为 true 则页面整体不能上下滚动(bounce效果),只在页面配置中有效,在globalStyle中设置无效 } } ] ``` ### 2. app.vue页代码,我测试是放在onLaunch内,也可以在onShow,这个根据自己的情况来 ```vue ``` ### 3. 接口 update.js ```js // api/update.js import { http } from '@/utils/http' // app版本信息 export const getAppVersionAPI = (data) => { return http({ method: 'GET', url: '/check-update', data }) } ``` ```js // 接口返回数据 { describe: '1. 修复已知问题
2. 优化用户体验', edition_url: '', //apk、wgt包下载地址或者应用市场地址 安卓应用市场 market://details?id=xxxx 苹果store itms-apps://itunes.apple.com/cn/app/xxxxxx edition_force: 0, //是否强制更新 0代表否 1代表是 package_type: 1, //0是整包升级(apk或者appstore或者安卓应用市场) 1是wgt升级 edition_issue:1, //是否发行 0否 1是 为了控制上架应用市场审核时不能弹出热更新框 edition_number:100, //版本号 最重要的manifest里的版本号 (检查更新主要以服务器返回的edition_number版本号是否大于当前app的版本号来实现是否更新) edition_version:'1.0.0',// 版本名称 manifest里的版本名称 edition_silence:0, // 是否静默更新 0代表否 1代表是 } ``` ### 4. http.js ```js // 请求基地址 const baseURL = 'http://192.168.110.44:3000/api' // 本机ipv4地址 // 配置拦截器 const httpInterceptor = { // 拦截前触发 invoke(options) { // 非 http 开头需要拼接完整地址 if (!options.url.startsWith('http')) { options.url = baseURL + options.url; } // 请求超时 默认60s options.timeout = 30000; // 添加后端所需请求头 options.header = { ...options.header, 'content-type': 'application/json', } }, }; // 启用请求拦截器 uni.addInterceptor('request', httpInterceptor); // 启用文件上传拦截器 uni.addInterceptor('uploadFile', httpInterceptor); // 请求函数 export const http = function(options) { // 返回 Promise 对象 return new Promise(function(resolve, reject) { uni.showLoading({ title: "加载中..." }); uni.request({ ...options, // 响应成功 success: (res) => { if (res.statusCode == 200) { // 提取核心数据 resolve(res.data); } else { reject(res.data); } }, // 响应失败 fail: () => { uni.hideLoading(); uni.showToast({ title: '接口请求失败', icon: 'error', }); }, complete: function() { uni.hideLoading(); } }); }); }; ``` **至此,uniapp端的app升级流程已经完成,一般来说,都有一个后台系统来管理app,接下来可以跟我一步步搭建一个本地后代来测试App升级流程。** **🐛🐛🐛如果不想按照以下流程搭建本地后台,可以使用作者测试用的项目**[gitee地址](https://gitee.com/GreatNavigation/dhh-upgrade-app) ## 本地升级后台系统 VUE3 + Elementplus ### 依赖 ```json { "name": "myadmin", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "axios": "^1.11.0", "element-plus": "^2.10.7", "sass": "^1.90.0", "vue": "^3.5.18", "vue-router": "^4.5.1" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", "vite": "^7.1.2" } } ``` ### 目录结构 ``` src/ ├── components/ │ ├── VersionList.vue │ └── VersionForm.vue │ └── CreateVersionDialog.vue ├── views/ │ └── AppVersionView.vue ├── api/ │ └── versionApi.js ├── router/ │ └── index.js └── App.vue ``` #### api/versionApi.js ```js import axios from 'axios'; const api = axios.create({ baseURL: 'http://localhost:3000/api' }); export default { getVersions: (appId, page = 1, size = 10) => api.get('/versions', { params: { app_id: appId, page, size } }), createVersion: (versionData, file) => { const formData = new FormData(); formData.append('data', JSON.stringify(versionData)); if (file) formData.append('package_file', file); return api.post('/versions', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); }, deleteVersions: (ids) => api.delete('/versions', { data: { ids } }) }; ``` #### router/index.js ```js import { createRouter, createWebHistory } from 'vue-router' // 导入组件 const Home = () => import('@/views/AppVersionView.vue') const ceshi = () => import('@/views/ceshi.vue') const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/ceshi', name: 'ceshi', component: ceshi } ] const router = createRouter({ history: createWebHistory(), routes }) export default router ``` #### main.js ```js import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' import router from './router' // 自动识别 ./router/index.js const app = createApp(App) app.use(ElementPlus) app.use(router) // 启用路由 app.mount('#app') ``` #### components/VersionList.vue ```vue ``` #### components/VersionForm.vue ```vue ``` #### components/CreateVersionDialog.vue ```js ``` #### views/AppVersionView.vue ```vue ``` #### App.vue ```vue ``` 至此后台系统已经完成 ## 本地后端 - 后端采用nodejs + server2016,数据库和node安装自己去找教程 - node版本我是: 22.12.0 ### 创建应用版本表 ```sql CREATE TABLE AppVersions ( id INT PRIMARY KEY IDENTITY(1,1), app_id NVARCHAR(100) NOT NULL, -- 应用ID app_name NVARCHAR(100) NOT NULL, -- 应用名称 update_title NVARCHAR(200) NOT NULL, -- 更新标题 update_content NVARCHAR(MAX) NOT NULL, -- 更新内容 platform NVARCHAR(20) NOT NULL, -- 平台(android/ios) version NVARCHAR(50) NOT NULL, -- 版本号 version_code INT NOT NULL, -- 内部版本号(用于比较) package_type TINYINT NOT NULL, -- 0:整包, 1:wgt资源包 download_url NVARCHAR(500), -- 下载链接 file_path NVARCHAR(500), -- 本地文件存储路径 min_version NVARCHAR(50), -- 原生app最低版本(仅wgt包使用) silent_update BIT DEFAULT 0, -- 静默更新(默认关闭) force_update BIT DEFAULT 0, -- 强制更新(默认关闭) status BIT DEFAULT 0, -- 上线发行状态(默认关闭) upload_date DATETIME DEFAULT GETDATE() -- 上传日期 ); ``` ### node实现 #### #### 安装依赖 ```bash npm install express mssql body-parser cors multer ``` #### 创建入口文件 app.js **注意SQL Server配置,不然连不上数据库** ```js const express = require('express'); const bodyParser = require('body-parser'); const cors = require('cors'); const multer = require('multer'); const sql = require('mssql'); const app = express(); const PORT = 3000; // 在文件上传配置前添加: const fs = require('fs'); const path = require('path'); app.use(cors()); app.use(bodyParser.json()); app.use('/uploads', express.static('uploads')); // SQL Server配置 // ****** 注意这里的配置,一定是自己的信息 ************* const dbConfig = { user: 'sa', password: 'root', server: '127.0.0.1', database: 'myServer', options: { encrypt: true, trustServerCertificate: true } }; // 确保上传目录存在 const uploadDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } // 文件上传配置 const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, uploadDir); }, filename: (req, file, cb) => { const ext = file.originalname.split('.').pop(); cb(null, `${file.fieldname}-${Date.now()}.${ext}`); } }); const upload = multer({ storage }); // 初始化数据库连接 sql.connect(dbConfig).catch(err => console.error('Database connection failed:', err)); // 版本检查接口 app.get('/api/check-update', async (req, res) => { try { const { edition_type, version_type, edition_number } = req.query; const pool = await sql.connect(dbConfig); const result = await pool.request() .input('app_id', sql.NVarChar, edition_type) .input('platform', sql.NVarChar, version_type) .input('version_code', sql.Int, parseInt(edition_number)) .query(` SELECT TOP 1 * FROM AppVersions WHERE app_id = @app_id AND platform = @platform AND version_code > @version_code AND status = 1 ORDER BY version_code DESC `); if (result.recordset.length === 0) { return res.status(404).json({ message: '无可用更新' }); } const updateInfo = result.recordset[0]; let downloadUrl = updateInfo.download_url; if (!downloadUrl && updateInfo.file_path) { // 构建完整的 HTTP URL const protocol = req.protocol; const host = req.get('host'); downloadUrl = `${protocol}://${host}${updateInfo.file_path}`; } res.json({ describe: updateInfo.update_content.replace(/\n/g, '
'), edition_url: downloadUrl, edition_force: updateInfo.force_update ? 1 : 0, package_type: updateInfo.package_type, edition_versionCode: updateInfo.version_code.toString(), edition_version: updateInfo.version, edition_silence: updateInfo.silent_update ? 1 : 0, }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 获取版本列表 app.get('/api/versions', async (req, res) => { try { const { app_id, page = 1, size = 10 } = req.query; const offset = (page - 1) * size; const pool = await sql.connect(dbConfig); let query = 'SELECT * FROM AppVersions'; let where = []; if (app_id) where.push(`app_id = '${app_id}'`); if (where.length) query += ` WHERE ${where.join(' AND ')}`; query += ` ORDER BY upload_date DESC OFFSET ${offset} ROWS FETCH NEXT ${size} ROWS ONLY`; const countQuery = 'SELECT COUNT(*) as total FROM AppVersions' + (where.length ? ` WHERE ${where.join(' AND ')}` : ''); const [result, countResult] = await Promise.all([ pool.request().query(query), pool.request().query(countQuery) ]); res.json({ versions: result.recordset, total: countResult.recordset[0].total }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 添加版本 app.post('/api/versions', upload.single('package_file'), async (req, res) => { try { const versionData = JSON.parse(req.body.data); let filePath = null; if (req.file) { // 使用 path.basename() 只获取文件名 const fileName = path.basename(req.file.path); filePath = `/uploads/${fileName}`; } const pool = await sql.connect(dbConfig); await pool.request() .input('app_id', sql.NVarChar, versionData.app_id) .input('app_name', sql.NVarChar, versionData.app_name) .input('update_title', sql.NVarChar, versionData.update_title) .input('update_content', sql.NVarChar, versionData.update_content) .input('platform', sql.NVarChar, versionData.platform) .input('version', sql.NVarChar, versionData.version) .input('version_code', sql.Int, versionData.version_code) .input('package_type', sql.TinyInt, versionData.package_type) .input('download_url', sql.NVarChar, versionData.download_url) .input('file_path', sql.NVarChar, filePath) .input('min_version', sql.NVarChar, versionData.min_version || null) .input('silent_update', sql.Bit, versionData.silent_update || false) .input('force_update', sql.Bit, versionData.force_update || false) .input('status', sql.Bit, versionData.status || false) .query(` INSERT INTO AppVersions ( app_id, app_name, update_title, update_content, platform, version, version_code, package_type, download_url, file_path, min_version, silent_update, force_update, status ) VALUES ( @app_id, @app_name, @update_title, @update_content, @platform, @version, @version_code, @package_type, @download_url, @file_path, @min_version, @silent_update, @force_update, @status ) `); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 删除版本(单/批量) app.delete('/api/versions', async (req, res) => { try { const ids = req.body.ids; if (!ids || !Array.isArray(ids) || ids.length === 0) { return res.status(400).json({ error: '缺少版本ID参数' }); } const pool = await sql.connect(dbConfig); await pool.request().query(`DELETE FROM AppVersions WHERE id IN (${ids.join(',')})`); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); ``` **🚀🚀🚀至此本地流程已经搭建完成,你可以放心大胆的测试了。如果中途出现什么问题,我给你找了几个老师,可以问他们。** 📌 [老师1](https://yuanbao.tencent.com/) 📌 [老师2](https://chat.deepseek.com/) 📌 [老师3](https://www.doubao.com/) 📌 [老师4](https://chatgpt.com/) 📌 [老师5](https://poe.com/)