feat(douyin-mashida): 新增抖音券核销后台界面(列表/核销/冲正/同步/对账/作废)

抖音订单并非纯自动发货:内部 SKU 命中 AFFILIATE_DASH_SKU_MAPPING 才走自动通道,
未命中显式回落人工,且核销依赖买家自己到 /redeem 输订单号。此前核销侧在后台完全
不可见:自动核销是 void .catch(warn) 只落日志,cancelVerify 出网方法已就绪但没有入口。

照快手电子凭证的骨架做一套抖音版(两张表两套状态机,不复用):
- 券码状态、平台侧状态、同步错误全部可视化,支持只看同步失败
- 核销/冲正/重新同步/平台对账/重发券码/作废,每个写操作都带审计
- 券码列显示「核销实际会用的券码」(platform_cert_no 优先),平台改过名时额外标注
- 冲正先打平台 cancelVerify 再回退本地,避免本地与平台状态漂移
This commit is contained in:
yml2213
2026-10-12 19:45:06 +08:00
parent 0dd26da4ee
commit 5a73fb74a9
14 changed files with 1896 additions and 0 deletions
+2
View File
@@ -4,6 +4,7 @@ import authRouter from './admin/auth.js'
import auditLogsRouter from './admin/audit-logs.js'
import dashboardRouter from './admin/dashboard.js'
import devMockRouter from './admin/dev-mock.js'
import douyinMashidaRouter from './admin/douyin-mashida.js'
import financeRouter from './admin/finance.js'
import filesRouter from './admin/files.js'
import kuaishouIndustryRouter from './admin/kuaishou-industry.js'
@@ -30,6 +31,7 @@ router.use(rbacRouter)
router.use(auditLogsRouter)
router.use(platformConfigRouter)
router.use(kuaishouIndustryRouter)
router.use(douyinMashidaRouter)
router.use(ordersRouter)
router.use(notificationsRouter)
router.use(realtimeRouter)
@@ -0,0 +1,188 @@
import { Router } from 'express'
import {
cancelAdminDouyinMashidaCertificateVerify,
checkAdminDouyinMashidaCertificate,
fetchAdminDouyinMashidaOrderDetail,
invalidateAdminDouyinMashidaCertificate,
listAdminDouyinMashidaCertificates,
resendAdminDouyinMashidaCertificate,
syncAdminDouyinMashidaCertificate,
verifyAdminDouyinMashidaCertificate,
} from '../../services/admin/douyin-mashida-certificate-service.js'
import { createJsonHandler, requireAdminRoles } from './session.js'
import type {
AdminDouyinMashidaCertificateActionRouteBody,
AdminDouyinMashidaCertificateListRouteQuery,
} from '../../types/admin/route-inputs.js'
/**
* 后台「抖音券核销」:与 `admin/kuaishou-industry.ts` 同型的运营工具路由,
* 操作对象换成我们自发的兑换券(`redeem_certificates`,provider=douyin_mashida)。
*
* 这里每个写操作都带审计:**<|hy_place▁holder▁no▁813|>码/核销/冲正会直接改变平台侧资金相关状态**,
* 没有留痕等于出事没法追溯。
*/
const router = Router()
const MANAGE_ROLES = ['admin', 'operator', 'support']
router.get(
'/douyin-mashida/certificates',
createJsonHandler(
(req) =>
listAdminDouyinMashidaCertificates(req.query as AdminDouyinMashidaCertificateListRouteQuery),
{
successMessage: 'ok',
errorMessage: '查询抖音兑换券失败',
scope: '[admin/douyin-mashida/certificates]',
},
),
)
router.post(
'/douyin-mashida/certificates/verify',
requireAdminRoles(MANAGE_ROLES),
createJsonHandler(
(req) =>
verifyAdminDouyinMashidaCertificate(req.body as AdminDouyinMashidaCertificateActionRouteBody),
{
successMessage: '券码已核销',
errorMessage: '手动核销券码失败',
scope: '[admin/douyin-mashida/certificates/verify]',
audit: (req, data) =>
buildCertificateAudit('douyin_mashida_certificate_verify', req.body, data),
},
),
)
router.post(
'/douyin-mashida/certificates/cancel-verify',
requireAdminRoles(MANAGE_ROLES),
createJsonHandler(
(req) =>
cancelAdminDouyinMashidaCertificateVerify(
req.body as AdminDouyinMashidaCertificateActionRouteBody,
),
{
successMessage: '券码已冲正',
errorMessage: '券码冲正失败',
scope: '[admin/douyin-mashida/certificates/cancel-verify]',
audit: (req, data) =>
buildCertificateAudit('douyin_mashida_certificate_cancel_verify', req.body, data),
},
),
)
router.post(
'/douyin-mashida/certificates/sync',
requireAdminRoles(MANAGE_ROLES),
createJsonHandler(
(req) =>
syncAdminDouyinMashidaCertificate(req.body as AdminDouyinMashidaCertificateActionRouteBody),
{
successMessage: '券码已重新同步',
errorMessage: '重新同步券码失败',
scope: '[admin/douyin-mashida/certificates/sync]',
audit: (req, data) =>
buildCertificateAudit('douyin_mashida_certificate_sync', req.body, data),
},
),
)
router.post(
'/douyin-mashida/certificates/check',
requireAdminRoles(MANAGE_ROLES),
createJsonHandler(
(req) =>
checkAdminDouyinMashidaCertificate(req.body as AdminDouyinMashidaCertificateActionRouteBody),
{
successMessage: '平台券码已查询',
errorMessage: '查询平台券码失败',
scope: '[admin/douyin-mashida/certificates/check]',
audit: (req, data) =>
buildCertificateAudit('douyin_mashida_certificate_check', req.body, data),
},
),
)
router.post(
'/douyin-mashida/certificates/resend',
requireAdminRoles(MANAGE_ROLES),
createJsonHandler(
(req) =>
resendAdminDouyinMashidaCertificate(req.body as AdminDouyinMashidaCertificateActionRouteBody),
{
successMessage: '券码已重发',
errorMessage: '重发券码失败',
scope: '[admin/douyin-mashida/certificates/resend]',
audit: (req, data) =>
buildCertificateAudit('douyin_mashida_certificate_resend', req.body, data),
},
),
)
router.post(
'/douyin-mashida/certificates/invalidate',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
invalidateAdminDouyinMashidaCertificate(
req.body as AdminDouyinMashidaCertificateActionRouteBody,
),
{
successMessage: '券码已作废',
errorMessage: '作废券码失败',
scope: '[admin/douyin-mashida/certificates/invalidate]',
audit: (req, data) =>
buildCertificateAudit('douyin_mashida_certificate_invalidate', req.body, data),
},
),
)
router.post(
'/douyin-mashida/order-detail',
requireAdminRoles(MANAGE_ROLES),
createJsonHandler(
(req) =>
fetchAdminDouyinMashidaOrderDetail(req.body as AdminDouyinMashidaCertificateActionRouteBody),
{
successMessage: '平台订单详情已查询',
errorMessage: '查询平台订单详情失败',
scope: '[admin/douyin-mashida/order-detail]',
audit: (req, data) => buildCertificateAudit('douyin_mashida_order_detail', req.body, data),
},
),
)
/** 审计只记「操作了哪张券 + 结果摘要」,不整包落 rawPayload(里面有商品快照)。 */
function buildCertificateAudit(action: string, body: unknown, data: unknown) {
const payload = body && typeof body === 'object' ? (body as Record<string, unknown>) : {}
const result = data && typeof data === 'object' ? (data as Record<string, unknown>) : {}
const certificate = result.certificate as Record<string, unknown> | undefined
return {
action,
targetType: 'douyin_mashida_certificate',
targetId: resolveAuditTargetId(payload, certificate),
data: {
certNo: String(certificate?.certNo || payload.certNo || '').trim(),
orderId: String(
certificate?.orderId || payload.orderId || payload.platformOrderId || '',
).trim(),
reason: String(payload.reason || '').trim(),
success: result.success !== false,
},
}
}
function resolveAuditTargetId(
payload: Record<string, unknown>,
certificate: Record<string, unknown> | undefined,
): string {
return String(
certificate?.certNo || payload.certNo || payload.orderId || payload.platformOrderId || '-',
)
}
export default router