feat(douyin-mashida): 抖音码速达最小接入后端(消息建单/券码同步/兑换核销/退款作废)
- 签名:出网 md5(secret+Σ(名+值)+secret) 与推送 md5(secret+data+pub_time+topic+secret) 两套独立实现,含文档 golden 向量单测(与 91 大写 / affiliate_dash HMAC 都不通用) - 进单:trade_TradePaid 推送 → 验签 → webhook_events 幂等 → order.orderDetail → upsertOrderFromSource(provider=douyin_mashida / platform=doudian) - 精度:19 位订单号超出 JS 安全整数,JSON.parse 会把 p_id 抹成 ...000; 故解析后从原始 JSON 文本用正则抓回精确数字(单测同时证明"不修会错") - 券码:迁移 094 redeem_certificates((provider,order_id) 唯一做发券幂等) + DYC 券码生成 + coupons.syncV2 同步(子单号优先、父单兜底) - 兑换:POST /api/v1/redeem/lookup|confirm,按 orderNo+IP 限流; 核销成功后 order.message 通知买家(best-effort,失败只记错误日志) - 退款/取消:作废未核销券;券已核销时打 WARN 资损告警(已消费不可回滚) - 后台:GET/POST /api/v1/admin/platform-config/douyin-mashida, secret 出参置空 + 掩码,掩码回写不覆盖原值 - 消息路由挂在全局 body parser 之前(app.ts),验签必须用 body 原始字符串
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import type { JsonObject } from '../../../types/json.js'
|
||||
|
||||
import { maskSecret } from '../../../utils/masking.js'
|
||||
import {
|
||||
getDouyinMashidaSourceConfig,
|
||||
saveDouyinMashidaSourceConfig,
|
||||
type DouyinMashidaSourceConfig,
|
||||
} from '../../platforms/douyin-mashida/source-config-service.js'
|
||||
|
||||
/**
|
||||
* 后台「抖音码速达」配置读写。
|
||||
*
|
||||
* ⚠️ 凭据遵守仓库既有约定(照抄 `kuaishou-industry-service.ts:106-133/216-244`):
|
||||
* 出参**不回显明文 secret**,只给 `secretMasked` + `hasSecret`;
|
||||
* 入参如果不填或填的是掩码值,**保留原值**,不能被空串覆盖。
|
||||
*/
|
||||
|
||||
const SECRET_FIELDS = ['secret'] as const
|
||||
|
||||
export function getAdminDouyinMashidaConfig() {
|
||||
return mapAdminDouyinMashidaConfig(getDouyinMashidaSourceConfig())
|
||||
}
|
||||
|
||||
export async function saveAdminDouyinMashidaConfig(payload: JsonObject = {}) {
|
||||
const current = getDouyinMashidaSourceConfig()
|
||||
|
||||
const saved = await saveDouyinMashidaSourceConfig({
|
||||
...payload,
|
||||
secret: resolveSecretValue(payload, current),
|
||||
})
|
||||
|
||||
return mapAdminDouyinMashidaConfig(saved)
|
||||
}
|
||||
|
||||
function mapAdminDouyinMashidaConfig(config: DouyinMashidaSourceConfig) {
|
||||
return {
|
||||
enabled: config.enabled,
|
||||
baseUrl: config.baseUrl,
|
||||
uid: config.uid,
|
||||
secret: '',
|
||||
secretMasked: maskSecret(config.secret),
|
||||
hasSecret: Boolean(config.secret),
|
||||
certLink: config.certLink,
|
||||
messageType: config.messageType,
|
||||
afterVerifyLink: config.afterVerifyLink,
|
||||
afterVerifyMessage: config.afterVerifyMessage,
|
||||
timeoutMs: config.timeoutMs,
|
||||
handledTopics: config.handledTopics,
|
||||
/** 联调自查:uid / secret / 兑换链接 三项都齐了才算配置完整 */
|
||||
ready: Boolean(config.enabled && config.uid && config.secret && config.certLink),
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSecretValue(payload: JsonObject, current: DouyinMashidaSourceConfig): string {
|
||||
const text = String(payload[SECRET_FIELDS[0]] ?? '').trim()
|
||||
if (!text || isMaskedSecretValue(text, current.secret)) {
|
||||
return current.secret
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
/** 空串 / 含 `****` / 等于当前掩码值 → 视为「未修改」。 */
|
||||
export function isMaskedSecretValue(value: unknown, currentSecret = ''): boolean {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return true
|
||||
}
|
||||
if (text.includes('****') || text.includes('***')) {
|
||||
return true
|
||||
}
|
||||
if (currentSecret && text === maskSecret(currentSecret)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user