367 lines
13 KiB
JavaScript
367 lines
13 KiB
JavaScript
'use strict';
|
||
|
||
// 活动报名(原价卡大促2.0 / 初始化复制SKU)云端任务服务。
|
||
// 自魔术师云后端 server.js 移植,存储由 data.json 改为本机系统库 MySQL 表。
|
||
// 响应契约与魔术师一致:{ code: 0, msg: 'ok', data };初始化记录不存在返回 code 3009。
|
||
|
||
const database = require('../config/database');
|
||
|
||
const PROMOTION_TABLE = 'pdd_campaign_promotion_tasks';
|
||
const COPY_SKU_TABLE = 'pdd_campaign_copy_sku_tasks';
|
||
const GOODS_INIT_TABLE = 'pdd_campaign_goods_init';
|
||
|
||
let tableReady = false;
|
||
|
||
function operatorKey(user = {}) {
|
||
return String(user.sub || user.userId || user.id || user.name || 'anonymous').slice(0, 64);
|
||
}
|
||
|
||
async function ensureTables() {
|
||
if (tableReady) return;
|
||
await database.local.query(
|
||
`CREATE TABLE IF NOT EXISTS ${PROMOTION_TABLE} (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
task_group_id BIGINT NOT NULL,
|
||
operator_key VARCHAR(64) NOT NULL,
|
||
mall_id VARCHAR(64) NOT NULL DEFAULT '',
|
||
goods_id VARCHAR(64) NOT NULL DEFAULT '',
|
||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||
settings_json MEDIUMTEXT NULL,
|
||
remark VARCHAR(255) NULL,
|
||
error_msg TEXT NULL,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
KEY idx_promo_operator (operator_key, mall_id),
|
||
KEY idx_promo_group (task_group_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='原价卡大促报名任务'`
|
||
);
|
||
await database.local.query(
|
||
`CREATE TABLE IF NOT EXISTS ${COPY_SKU_TABLE} (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
operator_key VARCHAR(64) NOT NULL,
|
||
mall_id VARCHAR(64) NOT NULL DEFAULT '',
|
||
goods_id VARCHAR(64) NOT NULL DEFAULT '',
|
||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||
error_msg TEXT NULL,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
KEY idx_copy_operator (operator_key, mall_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品初始化(复制SKU)任务'`
|
||
);
|
||
await database.local.query(
|
||
`CREATE TABLE IF NOT EXISTS ${GOODS_INIT_TABLE} (
|
||
goods_id VARCHAR(64) PRIMARY KEY,
|
||
operator_key VARCHAR(64) NOT NULL DEFAULT '',
|
||
data_json LONGTEXT NOT NULL,
|
||
add_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品初始化上报数据'`
|
||
);
|
||
tableReady = true;
|
||
}
|
||
|
||
async function query(sql, params = []) {
|
||
return database.local.query(sql, params);
|
||
}
|
||
|
||
function ok(data) {
|
||
return { code: 0, msg: 'ok', data: data === undefined ? null : data };
|
||
}
|
||
|
||
function fail(code, msg) {
|
||
return { code, msg, data: null };
|
||
}
|
||
|
||
function normalizeSettings(settings) {
|
||
if (!settings || typeof settings !== 'object') return settings;
|
||
return {
|
||
...settings,
|
||
mustActivityIds: Array.isArray(settings.mustActivityIds)
|
||
? settings.mustActivityIds.map(Number).filter(Number.isFinite)
|
||
: [],
|
||
otherActivityIds: Array.isArray(settings.otherActivityIds)
|
||
? settings.otherActivityIds.map(Number).filter(Number.isFinite)
|
||
: []
|
||
};
|
||
}
|
||
|
||
function mapPromotionRow(row) {
|
||
let settings = row.settings_json ? parseJsonSafe(row.settings_json) : {};
|
||
return {
|
||
id: Number(row.id),
|
||
mallId: Number(row.mall_id) || 0,
|
||
ext: { goodsId: String(row.goods_id || '') },
|
||
status: row.status,
|
||
settings,
|
||
remark: row.remark || '原价卡大促2.0',
|
||
taskGroupId: Number(row.task_group_id),
|
||
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null,
|
||
updatedAt: row.updated_at ? new Date(row.updated_at).toISOString() : null,
|
||
errorMsg: row.error_msg || ''
|
||
};
|
||
}
|
||
|
||
function parseJsonSafe(text) {
|
||
try {
|
||
return text ? JSON.parse(text) : {};
|
||
} catch {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
const TERMINAL_STATUSES = ['completed', 'failed', 'cancelled', 'expired'];
|
||
|
||
// ---------- 原价卡大促报名任务 ----------
|
||
|
||
async function createPromotionGroup({ operator, mallId, settings, goodsList, remark }) {
|
||
await ensureTables();
|
||
const normalized = normalizeSettings(settings);
|
||
const groupResult = await database.local.query(
|
||
`INSERT INTO ${PROMOTION_TABLE} (task_group_id, operator_key, mall_id, goods_id, status, settings_json, remark)
|
||
VALUES (0, ?, ?, '', 'pending', ?, ?)`,
|
||
[operatorKey(operator), String(mallId || ''), JSON.stringify(normalized || {}), String(remark || '原价卡大促2.0').slice(0, 255)]
|
||
);
|
||
const taskGroupId = groupResult.insertId;
|
||
const list = Array.isArray(goodsList) && goodsList.length ? goodsList : [{}];
|
||
const tasks = [];
|
||
for (const goods of list) {
|
||
const insertResult = await database.local.query(
|
||
`INSERT INTO ${PROMOTION_TABLE} (task_group_id, operator_key, mall_id, goods_id, status, settings_json, remark)
|
||
VALUES (?, ?, ?, ?, 'pending', ?, ?)`,
|
||
[
|
||
taskGroupId,
|
||
operatorKey(operator),
|
||
String(mallId || ''),
|
||
String(goods?.goodsId ?? ''),
|
||
JSON.stringify(normalized || {}),
|
||
String(remark || '原价卡大促2.0').slice(0, 255)
|
||
]
|
||
);
|
||
const rows = await query(`SELECT * FROM ${PROMOTION_TABLE} WHERE id = ?`, [insertResult.insertId]);
|
||
tasks.push({ task: mapPromotionRow(rows[0]) });
|
||
}
|
||
// 组行本身不作为任务,删除占位行
|
||
await database.local.query(`DELETE FROM ${PROMOTION_TABLE} WHERE id = ? AND goods_id = ''`, [taskGroupId]);
|
||
return { id: taskGroupId, originalPricePromotionTasks: tasks };
|
||
}
|
||
|
||
async function listPromotionGroups({ operator, mallId, page = 1, limit = 20 }) {
|
||
await ensureTables();
|
||
const key = operatorKey(operator);
|
||
const params = [key];
|
||
let mallFilter = '';
|
||
if (mallId) {
|
||
mallFilter = ' AND mall_id = ?';
|
||
params.push(String(mallId));
|
||
}
|
||
const rows = await query(
|
||
`SELECT * FROM ${PROMOTION_TABLE} WHERE operator_key = ?${mallFilter} ORDER BY id ASC`,
|
||
params
|
||
);
|
||
const groupMap = {};
|
||
const groupOrder = [];
|
||
for (const row of rows) {
|
||
const gid = Number(row.task_group_id) || Number(row.id);
|
||
if (!groupMap[gid]) {
|
||
groupMap[gid] = {
|
||
id: gid,
|
||
mallId: Number(row.mall_id) || 0,
|
||
remark: row.remark || '原价卡大促2.0',
|
||
settings: parseJsonSafe(row.settings_json),
|
||
status: 'pending',
|
||
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null,
|
||
updatedAt: row.created_at ? new Date(row.created_at).toISOString() : null,
|
||
originalPricePromotionTasks: []
|
||
};
|
||
groupOrder.push(gid);
|
||
}
|
||
const task = mapPromotionRow(row);
|
||
delete task.taskGroupId;
|
||
groupMap[gid].originalPricePromotionTasks.push({ task });
|
||
const status = row.status;
|
||
if (TERMINAL_STATUSES.includes(status)) groupMap[gid].status = status;
|
||
else if (status === 'running' && groupMap[gid].status === 'pending') groupMap[gid].status = 'running';
|
||
else if (status === 'completed' && groupMap[gid].status === 'pending') groupMap[gid].status = 'completed';
|
||
}
|
||
const groups = groupOrder.map((gid) => groupMap[gid]);
|
||
const normalizedPage = Math.max(Number(page) || 1, 1);
|
||
const normalizedLimit = Math.min(Math.max(Number(limit) || 20, 1), 500);
|
||
return {
|
||
list: groups.slice((normalizedPage - 1) * normalizedLimit, normalizedPage * normalizedLimit),
|
||
total: groups.length
|
||
};
|
||
}
|
||
|
||
// ---------- 初始化(复制SKU)任务 ----------
|
||
|
||
async function createCopySkuTask({ operator, mallId, goodsId }) {
|
||
await ensureTables();
|
||
const insertResult = await database.local.query(
|
||
`INSERT INTO ${COPY_SKU_TABLE} (operator_key, mall_id, goods_id, status)
|
||
VALUES (?, ?, ?, 'pending')`,
|
||
[operatorKey(operator), String(mallId || ''), String(goodsId || '')]
|
||
);
|
||
const rows = await query(`SELECT * FROM ${COPY_SKU_TABLE} WHERE id = ?`, [insertResult.insertId]);
|
||
const row = rows[0];
|
||
return {
|
||
task: {
|
||
id: Number(row.id),
|
||
goodsId: row.goods_id,
|
||
mallId: Number(row.mall_id) || 0,
|
||
status: row.status,
|
||
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null
|
||
}
|
||
};
|
||
}
|
||
|
||
async function listCopySkuTasks({ operator, mallId, page = 1, limit = 20 }) {
|
||
await ensureTables();
|
||
const key = operatorKey(operator);
|
||
const params = [key];
|
||
let mallFilter = '';
|
||
if (mallId) {
|
||
mallFilter = ' AND mall_id = ?';
|
||
params.push(String(mallId));
|
||
}
|
||
const totalRows = await query(
|
||
`SELECT COUNT(*) AS total FROM ${COPY_SKU_TABLE} WHERE operator_key = ?${mallFilter}`,
|
||
params
|
||
);
|
||
const rows = await query(
|
||
`SELECT * FROM ${COPY_SKU_TABLE} WHERE operator_key = ?${mallFilter} ORDER BY id DESC LIMIT ? OFFSET ?`,
|
||
[...params, Math.min(Math.max(Number(limit) || 20, 1), 500), (Math.max(Number(page) || 1, 1) - 1) * Math.min(Math.max(Number(limit) || 20, 1), 500)]
|
||
);
|
||
return {
|
||
list: rows.map((row) => ({
|
||
id: Number(row.id),
|
||
goodsId: row.goods_id,
|
||
mallId: Number(row.mall_id) || 0,
|
||
status: row.status,
|
||
errorMsg: row.error_msg || '',
|
||
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null
|
||
})),
|
||
total: Number(totalRows[0]?.total || 0)
|
||
};
|
||
}
|
||
|
||
// ---------- 任务状态流转(start/complete/clear 对两种任务通用) ----------
|
||
|
||
async function startTask({ operator, taskId, id }) {
|
||
await ensureTables();
|
||
const key = operatorKey(operator);
|
||
const numericId = Number(taskId ?? id);
|
||
if (!Number.isFinite(numericId)) return;
|
||
await database.local.query(
|
||
`UPDATE ${PROMOTION_TABLE} SET status = 'running' WHERE operator_key = ? AND id = ?`,
|
||
[key, numericId]
|
||
);
|
||
await database.local.query(
|
||
`UPDATE ${COPY_SKU_TABLE} SET status = 'running' WHERE operator_key = ? AND id = ?`,
|
||
[key, numericId]
|
||
);
|
||
}
|
||
|
||
async function completeTask({ operator, taskId, id, status, errorMsg }) {
|
||
await ensureTables();
|
||
const key = operatorKey(operator);
|
||
const numericId = Number(taskId ?? id);
|
||
if (!Number.isFinite(numericId)) return;
|
||
const nextStatus = TERMINAL_STATUSES.includes(String(status)) ? String(status) : 'completed';
|
||
await database.local.query(
|
||
`UPDATE ${PROMOTION_TABLE} SET status = ?, error_msg = ? WHERE operator_key = ? AND id = ?`,
|
||
[nextStatus, errorMsg || null, key, numericId]
|
||
);
|
||
await database.local.query(
|
||
`UPDATE ${COPY_SKU_TABLE} SET status = ?, error_msg = ? WHERE operator_key = ? AND id = ?`,
|
||
[nextStatus, errorMsg || null, key, numericId]
|
||
);
|
||
}
|
||
|
||
async function clearTerminalTasks({ operator, kind }) {
|
||
await ensureTables();
|
||
const key = operatorKey(operator);
|
||
const table = kind === 'copy-sku' ? COPY_SKU_TABLE : PROMOTION_TABLE;
|
||
const placeholders = TERMINAL_STATUSES.map(() => '?').join(',');
|
||
const result = await database.local.query(
|
||
`DELETE FROM ${table} WHERE operator_key = ? AND status IN (${placeholders})`,
|
||
[key, ...TERMINAL_STATUSES]
|
||
);
|
||
return { cleared: result.affectedRows || 0 };
|
||
}
|
||
|
||
// ---------- 商品初始化记录 ----------
|
||
|
||
async function reportGoodsInit({ operator, payload }) {
|
||
await ensureTables();
|
||
const goodsId = String(payload?.goodsId || '').trim();
|
||
if (!goodsId) return { goodsId: '', addTime: null };
|
||
const addTime = new Date();
|
||
await database.local.query(
|
||
`INSERT INTO ${GOODS_INIT_TABLE} (goods_id, operator_key, data_json, add_time)
|
||
VALUES (?, ?, ?, ?)
|
||
ON DUPLICATE KEY UPDATE operator_key = VALUES(operator_key), data_json = VALUES(data_json), add_time = VALUES(add_time)`,
|
||
[goodsId, operatorKey(operator), JSON.stringify(payload || {}), addTime]
|
||
);
|
||
return { goodsId, addTime: addTime.toISOString() };
|
||
}
|
||
|
||
async function getGoodsInitRecords(goodIds) {
|
||
await ensureTables();
|
||
const ids = (Array.isArray(goodIds) ? goodIds : []).map((id) => String(id ?? '').trim()).filter(Boolean);
|
||
if (!ids.length) return {};
|
||
const placeholders = ids.map(() => '?').join(',');
|
||
const rows = await query(
|
||
`SELECT goods_id, add_time FROM ${GOODS_INIT_TABLE} WHERE goods_id IN (${placeholders})`,
|
||
ids
|
||
);
|
||
const map = {};
|
||
for (const row of rows) {
|
||
map[row.goods_id] = row.add_time ? new Date(row.add_time).toISOString() : null;
|
||
}
|
||
return map;
|
||
}
|
||
|
||
async function getGoodsInit(goodsId) {
|
||
await ensureTables();
|
||
const gid = String(goodsId || '').trim();
|
||
if (!gid) return null;
|
||
const rows = await query(`SELECT * FROM ${GOODS_INIT_TABLE} WHERE goods_id = ?`, [gid]);
|
||
if (!rows.length) return null;
|
||
const row = rows[0];
|
||
return {
|
||
goodsId: row.goods_id,
|
||
addTime: row.add_time ? new Date(row.add_time).toISOString() : null,
|
||
...parseJsonSafe(row.data_json)
|
||
};
|
||
}
|
||
|
||
async function deleteGoodsInit(goodsId) {
|
||
await ensureTables();
|
||
const gid = String(goodsId || '').trim();
|
||
if (gid) await database.local.query(`DELETE FROM ${GOODS_INIT_TABLE} WHERE goods_id = ?`, [gid]);
|
||
}
|
||
|
||
// ---------- 活动白名单(与魔术师分享版一致:放行全部现实活动ID) ----------
|
||
|
||
function getSupportedActivityIds() {
|
||
const universalList = [];
|
||
for (let i = 1; i <= 99999; i++) universalList.push(i);
|
||
return { universalList, '24hSilentList': [] };
|
||
}
|
||
|
||
module.exports = {
|
||
ok,
|
||
fail,
|
||
createPromotionGroup,
|
||
listPromotionGroups,
|
||
createCopySkuTask,
|
||
listCopySkuTasks,
|
||
startTask,
|
||
completeTask,
|
||
clearTerminalTasks,
|
||
reportGoodsInit,
|
||
getGoodsInitRecords,
|
||
getGoodsInit,
|
||
deleteGoodsInit,
|
||
getSupportedActivityIds
|
||
};
|