更新记录
1.0.2(2025-08-18) 下载此版本
添加示例demo
1.0.1(2025-08-18) 下载此版本
优化图片不显示问题
1.0.0(2025-08-18) 下载此版本
安卓v3测试成功
查看更多平台兼容性
uni-app(4.76)
Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
---|---|---|---|---|---|---|---|---|
√ | √ | - | - | √ | - | √ | - | - |
微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 快应用-华为 | 快应用-联盟 |
---|---|---|---|---|---|---|---|---|---|---|
- | - | - | - | - | - | - | - | - | - | - |
App整包升级和热更新
-
因为自用,本插件后期可能不会维护,追求长期稳定的可以跳转原作者赵永强。
-
目前只测试了安卓-vue3的升级流程(其他环境下应该也没问题)。
-
采用uni_modules版本,带完整本地升级测试后台。
安装指引
1. pages.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,这个根据自己的情况来
<script setup>
import { ref } from 'vue';
import { getAppVersionAPI } from '@/api/update.js';
import silenceUpdate from '@/uni_modules/dhh-upgrade-app/js_sdk/silence-update.js';
import { onLaunch } from '@dcloudio/uni-app';
// App应用升级
const getCheckUpdateData = async () => {
// 获取应用信息
const inf = await new Promise(resolve => {
plus.runtime.getProperty(plus.runtime.appid, inf => {
resolve(inf);
});
});
// 接口传递的参数
const data = {
edition_type: uni.getSystemInfoSync().appId, // appid
version_type: uni.getSystemInfoSync().platform, // android或者ios
edition_number: inf.versionCode, // 打包时manifest设置的版本号
};
try {
const res = await getAppVersionAPI(data);
// 判断后端内部版本是否大于当前应用版本,并且是否强制更新
if (Number(res.edition_versionCode) > Number(inf.versionCode) && res.edition_force == 1) {
// 如果是wgt升级(1),并且静默更新(1)
if (res.package_type == 1 && res.edition_silence == 1) {
silenceUpdate(res.edition_url); // 静默更新
} else {
// 否则强制整包升级
uni.navigateTo({
url: '/uni_modules/dhh-upgrade-app/components/dhh-upgrade-app/dhh-upgrade-app?obj=' + JSON.stringify(res),
});
}
// 非强制更新: 整包和wgt
} else if (Number(res.edition_versionCode) > Number(inf.versionCode) && res.edition_force == 0) {
uni.navigateTo({
url: '/uni_modules/dhh-upgrade-app/components/dhh-upgrade-app/dhh-upgrade-app?obj=' + JSON.stringify(res),
});
} else {
// 不升级,可做其他操作
}
} catch (err) {
console.log('异常', err);
uni.showToast({ title: err.info || 'App升级接口异常,请联系俺管理员', icon: 'none' });
}
};
onLaunch(async () => {
await getCheckUpdateData();
});
</script>
<style>
/*每个页面公共css */
</style>
3. 接口 update.js
// api/update.js
import { http } from '@/utils/http'
// app版本信息
export const getAppVersionAPI = (data) => {
return http({
method: 'GET',
url: '/check-update',
data
})
}
// 接口返回数据
{
describe: '1. 修复已知问题<br>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
// 请求基地址
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地址
本地升级后台系统 VUE3 + Elementplus
依赖
{
"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
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
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
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
<template>
<div>
<el-table
:data="versions"
v-loading="loading"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="app_id" label="应用ID" width="160" />
<el-table-column prop="update_title" label="更新标题" min-width="180" />
<el-table-column label="安装包类型" width="120">
<template #default="{ row }">
{{ row.package_type === 0 ? "整包升级" : "wgt升级" }}
</template>
</el-table-column>
<el-table-column label="平台" width="100">
<template #default="{ row }">
<el-tag :type="row.platform === 'android' ? 'success' : 'warning'">
{{ row.platform === "android" ? "安卓" : "iOS" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="version" label="版本号" width="120" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status ? 'success' : 'info'">
{{ row.status ? "已上线" : "未上线" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="upload_date" label="上传日期" width="180" />
<el-table-column label="操作" fixed="right" width="160">
<template #default="{ row }">
<el-button type="danger" size="small" @click="deleteVersion(row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="toolbar">
<el-button
type="danger"
:disabled="selectedIds.length === 0"
@click="confirmBatchDelete"
>
批量删除 ({{ selectedIds.length }})
</el-button>
<el-pagination
:current-page="page"
:page-size="size"
:total="total"
layout="total, sizes, prev, pager, next"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, watch } from "vue";
import { ElMessageBox } from "element-plus";
const props = defineProps({
versions: Array,
loading: Boolean,
total: Number,
page: Number,
size: Number,
});
const emit = defineEmits(["delete-selected", "update:page", "update:size"]);
const selectedIds = ref([]);
// 监听数据变化,清空选中状态
watch(
() => props.versions,
() => {
selectedIds.value = [];
},
{ deep: true }
);
// 选择行变化
const handleSelectionChange = (selection) => {
selectedIds.value = selection.map((item) => item.id);
};
// 单个删除
const deleteVersion = (id) => {
confirmDelete([id]);
};
// 批量删除确认
const confirmBatchDelete = () => {
if (selectedIds.value.length === 0) return;
confirmDelete(selectedIds.value);
};
// 确认删除
const confirmDelete = (ids) => {
ElMessageBox.confirm("确定删除所选版本吗?删除后无法恢复", "警告", {
confirmButtonText: "删除",
cancelButtonText: "取消",
type: "warning",
customClass: "delete-confirm-dialog",
})
.then(() => {
emit("delete-selected", ids);
})
.catch(() => {
// 取消操作
});
};
// 分页变化处理
const handlePageChange = (newPage) => {
emit("update:page", newPage);
};
const handleSizeChange = (newSize) => {
emit("update:size", newSize);
};
</script>
<style scoped>
.toolbar {
display: flex;
justify-content: space-between;
margin-top: 20px;
align-items: center;
}
/* 优化删除确认对话框样式 */
:deep(.delete-confirm-dialog) {
min-width: 350px;
}
</style>
components/VersionForm.vue
<template>
<el-form ref="formRef" :model="formData" label-width="140px">
<el-form-item label="当前App ID">
<el-input :value="app.id" disabled />
</el-form-item>
<el-form-item label="应用名称">
<el-input :value="app.name" disabled />
</el-form-item>
<el-form-item label="更新标题" prop="update_title" required>
<el-input v-model="formData.update_title" placeholder="输入更新标题" />
</el-form-item>
<el-form-item label="更新内容" prop="update_content" required>
<el-input
v-model="formData.update_content"
type="textarea"
:rows="3"
placeholder="描述更新内容,支持换行"
/>
</el-form-item>
<el-form-item label="选择平台" prop="platform" required>
<el-radio-group v-model="formData.platform">
<el-radio label="android">Android</el-radio>
<el-radio label="ios">iOS</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="版本号" prop="version" required>
<el-input v-model="formData.version" placeholder="例如:1.0.0" />
</el-form-item>
<el-form-item label="内部版本号" prop="version_code" required>
<el-input-number
v-model="formData.version_code"
:min="1"
controls-position="right"
/>
</el-form-item>
<el-form-item
v-if="showMinVersion"
label="原生App最低版本"
prop="min_version"
required
>
<el-input v-model="formData.min_version" placeholder="例如:1.0.0" />
</el-form-item>
<el-form-item label="上传安装包">
<el-upload
:file-list="fileList"
:auto-upload="false"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:limit="1"
>
<el-button type="primary">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">
仅支持上传{{ formData.platform }}的{{
packageType === "0" ? "安装包" : "wgt包"
}}文件
</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="或输入下载链接">
<el-input
v-model="formData.download_url"
placeholder="完整的下载链接地址"
/>
</el-form-item>
<template v-if="packageType === '1'">
<el-form-item label="静默更新" prop="silent_update">
<el-switch v-model="formData.silent_update" />
</el-form-item>
</template>
<el-form-item label="强制更新" prop="force_update">
<el-switch v-model="formData.force_update" />
</el-form-item>
<el-form-item label="上线发行" prop="status">
<el-switch v-model="formData.status" />
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, watch } from "vue";
const props = defineProps({
app: {
type: Object,
required: true,
},
packageType: {
type: String,
required: true,
},
showMinVersion: {
type: Boolean,
default: false,
},
silentDefault: {
type: Boolean,
default: false,
},
forceDefault: {
type: Boolean,
default: false,
},
});
const formData = ref({
app_id: props.app.id,
app_name: props.app.name,
update_title: "",
update_content: "",
platform: "android",
version: "",
version_code: null,
package_type: props.packageType,
download_url: "",
min_version: "",
silent_update: props.silentDefault,
force_update: props.forceDefault,
status: false,
});
const formRef = ref(null);
const fileList = ref([]);
const file = ref(null);
// 重置表单
const resetForm = () => {
formData.value = {
app_id: props.app.id,
app_name: props.app.name,
update_title: "",
update_content: "",
platform: "android",
version: "",
version_code: null,
package_type: props.packageType,
download_url: "",
min_version: "",
silent_update: props.silentDefault,
force_update: props.forceDefault,
status: false,
};
fileList.value = [];
file.value = null;
};
// 处理文件变化
const handleFileChange = (uploadFile) => {
fileList.value = [uploadFile];
file.value = uploadFile.raw;
};
// 处理文件移除
const handleFileRemove = () => {
fileList.value = [];
file.value = null;
};
// 表单验证
const validate = () => {
return new Promise((resolve, reject) => {
formRef.value.validate((valid) => {
if (valid) {
resolve({
formData: formData.value,
file: file.value,
});
} else {
reject(new Error("请填写完整表单信息"));
}
});
});
};
defineExpose({
resetForm,
validate,
});
</script>
components/CreateVersionDialog.vue
<template>
<el-dialog v-model="visible" title="发布新版本" width="800px">
<el-tabs v-model="activeTab">
<el-tab-pane label="原生App安装包" name="full">
<VersionForm
ref="fullForm"
:app="currentApp"
package-type="0"
:show-min-version="false"
/>
</el-tab-pane>
<el-tab-pane label="wgt资源包" name="wgt">
<VersionForm
ref="wgtForm"
:app="currentApp"
package-type="1"
:show-min-version="true"
:silent-default="true"
:force-default="true"
/>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="submitForm">发布</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
import { ElMessage } from 'element-plus';
import VersionForm from './VersionForm.vue';
import versionApi from '@/api/versionApi'; // 确保导入API
const props = defineProps({
modelValue: Boolean,
currentApp: Object
});
const emit = defineEmits(['update:modelValue', 'created']);
const visible = ref(false);
const activeTab = ref('full');
const fullForm = ref(null);
const wgtForm = ref(null);
const currentApp = computed(() => props.currentApp || {});
// 监听外部modelValue变化
watch(() => props.modelValue, (val) => {
visible.value = val;
});
// 监听弹窗显示状态变化
watch(visible, (val) => {
if (!val) {
emit('update:modelValue', false);
// 重置表单
if (fullForm.value) fullForm.value.resetForm();
if (wgtForm.value) wgtForm.value.resetForm();
}
});
// 提交表单
const submitForm = async () => {
try {
const formRef = activeTab.value === 'full' ? fullForm.value : wgtForm.value;
if (!formRef) {
throw new Error('表单未初始化');
}
const { formData, file } = await formRef.validate();
if (!formData.download_url && !file) {
throw new Error('请上传文件或填写下载链接');
}
// 确保app_id存在
if (!formData.app_id) {
formData.app_id = currentApp.value.id;
}
// 调用API
await versionApi.createVersion(formData, file);
ElMessage.success('版本创建成功');
visible.value = false;
emit('created');
} catch (error) {
ElMessage.error('创建失败: ' + (error.message || error));
}
};
</script>
views/AppVersionView.vue
<template>
<div class="app-version-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<h2>App版本管理</h2>
</div>
</template>
<div class="filter-container">
<el-select
v-model="selectedApp"
placeholder="选择应用"
@change="fetchVersions"
>
<el-option
v-for="app in apps"
:key="app.id"
:label="app.name"
:value="app.id"
/>
</el-select>
<el-button type="primary" @click="openCreateDialog" class="create-btn">
发布新版本
</el-button>
</div>
<VersionList
:loading="loading"
:versions="versions"
:total="total"
:page="page"
:size="size"
@delete-selected="deleteSelected"
@refresh="fetchVersions"
/>
</el-card>
<CreateVersionDialog
v-model="createDialogVisible"
:current-app="currentApp"
@created="onVersionCreated"
/>
</div>
</template>
<script setup>
import { ref, watch, computed } from "vue";
import { ElMessage } from "element-plus";
import VersionList from "@/components/VersionList.vue";
import CreateVersionDialog from "@/components/CreateVersionDialog.vue";
import versionApi from "@/api/versionApi";
// 演示数据
const apps = ref([
{ id: "__UNI__F0A5769", name: "升级app" },
{ id: "com.example.app1", name: "电商App" },
{ id: "com.example.app2", name: "社交App" },
]);
const selectedApp = ref("__UNI__F0A5769");
const currentApp = computed(() =>
apps.value.find((a) => a.id === selectedApp.value)
);
const versions = ref([]);
const total = ref(0);
const page = ref(1);
const size = ref(10);
const loading = ref(false);
// 弹窗控制
const createDialogVisible = ref(false);
// 加载版本列表
const fetchVersions = async () => {
try {
loading.value = true;
const response = await versionApi.getVersions(
selectedApp.value,
page.value,
size.value
);
versions.value = response.data.versions;
total.value = response.data.total;
} catch (error) {
ElMessage.error("获取版本列表失败: " + error.message);
} finally {
loading.value = false;
}
};
// 打开创建对话框
const openCreateDialog = () => {
if (!selectedApp.value) {
ElMessage.warning("请先选择一个应用");
return;
}
createDialogVisible.value = true;
};
// 删除选中的版本
const deleteSelected = async (ids) => {
try {
await versionApi.deleteVersions(ids);
ElMessage.success("删除成功");
// 删除后重新获取数据
await fetchVersions();
} catch (error) {
ElMessage.error("删除失败: " + error.message);
}
};
// 版本创建成功后的回调
const onVersionCreated = () => {
createDialogVisible.value = false;
fetchVersions();
};
// 监听分页变化
watch([page, size], fetchVersions);
// 初始化
fetchVersions();
</script>
<style scoped>
.app-version-container {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.box-card {
margin-bottom: 20px;
}
.filter-container {
display: flex;
margin-bottom: 20px;
gap: 15px;
align-items: center;
}
.create-btn {
margin-left: auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
App.vue
<script setup>
import { ref } from "vue";
</script>
<template>
<router-view />
<!-- 路由内容将在这里渲染 -->
</template>
<style scoped></style>
至此后台系统已经完成
本地后端
- 后端采用nodejs + server2016,数据库和node安装自己去找教程
- node版本我是: 22.12.0
创建应用版本表
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实现
安装依赖
npm install express mssql body-parser cors multer
创建入口文件 app.js
注意SQL Server配置,不然连不上数据库
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, '<br>'),
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}`);
});
🚀🚀🚀至此本地流程已经搭建完成,你可以放心大胆的测试了。如果中途出现什么问题,我给你找了几个老师,可以问他们。