cangku/backend/src/services/campaignCloudService.js

367 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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
};