Browse Source

feat: add download version APIs

Pchen. 1 week ago
parent
commit
84bb871d93

+ 37 - 0
apis/Download/Version/Admin/Delete.js

@@ -0,0 +1,37 @@
+const API = require("../../../../lib/API");
+const AccessControl = require("../../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../../BaseStdResponse");
+const VersionService = require("../../../../lib/DownloadVersionService");
+
+class AdminDownloadVersionDelete extends API {
+    constructor() {
+        super();
+        this.setPath('/Admin/DownloadVersion');
+        this.setMethod('DELETE');
+    }
+
+    async onRequest(req, res) {
+        const { uuid, session, id } = req.body;
+        if ([uuid, session, id].some(v => v === '' || v === null || v === undefined))
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
+
+        const permission = await AccessControl.getPermission(uuid);
+        if (!permission.includes('admin') && !permission.includes('service'))
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
+
+        try {
+            const result = await VersionService.deleteVersion(id);
+            if (!result || result.affectedRows === 0)
+                return res.json({ ...BaseStdResponse.ERR, msg: '删除下载版本失败' });
+            return res.json({ ...BaseStdResponse.OK });
+        } catch (err) {
+            this.logger.error(`删除下载版本失败:${err.stack || err}`);
+            return res.json({ ...BaseStdResponse.ERR, msg: '删除下载版本失败' });
+        }
+    }
+}
+
+module.exports.AdminDownloadVersionDelete = AdminDownloadVersionDelete;

+ 35 - 0
apis/Download/Version/Admin/List.js

@@ -0,0 +1,35 @@
+const API = require("../../../../lib/API");
+const AccessControl = require("../../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../../BaseStdResponse");
+const VersionService = require("../../../../lib/DownloadVersionService");
+
+class AdminDownloadVersionList extends API {
+    constructor() {
+        super();
+        this.setPath('/Admin/DownloadVersion/List');
+        this.setMethod('GET');
+    }
+
+    async onRequest(req, res) {
+        const { uuid, session, client_type } = req.query;
+        if ([uuid, session].some(v => v === '' || v === null || v === undefined))
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
+
+        const permission = await AccessControl.getPermission(uuid);
+        if (!permission.includes('admin') && !permission.includes('service'))
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
+
+        try {
+            const rows = await VersionService.listVersions(client_type || "");
+            return res.json({ ...BaseStdResponse.OK, data: rows || [] });
+        } catch (err) {
+            this.logger.error(`获取下载版本列表失败:${err.stack || err}`);
+            return res.json({ ...BaseStdResponse.ERR, msg: '获取下载版本列表失败' });
+        }
+    }
+}
+
+module.exports.AdminDownloadVersionList = AdminDownloadVersionList;

+ 37 - 0
apis/Download/Version/Admin/Upsert.js

@@ -0,0 +1,37 @@
+const API = require("../../../../lib/API");
+const AccessControl = require("../../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../../BaseStdResponse");
+const VersionService = require("../../../../lib/DownloadVersionService");
+
+class AdminDownloadVersionUpsert extends API {
+    constructor() {
+        super();
+        this.setPath('/Admin/DownloadVersion');
+        this.setMethod('POST');
+    }
+
+    async onRequest(req, res) {
+        const { uuid, session, client_type, version_name, download_url } = req.body;
+        if ([uuid, session, client_type, version_name, download_url].some(v => v === '' || v === null || v === undefined))
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
+
+        const permission = await AccessControl.getPermission(uuid);
+        if (!permission.includes('admin') && !permission.includes('service'))
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
+
+        try {
+            const { id, result } = await VersionService.saveVersion(req.body);
+            if (!result || result.affectedRows === 0)
+                return res.json({ ...BaseStdResponse.ERR, msg: '保存下载版本失败' });
+            return res.json({ ...BaseStdResponse.OK, id });
+        } catch (err) {
+            this.logger.error(`保存下载版本失败:${err.stack || err}`);
+            return res.json({ ...BaseStdResponse.ERR, msg: '保存下载版本失败' });
+        }
+    }
+}
+
+module.exports.AdminDownloadVersionUpsert = AdminDownloadVersionUpsert;

+ 37 - 0
apis/Public/CheckClientVersion.js

