Files
order_site/apps/backend/src/routes/admin/douyin-mashida.ts
T
yml2213 5a73fb74a9 feat(douyin-mashida): 新增抖音券核销后台界面(列表/核销/冲正/同步/对账/作废)
抖音订单并非纯自动发货:内部 SKU 命中 AFFILIATE_DASH_SKU_MAPPING 才走自动通道,
未命中显式回落人工,且核销依赖买家自己到 /redeem 输订单号。此前核销侧在后台完全
不可见:自动核销是 void .catch(warn) 只落日志,cancelVerify 出网方法已就绪但没有入口。

照快手电子凭证的骨架做一套抖音版(两张表两套状态机,不复用):
- 券码状态、平台侧状态、同步错误全部可视化,支持只看同步失败
- 核销/冲正/重新同步/平台对账/重发券码/作废,每个写操作都带审计
- 券码列显示「核销实际会用的券码」(platform_cert_no 优先),平台改过名时额外标注
- 冲正先打平台 cancelVerify 再回退本地,避免本地与平台状态漂移
2026-10-12 19:45:06 +08:00

189 lines
6.1 KiB
TypeScript
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.
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