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