From 588cf5f596652648ab66ce0b5d8c94b163e3774f Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 8 Jul 2026 12:32:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E8=A1=8C=E4=B8=9A=E5=87=AD?= =?UTF-8?q?=E8=AF=81=E5=8F=91=E8=B4=A7=E5=9B=9E=E8=B0=83=E9=87=91=E9=A2=9D?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../send-callback-service.ts | 2 - .../send-code-service.test.ts | 80 ++++++++++++ .../kuaishou-industry/send-code-service.ts | 122 +++++++++++++++++- 3 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 apps/backend/src/services/platforms/kuaishou-industry/send-code-service.test.ts diff --git a/apps/backend/src/services/platforms/kuaishou-industry/send-callback-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/send-callback-service.ts index 13c4520c..ca9331e0 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/send-callback-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/send-callback-service.ts @@ -23,7 +23,6 @@ type SendCallbackInput = { totalGoodsValue?: number token: string eticketType?: string - ext?: string expressCode?: string expressNo?: string } @@ -77,7 +76,6 @@ export async function sendCallback(input: SendCallbackInput): Promise<{ success: if (input.totalGoodsValue != null) bizParams.totalGoodsValue = input.totalGoodsValue if (input.eticketType) bizParams.eticketType = input.eticketType - if (input.ext) bizParams.ext = input.ext if (input.expressCode) bizParams.expressCode = input.expressCode if (input.expressNo) bizParams.expressNo = input.expressNo diff --git a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.test.ts b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.test.ts new file mode 100644 index 00000000..5d0dc90f --- /dev/null +++ b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.test.ts @@ -0,0 +1,80 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + resolveSendCallbackGoodsValuePlan, + resolveSendCallbackPreferredTotalGoodsValue, +} from './send-code-service.js' + +test('resolveSendCallbackPreferredTotalGoodsValue converts 91 maxAmount yuan to fen', () => { + const total = resolveSendCallbackPreferredTotalGoodsValue({ + raw_payload_json: JSON.stringify({ + body: { + maxAmount: '0.01', + }, + }), + total_amount: 100, + } as any) + + assert.equal(total, 1) +}) + +test('resolveSendCallbackPreferredTotalGoodsValue falls back to order total amount', () => { + const total = resolveSendCallbackPreferredTotalGoodsValue({ + raw_payload_json: '{}', + total_amount: 66, + } as any) + + assert.equal(total, 66) +}) + +test('resolveSendCallbackGoodsValuePlan prefers 91 amount over ext payment', () => { + const plan = resolveSendCallbackGoodsValuePlan( + [{}], + '{"payment":100,"platformBearAmount":0}', + 1, + ) + + assert.deepEqual(plan.goodsValues, [1]) + assert.equal(plan.totalGoodsValue, 1) + assert.equal(plan.source, 'preferred') +}) + +test('resolveSendCallbackGoodsValuePlan splits preferred amount and keeps sum equal to total', () => { + const plan = resolveSendCallbackGoodsValuePlan( + [{}, {}, {}], + '{"payment":999}', + 100, + ) + + assert.deepEqual(plan.goodsValues, [33, 33, 34]) + assert.equal(plan.totalGoodsValue, 100) + assert.equal(plan.source, 'preferred') +}) + +test('resolveSendCallbackGoodsValuePlan uses ext payment for a single voucher', () => { + const plan = resolveSendCallbackGoodsValuePlan( + [{}], + '{"payment":1,"platformBearAmount":0}', + ) + + assert.deepEqual(plan.goodsValues, [1]) + assert.equal(plan.totalGoodsValue, 1) +}) + +test('resolveSendCallbackGoodsValuePlan splits ext payment and keeps sum equal to total', () => { + const plan = resolveSendCallbackGoodsValuePlan( + [{}, {}, {}], + '{"payment":100}', + ) + + assert.deepEqual(plan.goodsValues, [33, 33, 34]) + assert.equal(plan.totalGoodsValue, 100) +}) + +test('resolveSendCallbackGoodsValuePlan falls back to one weight per voucher', () => { + const plan = resolveSendCallbackGoodsValuePlan([{}, {}], '') + + assert.deepEqual(plan.goodsValues, [1, 1]) + assert.equal(plan.totalGoodsValue, 2) +}) diff --git a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts index 746e6a57..3d5593f2 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts @@ -21,6 +21,8 @@ import { resolveKuaishouIndustryVoucherValidity, } from './voucher-service.js' import { bindKuaishouIndustryVouchersToOrderTasks } from './voucher-binding-service.js' +import { parseAmountToFen } from '../../../utils/money.js' +import type { OrderRow } from '../../../types/repository/rows.js' type JsonObject = Record @@ -100,6 +102,7 @@ export async function handleSendCode(rawBody: JsonObject = {}) { token: params.token, eticketType: params.eticketType, ext: params.ext, + preferredTotalGoodsValue: resolveSendCallbackPreferredTotalGoodsValue(order), }) return response @@ -112,6 +115,7 @@ function fireSendCallback(input: { token: string eticketType?: string ext?: string + preferredTotalGoodsValue?: number }) { const eticketItems = input.etickets.map((e) => ({ id: String(e.id || ''), @@ -119,29 +123,38 @@ function fireSendCallback(input: { num: Number(e.num) || 1, validStartTime: Number(e.validStartTime) || 0, validEndTime: Number(e.validEndTime) || 0, - goodsValue: Number(e.goodsValue) || 0, + })) + const goodsValuePlan = resolveSendCallbackGoodsValuePlan( + eticketItems, + input.ext, + input.preferredTotalGoodsValue, + ) + const eticketItemsWithGoodsValue = eticketItems.map((item, index) => ({ + ...item, + goodsValue: goodsValuePlan.goodsValues[index] || 1, })) const sendNum = eticketItems.reduce((sum, e) => sum + e.num, 0) - const totalGoodsValue = eticketItems.reduce((sum, e) => sum + e.goodsValue, 0) + const totalGoodsValue = goodsValuePlan.totalGoodsValue logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调已调度', { oid: input.oid, sendType: input.sendType, sendNum, totalGoodsValue, - etickets: eticketItems, + goodsValueSource: goodsValuePlan.source, + preferredTotalGoodsValue: normalizePositiveInteger(input.preferredTotalGoodsValue), + etickets: eticketItemsWithGoodsValue, eticketType: input.eticketType || '', }) sendCallback({ oid: input.oid, sendType: input.sendType, - etickets: eticketItems, + etickets: eticketItemsWithGoodsValue, sendNum, totalGoodsValue, token: input.token, ...(input.eticketType ? { eticketType: input.eticketType } : {}), - ...(input.ext ? { ext: input.ext } : {}), }).then((result) => { if (!result.success) { logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调异步执行失败', { @@ -163,3 +176,102 @@ function fireSendCallback(input: { }, { level: 'warn' }) }) } + +export function resolveSendCallbackGoodsValuePlan( + etickets: Array<{ goodsValue?: unknown, [key: string]: unknown }>, + ext = '', + preferredTotalGoodsValue: unknown = null, +) { + const count = Math.max(etickets.length, 1) + const preferredTotal = normalizePositiveInteger(preferredTotalGoodsValue) + if (preferredTotal >= count) { + return buildSplitGoodsValuePlan(preferredTotal, count, 'preferred') + } + + const extTotal = resolveGoodsValueTotalFromExt(ext) + if (extTotal >= count) { + return buildSplitGoodsValuePlan(extTotal, count, 'ext') + } + + const explicitGoodsValues = etickets + .map((item) => normalizePositiveInteger(item.goodsValue)) + .filter((value) => value > 0) + if (explicitGoodsValues.length === etickets.length && explicitGoodsValues.length > 0) { + return { + totalGoodsValue: explicitGoodsValues.reduce((sum, value) => sum + value, 0), + goodsValues: explicitGoodsValues, + source: 'eticket', + } + } + + const goodsValues = Array.from({ length: count }, () => 1) + return { + totalGoodsValue: count, + goodsValues, + source: 'fallback', + } +} + +export function resolveSendCallbackPreferredTotalGoodsValue(order: OrderRow | null): number { + if (!order) { + return 0 + } + + const rawPayload = parseJsonObject(order.raw_payload_json) + const body = parseJsonObject(rawPayload.body) + const maxAmountFen = normalizePositiveInteger(parseAmountToFen(body.maxAmount ?? rawPayload.maxAmount)) + if (maxAmountFen > 0) { + return maxAmountFen + } + + return normalizePositiveInteger(order.total_amount) +} + +function buildSplitGoodsValuePlan(totalGoodsValue: number, count: number, source: string) { + const goodsValues = splitGoodsValue(totalGoodsValue, count) + return { + totalGoodsValue: goodsValues.reduce((sum, value) => sum + value, 0), + goodsValues, + source, + } +} + +function splitGoodsValue(totalGoodsValue: number, count: number): number[] { + const base = Math.floor(totalGoodsValue / count) + if (base <= 0) { + return Array.from({ length: count }, () => 1) + } + + return Array.from({ length: count }, (_item, index) => + index === count - 1 ? totalGoodsValue - base * (count - 1) : base, + ) +} + +function resolveGoodsValueTotalFromExt(ext: unknown): number { + const parsed = parseJsonObject(ext) + return normalizePositiveInteger( + parsed.totalGoodsValue + ?? parsed.goodsValue + ?? parsed.payment + ?? parsed.payAmount + ?? parsed.amount, + ) +} + +function normalizePositiveInteger(value: unknown): number { + const parsed = Number(value) + return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : 0 +} + +function parseJsonObject(value: unknown): JsonObject { + if (value && typeof value === 'object' && !Array.isArray(value)) { + return value as JsonObject + } + + try { + const parsed = JSON.parse(String(value || '{}')) + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + } catch { + return {} + } +}