@@ -0,0 +1,37 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const VersionService = require("../../lib/DownloadVersionService");
+
+class CheckClientVersion extends API {
+    constructor() {
+        super();
+        this.setPath('/Public/CheckClientVersion');
+        this.setMethod('GET');
+        this.noEncrypt();
+    }
+
+    async onRequest(req, res) {
+        const clientType = VersionService.normalizeClientType(req.query.client_type || req.query.clientType);
+        const versionName = req.query.version || req.query.version_name || "";
+        const versionCode = req.query.version_code || req.query.versionCode || 0;
+
+        if (!clientType)
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER, msg: 'client_type不能为空' });
+
+        try {
+            const latest = await VersionService.getLatestVersion(clientType);
+            return res.json({
+                ...BaseStdResponse.OK,
+                data: VersionService.toPublicPayload(latest, {
+                    version_name: versionName,
+                    version_code: versionCode
+                })
+            });
+        } catch (err) {
+            this.logger.error(`检查客户端版本失败:${err.stack || err}`);
+            return res.json({ ...BaseStdResponse.ERR, msg: '检查客户端版本失败' });
+        }
+    }
+}
+
+module.exports.CheckClientVersion = CheckClientVersion;

+ 26 - 17
apis/Public/GetAndroidAppVersion.js

@@ -1,32 +1,41 @@
 const API = require("../../lib/API");
 const API = require("../../lib/API");
 const { BaseStdResponse } = require("../../BaseStdResponse");
 const { BaseStdResponse } = require("../../BaseStdResponse");
+const VersionService = require("../../lib/DownloadVersionService");
+
+function toUniAppVersion(row) {
+    if (!row) return null;
+    return {
+        describe: row.release_notes_html || row.release_notes || '',
+        edition_url: row.download_url,
+        edition_force: Number(row.force_update) || 0,
+        package_type: Number(row.package_type) || 0,
+        edition_issue: Number(row.is_active) === 0 ? 0 : 1,
+        edition_number: Number(row.version_code) || 0,
+        edition_name: row.version_name,
+        edition_silence: Number(row.silent_update) || 0,
+        log: row.release_notes || ''
+    };
+}
 
 
 class GetAndroidAppVersion extends API {
 class GetAndroidAppVersion extends API {
     constructor() {
     constructor() {
         super();
         super();
-
         this.setPath('/Public/GetAndroidAppVersion')
         this.setPath('/Public/GetAndroidAppVersion')
         this.setMethod('GET')
         this.setMethod('GET')
     }
     }
 
 
     async onRequest(req, res) {
     async onRequest(req, res) {
-        const data = {
-            describe: '<p>1.修复无法登录乐跑小程序的问题</p> <p>2、本次升级为强制整包升级,请在更新包下载完毕后点击安装</p><p><small style="color: red">更新过程中请勿关闭程序或切到后台,以免导致升级失败</small></p>',
-            edition_url: 'https://lepao-cloud.xxoo365.top/down.php/1c1538a08b791cebc1aa4462ed051fad.apk', //apk、wgt包下载地址或者应用市场地址  安卓应用市场 market://details?id=xxxx 苹果store itms-apps://itunes.apple.com/cn/app/xxxxxx
-            edition_force: 1, //是否强制更新 0代表否 1代表是
-            package_type: 0, //0是整包升级(apk或者appstore或者安卓应用市场) 1是wgt升级
-            edition_issue: 1, //是否发行  0否 1是 为了控制上架应用市场审核时不能弹出热更新框
-            edition_number: 131, //版本号 最重要的manifest里的版本号 (检查更新主要以服务器返回的edition_number版本号是否大于当前app的版本号来实现是否更新)
-            edition_name: '1.3.1', // 版本名称 manifest里的版本名称
-            edition_silence: 0, // 是否静默更新 0代表否 1代表是
-            log: '1、优化应用内核,加快响应速度; 2、修复已知问题' //系统设置内日志
+        try {
+            const latest = await VersionService.getLatestVersion("android");
+            return res.json({
+                ...BaseStdResponse.OK,
+                data: toUniAppVersion(latest)
+            })
+        } catch (err) {
+            this.logger.error(`读取安卓版本配置失败:${err.stack || err}`);
+            return res.json({ ...BaseStdResponse.ERR, msg: '读取安卓版本配置失败' });
         }
         }
-
-        res.json({
-            ...BaseStdResponse.OK,
-            data
-        })
     }
     }
 }
 }
 
 
