225 lines
7.6 KiB
JavaScript
225 lines
7.6 KiB
JavaScript
'use strict';
|
|
|
|
const database = require('../config/database');
|
|
|
|
const APP_TABLE = 'external_automation_apps';
|
|
const PLATFORMS = new Set(['windows', 'macos', 'linux', 'all']);
|
|
|
|
let schemaReady = false;
|
|
let schemaPromise = null;
|
|
|
|
function normalizeText(value, maxLength = 255) {
|
|
return String(value ?? '').trim().slice(0, maxLength);
|
|
}
|
|
|
|
function normalizeId(value) {
|
|
const id = Number.parseInt(value, 10);
|
|
if (!Number.isFinite(id) || id <= 0) {
|
|
const error = new Error('App 链接 ID 不正确');
|
|
error.status = 400;
|
|
throw error;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
function normalizeUrl(value, { required = false, label = '链接' } = {}) {
|
|
const text = normalizeText(value, 2000);
|
|
if (!text && !required) return '';
|
|
if (!text) {
|
|
const error = new Error(`请输入${label}`);
|
|
error.status = 400;
|
|
throw error;
|
|
}
|
|
try {
|
|
const url = new URL(text);
|
|
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('unsupported protocol');
|
|
return url.toString();
|
|
} catch {
|
|
const error = new Error(`${label}必须是有效的 HTTP 或 HTTPS 地址`);
|
|
error.status = 400;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function getActor(user = {}) {
|
|
return normalizeText(user.name || user.username || user.userName || user.email || user.sub, 120) || null;
|
|
}
|
|
|
|
function mapAppRow(row = {}) {
|
|
return {
|
|
id: Number(row.id),
|
|
name: row.name || '',
|
|
platform: row.platform || 'all',
|
|
version: row.version || '',
|
|
downloadUrl: row.download_url || '',
|
|
releaseNotesUrl: row.release_notes_url || '',
|
|
description: row.description || '',
|
|
enabled: row.status === 'active',
|
|
sortOrder: Number(row.sort_order || 0),
|
|
createdBy: row.created_by_name || '',
|
|
updatedBy: row.updated_by_name || '',
|
|
createdAt: row.created_at || null,
|
|
updatedAt: row.updated_at || null,
|
|
};
|
|
}
|
|
|
|
async function ensureExternalAutomationAppSchema() {
|
|
if (schemaReady) return;
|
|
if (schemaPromise) return schemaPromise;
|
|
schemaPromise = database.local.query(
|
|
`CREATE TABLE IF NOT EXISTS ${APP_TABLE} (
|
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
|
name VARCHAR(120) NOT NULL COMMENT 'App名称',
|
|
platform ENUM('windows', 'macos', 'linux', 'all') NOT NULL DEFAULT 'windows' COMMENT '适用平台',
|
|
version VARCHAR(80) NULL COMMENT '版本号',
|
|
download_url VARCHAR(2000) NOT NULL COMMENT '下载或启动链接',
|
|
release_notes_url VARCHAR(2000) NULL COMMENT '更新说明链接',
|
|
description VARCHAR(500) NULL COMMENT '备注',
|
|
status ENUM('active', 'disabled') NOT NULL DEFAULT 'active' COMMENT '状态',
|
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序值',
|
|
created_by_name VARCHAR(120) NULL COMMENT '创建人',
|
|
updated_by_name VARCHAR(120) NULL COMMENT '更新人',
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
KEY idx_external_automation_app_status_sort (status, sort_order, updated_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='自动上架第三方App链接'`
|
|
).then(() => {
|
|
schemaReady = true;
|
|
});
|
|
try {
|
|
await schemaPromise;
|
|
} finally {
|
|
schemaPromise = null;
|
|
}
|
|
}
|
|
|
|
async function getAppById(id) {
|
|
await ensureExternalAutomationAppSchema();
|
|
const normalizedId = normalizeId(id);
|
|
const rows = await database.local.query(`SELECT * FROM ${APP_TABLE} WHERE id = ? LIMIT 1`, [normalizedId]);
|
|
if (!rows[0]) {
|
|
const error = new Error('App 链接不存在');
|
|
error.status = 404;
|
|
throw error;
|
|
}
|
|
return rows[0];
|
|
}
|
|
|
|
async function listExternalAutomationApps() {
|
|
await ensureExternalAutomationAppSchema();
|
|
const rows = await database.local.query(
|
|
`SELECT * FROM ${APP_TABLE}
|
|
ORDER BY status = 'active' DESC, sort_order ASC, updated_at DESC, id DESC`
|
|
);
|
|
return rows.map(mapAppRow);
|
|
}
|
|
|
|
function normalizeAppInput(input = {}, { partial = false } = {}) {
|
|
const output = {};
|
|
if (!partial || Object.prototype.hasOwnProperty.call(input, 'name')) {
|
|
output.name = normalizeText(input.name, 120);
|
|
if (!output.name) {
|
|
const error = new Error('请输入 App 名称');
|
|
error.status = 400;
|
|
throw error;
|
|
}
|
|
}
|
|
if (!partial || Object.prototype.hasOwnProperty.call(input, 'platform')) {
|
|
output.platform = normalizeText(input.platform, 20).toLowerCase() || 'windows';
|
|
if (!PLATFORMS.has(output.platform)) {
|
|
const error = new Error('App 平台不正确');
|
|
error.status = 400;
|
|
throw error;
|
|
}
|
|
}
|
|
if (!partial || Object.prototype.hasOwnProperty.call(input, 'version')) {
|
|
output.version = normalizeText(input.version, 80);
|
|
}
|
|
if (!partial || Object.prototype.hasOwnProperty.call(input, 'downloadUrl')) {
|
|
output.downloadUrl = normalizeUrl(input.downloadUrl, { required: true, label: 'App 下载链接' });
|
|
}
|
|
if (!partial || Object.prototype.hasOwnProperty.call(input, 'releaseNotesUrl')) {
|
|
output.releaseNotesUrl = normalizeUrl(input.releaseNotesUrl, { label: '更新说明链接' });
|
|
}
|
|
if (!partial || Object.prototype.hasOwnProperty.call(input, 'description')) {
|
|
output.description = normalizeText(input.description, 500);
|
|
}
|
|
if (!partial || Object.prototype.hasOwnProperty.call(input, 'enabled')) {
|
|
output.enabled = input.enabled !== false;
|
|
}
|
|
if (!partial || Object.prototype.hasOwnProperty.call(input, 'sortOrder')) {
|
|
const sortOrder = Number.parseInt(input.sortOrder, 10);
|
|
output.sortOrder = Number.isFinite(sortOrder) ? Math.max(-9999, Math.min(9999, sortOrder)) : 0;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
async function createExternalAutomationApp(input = {}, user = {}) {
|
|
await ensureExternalAutomationAppSchema();
|
|
const data = normalizeAppInput(input);
|
|
const actor = getActor(user);
|
|
const result = await database.local.insert(
|
|
`INSERT INTO ${APP_TABLE}
|
|
(name, platform, version, download_url, release_notes_url, description, status, sort_order, created_by_name, updated_by_name)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
data.name,
|
|
data.platform,
|
|
data.version || null,
|
|
data.downloadUrl,
|
|
data.releaseNotesUrl || null,
|
|
data.description || null,
|
|
data.enabled ? 'active' : 'disabled',
|
|
data.sortOrder,
|
|
actor,
|
|
actor,
|
|
]
|
|
);
|
|
return mapAppRow(await getAppById(result.insertId));
|
|
}
|
|
|
|
async function updateExternalAutomationApp(id, input = {}, user = {}) {
|
|
const existing = await getAppById(id);
|
|
const data = normalizeAppInput(input, { partial: true });
|
|
const columnMap = {
|
|
name: 'name',
|
|
platform: 'platform',
|
|
version: 'version',
|
|
downloadUrl: 'download_url',
|
|
releaseNotesUrl: 'release_notes_url',
|
|
description: 'description',
|
|
enabled: 'status',
|
|
sortOrder: 'sort_order',
|
|
};
|
|
const updates = [];
|
|
const params = [];
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
updates.push(`${columnMap[key]} = ?`);
|
|
params.push(key === 'enabled'
|
|
? (value ? 'active' : 'disabled')
|
|
: (['version', 'releaseNotesUrl', 'description'].includes(key) ? (value || null) : value));
|
|
});
|
|
updates.push('updated_by_name = ?');
|
|
params.push(getActor(user));
|
|
await database.local.query(
|
|
`UPDATE ${APP_TABLE} SET ${updates.join(', ')} WHERE id = ?`,
|
|
[...params, existing.id]
|
|
);
|
|
return mapAppRow(await getAppById(existing.id));
|
|
}
|
|
|
|
async function deleteExternalAutomationApp(id) {
|
|
const existing = await getAppById(id);
|
|
await database.local.query(`DELETE FROM ${APP_TABLE} WHERE id = ?`, [existing.id]);
|
|
return mapAppRow(existing);
|
|
}
|
|
|
|
module.exports = {
|
|
createExternalAutomationApp,
|
|
deleteExternalAutomationApp,
|
|
ensureExternalAutomationAppSchema,
|
|
listExternalAutomationApps,
|
|
normalizeAppInput,
|
|
updateExternalAutomationApp,
|
|
};
|