feat(finance): 积分钱包操作 + 客服「确认履约」闭环
把「配置 → 待办 → 确认履约 → 扣额度 → 写成本快照」整条链路打通, 客服点一下就完成履约,不需要填复杂的东西。 积分钱包(P1.3): - platform-credit-service.ts:充值 / 调整 / 消耗 / 流水查询。 - 三平台积分互不通用,每客服每平台一个账户;余额只能由流水产生, 消耗不足直接拒绝(409),不允许负余额,与备用金口径一致。 - 换算率按平台:南航、乐玩 1 积分 = 1 分,ROCK 1 积分 = 1 元。 - 充值 / 调整只对 admin/operator/finance 开放;客服只能看自己的钱包。 - 备用金现金不在这里,走现有 fund_* 链路。 确认履约(P1.4): - manual-fulfillment-service.ts:待办列表 + 确认履约。 - 待办口径:order_items.fulfillment_mode='manual' 且已配 fulfillment_config_id。 人工单**不建 fulfillment_tasks**——那是自动发货通道的体系,人工单历来不走它 (本地 102,955 个人工单的任务行数为 0)。 - 「已履约」判定:该订单项存在有效 manual_case 成本快照。 - 固定成本(消耗积分 > 0)→ 零输入;成本不固定(= 0)→ 只填实际成本金额。 - 确认时:积分平台扣积分钱包、备用金现金走 writeFundExpense, 随后写 actual 成本快照(主体 = 积分流水 id 或备用金支出单 id)。 - 渠道与消耗积分永远提前配好,客服不选;重复履约直接拒绝。 前端: - 新增「人工履约」页(客服菜单):待确认 / 已履约 / 全部,确认履约弹窗。 - 备用金页新增「平台积分」Tab(客服端与管理端各一份): 余额卡片、积分流水、管理员充值与调整。 E2E 实测:配置 ROCK 100 积分 → 成本 ¥100.00(1 积分 = 1 元);充值 5000 积分 = ¥5000;确认履约后积分 5000 → 4900,成本快照 actual ¥100.00 写入; 重复履约被拒绝。测试数据已清理,登记表保留 357 条扫描结果供运营使用。 校验:后端 765 项 / 0 失败,前端 143 项全通过(含 tsc -b 与 build)。 Co-Authored-By: Claude Opus 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
4bcb4e6b57
commit
d07d88e25c
@@ -31,6 +31,13 @@ import {
|
||||
updateAdminChannelMapping,
|
||||
updateAdminProduct,
|
||||
} from '../../services/cost-center/product-catalog-service.js'
|
||||
import {
|
||||
adjustPlatformCredit,
|
||||
grantPlatformCredit,
|
||||
listPlatformCreditAccounts,
|
||||
listPlatformCreditEntries,
|
||||
listPlatformCreditOwners,
|
||||
} from '../../services/cost-center/platform-credit-service.js'
|
||||
import {
|
||||
deleteManualFulfillmentConfig,
|
||||
getManualFulfillmentConfig,
|
||||
@@ -745,4 +752,95 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 客服平台积分钱包(南航 / 乐玩 / ROCK)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get(
|
||||
'/cost-center/platform-credits/accounts',
|
||||
requireAdminPermission('finance.view'),
|
||||
createJsonHandler((req) => listPlatformCreditAccounts(sessionOf(req)), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取积分钱包失败',
|
||||
scope: '[admin/cost-center/platform-credits/accounts]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/cost-center/platform-credits/entries',
|
||||
requireAdminPermission('finance.view'),
|
||||
createJsonHandler(
|
||||
(req) => listPlatformCreditEntries(req.query as Record<string, unknown>, sessionOf(req)),
|
||||
{
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取积分流水失败',
|
||||
scope: '[admin/cost-center/platform-credits/entries]',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/cost-center/platform-credits/owners',
|
||||
requireAdminPermission('finance.config'),
|
||||
createJsonHandler(() => listPlatformCreditOwners(), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取客服列表失败',
|
||||
scope: '[admin/cost-center/platform-credits/owners]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/cost-center/platform-credits/grant',
|
||||
requireAdminPermission('finance.config'),
|
||||
createJsonHandler((req) => grantPlatformCredit(bodyOf(req), sessionOf(req)), {
|
||||
successMessage: '积分已充值',
|
||||
errorMessage: '积分充值失败',
|
||||
scope: '[admin/cost-center/platform-credits/grant]',
|
||||
audit: (req, data) => {
|
||||
const result = data as {
|
||||
platformKey?: string
|
||||
quantity?: number
|
||||
amountFen?: number
|
||||
}
|
||||
return {
|
||||
action: 'platform_credit_granted',
|
||||
targetType: 'platform_credit_account',
|
||||
targetId: String(req.body?.ownerAdminUserId || ''),
|
||||
data: {
|
||||
platformKey: result.platformKey || '',
|
||||
quantity: result.quantity || 0,
|
||||
amountFen: result.amountFen || 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/cost-center/platform-credits/adjust',
|
||||
requireAdminPermission('finance.config'),
|
||||
createJsonHandler((req) => adjustPlatformCredit(bodyOf(req), sessionOf(req)), {
|
||||
successMessage: '积分已调整',
|
||||
errorMessage: '积分调整失败',
|
||||
scope: '[admin/cost-center/platform-credits/adjust]',
|
||||
audit: (req, data) => {
|
||||
const result = data as {
|
||||
platformKey?: string
|
||||
quantity?: number
|
||||
amountFen?: number
|
||||
}
|
||||
return {
|
||||
action: 'platform_credit_adjusted',
|
||||
targetType: 'platform_credit_account',
|
||||
targetId: String(req.body?.ownerAdminUserId || ''),
|
||||
data: {
|
||||
platformKey: result.platformKey || '',
|
||||
quantity: result.quantity || 0,
|
||||
amountFen: result.amountFen || 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export default router
|
||||
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
createManualOrderCost,
|
||||
listManualOrderCostItems,
|
||||
} from '../../services/finance/manual-order-cost-service.js'
|
||||
import {
|
||||
confirmManualFulfillment,
|
||||
listManualFulfillmentPending,
|
||||
} from '../../services/finance/manual-fulfillment-service.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import {
|
||||
createJsonHandler,
|
||||
@@ -153,4 +157,51 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 人工履约待办与确认履约
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get(
|
||||
'/finance/manual-fulfillment/pending',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) => listManualFulfillmentPending(req.query as Record<string, unknown>, sessionOf(req)),
|
||||
{
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取人工履约待办失败',
|
||||
scope: '[admin/finance/manual-fulfillment/pending]',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/finance/manual-fulfillment/confirm',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler((req) => confirmManualFulfillment(bodyOf(req), sessionOf(req)), {
|
||||
successMessage: '履约已确认',
|
||||
errorMessage: '确认履约失败',
|
||||
scope: '[admin/finance/manual-fulfillment/confirm]',
|
||||
audit: (req, data) => {
|
||||
const result = data as {
|
||||
orderItemId?: number
|
||||
costSnapshotId?: number
|
||||
platformKey?: string
|
||||
creditQuantity?: number
|
||||
costFen?: number
|
||||
}
|
||||
return {
|
||||
action: 'manual_fulfillment_confirmed',
|
||||
targetType: 'order_item',
|
||||
targetId: String(result.orderItemId || ''),
|
||||
data: {
|
||||
costSnapshotId: result.costSnapshotId || null,
|
||||
platformKey: result.platformKey || '',
|
||||
creditQuantity: result.creditQuantity || 0,
|
||||
costFen: result.costFen || 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export default router
|
||||
|
||||
Reference in New Issue
Block a user