-module.exports.GetAndroidAppVersion = GetAndroidAppVersion
+module.exports.GetAndroidAppVersion = GetAndroidAppVersion

+ 178 - 0
lib/DownloadVersionService.js

@@ -0,0 +1,178 @@
+const db = require("../plugin/DataBase/db");
+
+function normalizeClientType(value) {
+    return String(value || "").trim().toLowerCase();
+}
+
+function parseVersionParts(version) {
+    return String(version || "")
+        .trim()
+        .replace(/^[vV]/, "")
+        .split(/[.+_-]/)
+        .map(part => {
+            const n = Number.parseInt(part, 10);
+            return Number.isFinite(n) ? n : 0;
+        });
+}
+
+function compareVersionName(a, b) {
+    const left = parseVersionParts(a);
+    const right = parseVersionParts(b);
+    const len = Math.max(left.length, right.length);
+    for (let i = 0; i < len; i++) {
+        const diff = (left[i] || 0) - (right[i] || 0);
+        if (diff !== 0) return diff > 0 ? 1 : -1;
+    }
+    return 0;
+}
+
+function isVersionNewer(remote, local) {
+    const remoteCode = Number(remote.version_code);
+    const localCode = Number(local.version_code);
+    if (Number.isFinite(remoteCode) && remoteCode > 0 && Number.isFinite(localCode) && localCode > 0)
+        return remoteCode > localCode;
+    return compareVersionName(remote.version_name, local.version_name) > 0;
+}
+
+function toPublicPayload(row, localVersion = {}) {
+    if (!row) {
+        return {
+            has_update: false,
+            update_available: false
+        };
+    }
+    const remote = {
+        version_name: row.version_name,
+        version_code: Number(row.version_code) || 0
+    };
+    const local = {
+        version_name: localVersion.version_name,
+        version_code: localVersion.version_code
+    };
+    const hasUpdate = isVersionNewer(remote, local);
+    return {
+        has_update: hasUpdate,
+        update_available: hasUpdate,
+        client_type: row.client_type,
+        title: row.title || "",
+        version: row.version_name,
+        version_name: row.version_name,
+        version_code: Number(row.version_code) || 0,
+        download_url: row.download_url,
+        sha256: row.sha256 || "",
+        file_size: Number(row.file_size) || 0,
+        package_type: Number(row.package_type) || 0,
+        force_update: Number(row.force_update) || 0,
+        silent_update: Number(row.silent_update) || 0,
+        release_notes: row.release_notes || "",
+        release_notes_html: row.release_notes_html || "",
+        min_supported_version: row.min_supported_version || ""
+    };
+}
+
+async function getLatestVersion(clientType) {
+    const normalized = normalizeClientType(clientType);
+    const rows = await db.query(`
+        SELECT id, client_type, version_name, version_code, title, download_url,
+               sha256, file_size, package_type, force_update, silent_update,
+               is_active, release_notes, release_notes_html, min_supported_version,
+               sort_order, created_at, updated_at
+        FROM download_version
+        WHERE client_type = ? AND is_active = 1
+        ORDER BY version_code DESC, sort_order ASC, id DESC
+        LIMIT 1
+    `, [normalized]);
+    return rows && rows[0] ? rows[0] : null;
+}
+
+async function listVersions(clientType = "") {
+    const normalized = normalizeClientType(clientType);
+    if (normalized) {
+        return await db.query(`
+            SELECT id, client_type, version_name, version_code, title, download_url,
+                   sha256, file_size, package_type, force_update, silent_update,
+                   is_active, release_notes, release_notes_html, min_supported_version,
+                   sort_order, created_at, updated_at
+            FROM download_version
+            WHERE client_type = ?
+            ORDER BY client_type ASC, version_code DESC, sort_order ASC, id DESC
+        `, [normalized]);
+    }
+    return await db.query(`
+        SELECT id, client_type, version_name, version_code, title, download_url,
+               sha256, file_size, package_type, force_update, silent_update,
+               is_active, release_notes, release_notes_html, min_supported_version,
+               sort_order, created_at, updated_at
+        FROM download_version
+        ORDER BY client_type ASC, version_code DESC, sort_order ASC, id DESC
+    `);
+}
+
+async function saveVersion(payload) {
+    const now = Date.now();
+    const data = {
+        client_type: normalizeClientType(payload.client_type),
+        version_name: String(payload.version_name || payload.version || "").trim(),
+        version_code: Number(payload.version_code) || 0,
+        title: String(payload.title || "").trim(),
+        download_url: String(payload.download_url || "").trim(),
+        sha256: String(payload.sha256 || "").trim().toLowerCase(),
+        file_size: Number(payload.file_size) || 0,
+        package_type: Number(payload.package_type) || 0,
+        force_update: Number(payload.force_update) === 1 ? 1 : 0,
+        silent_update: Number(payload.silent_update) === 1 ? 1 : 0,
+        is_active: Number(payload.is_active) === 0 ? 0 : 1,
+        release_notes: payload.release_notes || "",
+        release_notes_html: payload.release_notes_html || "",
+        min_supported_version: String(payload.min_supported_version || "").trim(),
+        sort_order: Number(payload.sort_order) || 0
+    };
+
+    if (payload.id) {
+        const result = await db.query(`
+            UPDATE download_version SET
+                client_type = ?, version_name = ?, version_code = ?, title = ?,
+                download_url = ?, sha256 = ?, file_size = ?, package_type = ?,
+                force_update = ?, silent_update = ?, is_active = ?,
+                release_notes = ?, release_notes_html = ?, min_supported_version = ?,
+                sort_order = ?, updated_at = ?
+            WHERE id = ?
+        `, [
+            data.client_type, data.version_name, data.version_code, data.title,
+            data.download_url, data.sha256, data.file_size, data.package_type,
+            data.force_update, data.silent_update, data.is_active,
+            data.release_notes, data.release_notes_html, data.min_supported_version,
+            data.sort_order, now, payload.id
+        ]);
+        return { id: payload.id, result };
+    }
+
+    const result = await db.query(`
+        INSERT INTO download_version
+            (client_type, version_name, version_code, title, download_url,
+             sha256, file_size, package_type, force_update, silent_update,
+             is_active, release_notes, release_notes_html, min_supported_version,
+             sort_order, created_at, updated_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    `, [
+        data.client_type, data.version_name, data.version_code, data.title,
+        data.download_url, data.sha256, data.file_size, data.package_type,
+        data.force_update, data.silent_update, data.is_active,
+        data.release_notes, data.release_notes_html, data.min_supported_version,
+        data.sort_order, now, now
+    ]);
+    return { id: result && result.insertId, result };
+}
+
+async function deleteVersion(id) {
+    return await db.query("DELETE FROM download_version WHERE id = ?", [id]);
+}
+
+module.exports = {
+    getLatestVersion,
+    listVersions,
+    saveVersion,
+    deleteVersion,
+    toPublicPayload,
+    normalizeClientType
+};

