合并远程 main 最新改动

This commit is contained in:
yml2213
2026-11-05 16:11:40 +08:00
9 changed files with 393 additions and 19 deletions
@@ -33,7 +33,12 @@ const AFTER_SALES_CASE_SELECT = `
) AS shop_name,
wu.username AS worker_username,
wu.display_name AS worker_display_name,
wos.quantity AS share_quantity
wos.quantity AS share_quantity,
-- 售后追缴口径:整单取工单发单金额,拼单取责任份额的一份价格与合计金额
wo.reward_amount AS work_order_reward_amount,
wo.sharing_enabled AS work_order_sharing_enabled,
wos.unit_reward AS share_unit_reward,
wos.share_reward AS share_reward_amount
FROM worker_after_sales_cases ac
INNER JOIN work_orders wo ON wo.id = ac.work_order_id
LEFT JOIN orders o ON o.id = wo.order_id
@@ -201,6 +201,10 @@ export type WorkerAfterSalesCaseRow = {
worker_username?: string
worker_display_name?: string
share_quantity?: number | null
work_order_reward_amount?: number | string | null
work_order_sharing_enabled?: boolean | null
share_unit_reward?: number | string | null
share_reward_amount?: number | string | null
}
export type WorkerAfterSalesCaseEventRow = {
@@ -0,0 +1,24 @@
/**
* 售后默认追缴金额口径(单位:分):
* - 整单售后(没有责任份额):取工单发单金额 work_orders.reward_amount;
* - 拼单售后:取该责任份额的一份价格 work_order_shares.unit_reward,
* 历史上 unit_reward 可能为 0,此时回退为 share_reward / quantity(至少 1 份)。
*
* 注意:这里只给前端一个建议默认值,最终追缴金额仍由管理员在弹窗里确认/修改。
*/
export function resolveAfterSalesSuggestedRecoveryFen(input: {
shareId: number | null
shareUnitReward: number
shareRewardAmount: number
shareQuantity: number | null
workOrderRewardAmount: number
}): number {
const workOrderRewardAmount = Math.max(0, Math.round(Number(input.workOrderRewardAmount || 0)))
if (!input.shareId) return workOrderRewardAmount
const unitReward = Math.max(0, Math.round(Number(input.shareUnitReward || 0)))
if (unitReward > 0) return unitReward
const quantity = Math.max(1, Math.round(Number(input.shareQuantity || 1)))
return Math.max(0, Math.round(Number(input.shareRewardAmount || 0) / quantity))
}
@@ -0,0 +1,106 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
import { resolveAfterSalesSuggestedRecoveryFen } from './after-sales-recovery-amount.js'
test('整单售后:默认追缴金额取工单发单金额', () => {
assert.equal(
resolveAfterSalesSuggestedRecoveryFen({
shareId: null,
shareUnitReward: 0,
shareRewardAmount: 0,
shareQuantity: null,
workOrderRewardAmount: 12_800,
}),
12_800,
)
})
test('拼单售后:默认追缴金额取该责任份额的一份价格', () => {
assert.equal(
resolveAfterSalesSuggestedRecoveryFen({
shareId: 99,
shareUnitReward: 3_200,
shareRewardAmount: 9_600,
shareQuantity: 3,
workOrderRewardAmount: 12_800,
}),
3_200,
)
})
test('拼单售后:一份价格缺失时回退为 合计 / 份数', () => {
assert.equal(
resolveAfterSalesSuggestedRecoveryFen({
shareId: 99,
shareUnitReward: 0,
shareRewardAmount: 9_600,
shareQuantity: 3,
workOrderRewardAmount: 12_800,
}),
3_200,
)
})
test('拼单售后:份数缺失时按 1 份处理,不出现除零', () => {
assert.equal(
resolveAfterSalesSuggestedRecoveryFen({
shareId: 99,
shareUnitReward: 0,
shareRewardAmount: 4_500,
shareQuantity: null,
workOrderRewardAmount: 4_500,
}),
4_500,
)
})
test('金额异常(负数、NaN)一律归零,不产生负数追缴', () => {
assert.equal(
resolveAfterSalesSuggestedRecoveryFen({
shareId: null,
shareUnitReward: 0,
shareRewardAmount: 0,
shareQuantity: null,
workOrderRewardAmount: -100,
}),
0,
)
assert.equal(
resolveAfterSalesSuggestedRecoveryFen({
shareId: 1,
shareUnitReward: Number.NaN,
shareRewardAmount: Number.NaN,
shareQuantity: 2,
workOrderRewardAmount: 0,
}),
0,
)
})
test('售后列表 SQL 必须带出发单金额与责任份额金额,否则处置弹窗拿不到默认值', async () => {
const source = await readFile(
new URL('../../repositories/worker-platform/after-sales-repo.ts', import.meta.url),
'utf8',
)
for (const fragment of [
'wo.reward_amount AS work_order_reward_amount',
'wo.sharing_enabled AS work_order_sharing_enabled',
'wos.unit_reward AS share_unit_reward',
'wos.share_reward AS share_reward_amount',
]) {
assert.ok(source.includes(fragment), `AFTER_SALES_CASE_SELECT 缺少 ${fragment}`)
}
})
test('售后 mapper 必须返回建议追缴金额,供前端默认填充', async () => {
const source = await readFile(new URL('./worker-finance-mappers.ts', import.meta.url), 'utf8')
assert.ok(
source.includes('suggestedRecoveryAmount: resolveAfterSalesSuggestedRecoveryFen({'),
'mapWorkerAfterSalesCase 必须计算 suggestedRecoveryAmount',
)
assert.ok(source.includes('workOrderRewardAmount,'), 'mapper 必须返回 workOrderRewardAmount')
assert.ok(source.includes('shareUnitReward,'), 'mapper 必须返回 shareUnitReward')
assert.ok(source.includes('shareRewardAmount,'), 'mapper 必须返回 shareRewardAmount')
})
@@ -7,6 +7,7 @@ import {
} from '../../repositories/worker-platform/index.js'
import { safeParseJson } from '../admin/admin-query-utils.js'
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
import { resolveAfterSalesSuggestedRecoveryFen } from './after-sales-recovery-amount.js'
import { createWorkerSessionToken } from './worker-auth-utils.js'
import {
normalizeBoolean,
@@ -227,20 +228,45 @@ export function mapWorkerAfterSalesCase(afterSalesCase: {
worker_username?: string
worker_display_name?: string
share_quantity?: number | null
work_order_reward_amount?: number | string | null
work_order_sharing_enabled?: boolean | null
share_unit_reward?: number | string | null
share_reward_amount?: number | string | null
}) {
const snapshot = safeParseJson(afterSalesCase.acceptance_snapshot_json)
const shareId = afterSalesCase.work_order_share_id
? Number(afterSalesCase.work_order_share_id)
: null
const shareQuantity = afterSalesCase.share_quantity ? Number(afterSalesCase.share_quantity) : null
const workOrderRewardAmount = Math.max(
0,
Math.round(Number(afterSalesCase.work_order_reward_amount || 0)),
)
const shareUnitReward = Math.max(0, Math.round(Number(afterSalesCase.share_unit_reward || 0)))
const shareRewardAmount = Math.max(0, Math.round(Number(afterSalesCase.share_reward_amount || 0)))
return {
caseId: Number(afterSalesCase.id),
caseNo: String(afterSalesCase.case_no || ''),
workOrderId: Number(afterSalesCase.work_order_id),
shareId: afterSalesCase.work_order_share_id ? Number(afterSalesCase.work_order_share_id) : null,
shareId,
workerId: Number(afterSalesCase.worker_id),
workOrderNo: String(afterSalesCase.work_order_no || ''),
platformOrderId: String(afterSalesCase.platform_order_id || ''),
productName: String(afterSalesCase.product_name || ''),
shopName: String(afterSalesCase.shop_name || ''),
workerName: String(afterSalesCase.worker_display_name || afterSalesCase.worker_username || ''),
shareQuantity: afterSalesCase.share_quantity ? Number(afterSalesCase.share_quantity) : null,
shareQuantity,
workOrderRewardAmount,
sharingEnabled: Boolean(afterSalesCase.work_order_sharing_enabled),
shareUnitReward,
shareRewardAmount,
suggestedRecoveryAmount: resolveAfterSalesSuggestedRecoveryFen({
shareId,
shareUnitReward,
shareRewardAmount,
shareQuantity,
workOrderRewardAmount,
}),
problemType: String(afterSalesCase.problem_type || ''),
complaintNote: String(afterSalesCase.complaint_note || ''),
complaintFiles: normalizeUploadedFiles(safeParseJson(afterSalesCase.complaint_files_json)).map(