更新记录

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}`);
});

🚀🚀🚀至此本地流程已经搭建完成,你可以放心大胆的测试了。如果中途出现什么问题,我给你找了几个老师,可以问他们。

📌 老师1 📌 老师2 📌 老师3 📌 老师4 📌 老师5

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议

暂无用户评论。