+ 23 - 0
sql/download_version.sql

@@ -0,0 +1,23 @@
+CREATE TABLE IF NOT EXISTS download_version (
+    id INT NOT NULL AUTO_INCREMENT,
+    client_type VARCHAR(64) NOT NULL,
+    version_name VARCHAR(64) NOT NULL,
+    version_code INT NOT NULL DEFAULT 0,
+    title VARCHAR(128) NOT NULL DEFAULT '',
+    download_url TEXT NOT NULL,
+    sha256 VARCHAR(128) NOT NULL DEFAULT '',
+    file_size BIGINT NOT NULL DEFAULT 0,
+    package_type TINYINT NOT NULL DEFAULT 0,
+    force_update TINYINT NOT NULL DEFAULT 0,
+    silent_update TINYINT NOT NULL DEFAULT 0,
+    is_active TINYINT NOT NULL DEFAULT 1,
+    release_notes TEXT,
+    release_notes_html TEXT,
+    min_supported_version VARCHAR(64) NOT NULL DEFAULT '',
+    sort_order INT NOT NULL DEFAULT 0,
+    created_at BIGINT NOT NULL DEFAULT 0,
+    updated_at BIGINT NOT NULL DEFAULT 0,
+    PRIMARY KEY (id),
+    KEY idx_client_active (client_type, is_active),
+    KEY idx_client_version_code (client_type, version_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;