Files
order_site/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts
T
yml2213 792680bd19 重构 lewan 履约:拆分发货模块并统一 registry 入口
将 task-finalization 拆为 dispatch/stock/return/redeem/confirm 等用例,
claim 与 admin 经 executor registry 调用;admin 退号仅清理虚拟号不核销。
2026-07-10 22:23:26 +08:00

564 lines
18 KiB
TypeScript

import { getOrderById } from '../../../repositories/order-repo.js'
import { listTasksByOrderId, updateTask } from '../../../repositories/task-repo.js'
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import type { JsonObject } from '../../../types/json.js'
import {
findKuaishouIndustryVoucherByCode,
listKuaishouIndustryVouchersByOid,
listKuaishouIndustryVouchersByTaskId,
} from '../../../repositories/kuaishou-industry-voucher-repo.js'
import { resendKuaishouIndustryVoucherSendCallback } from '../../platforms/kuaishou-industry/send-code-service.js'
import { consumeKuaishouIndustryVoucher } from '../../platforms/kuaishou-industry/voucher-service.js'
import {
attachKuaishouIndustryVoucherToTask,
bindKuaishouIndustryVouchersToOrderTasks,
} from '../../platforms/kuaishou-industry/voucher-binding-service.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS } from '../../../domain/task-status.js'
import { createAdminViewerContext, parseTaskContext } from '../admin-read-shared-helpers.js'
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
import {
dispatchFulfillmentTask,
prepareFulfillmentBinding,
rebindFulfillmentRole,
refreshFulfillmentRole,
returnFulfillmentNumber,
} from '../../fulfillment/executors/registry.js'
import {
isKuaishouCloudTask,
normalizeKuaishouCloudFlow,
} from './kuaishou-cloud-helpers.js'
import type {
AdminEntityIdInput,
AdminViewerSessionInput,
} from '../../../types/admin/read-inputs.js'
import type {
AdminTaskKuaishouIndustryConsumeInput,
} from '../../../types/admin/write-inputs.js'
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js'
export async function prepareAdminTaskKuaishouCloudFulfillment(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限准备绑定资源', {
statusCode: 403,
errorCode: 'admin_task_prepare_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const result = await prepareFulfillmentBinding(task, {
source: 'admin_task_prepare',
actor: buildAdminActionActor(session),
})
if (!result?.task) {
throw createHttpError('当前任务不支持准备绑定资源', {
statusCode: 409,
errorCode: 'admin_task_prepare_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
claimUrl: String(result.claimUrl || ''),
token: String(result.token || ''),
}
}
export async function dispatchAdminTaskKuaishouCloudFulfillment(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限执行发货', {
statusCode: 403,
errorCode: 'admin_task_dispatch_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const result = await dispatchFulfillmentTask(task, {
source: 'admin_task_dispatch',
actor: buildAdminActionActor(session),
autoFinalize: false,
})
if (!result?.task) {
throw createHttpError('当前任务不支持 executor 发货', {
statusCode: 409,
errorCode: 'admin_task_dispatch_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
}
}
export async function refreshAdminTaskKuaishouCloudRoleInfo(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canOperateAssistedTask) {
throw createHttpError('当前账号没有权限刷新角色信息', {
statusCode: 403,
errorCode: 'admin_task_refresh_kuaishou_cloud_role_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const result = await refreshFulfillmentRole(task, {
source: 'admin_task_refresh_role',
actor: buildAdminActionActor(session),
forceProbe: true,
recordEvent: true,
})
if (!result?.task) {
throw createHttpError('当前任务不支持刷新角色信息', {
statusCode: 409,
errorCode: 'admin_task_refresh_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
}
}
export async function rebindAdminTaskKuaishouCloudRole(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const viewerContext = createAdminViewerContext(session)
// 与刷新角色一致:客服可换绑协助处理 UID 绑错,不开放完整任务生命周期
if (!viewerContext.canOperateAssistedTask) {
throw createHttpError('当前账号没有权限换绑角色', {
statusCode: 403,
errorCode: 'admin_task_rebind_kuaishou_cloud_forbidden',
})
}
const result = await rebindFulfillmentRole(task, {
source: 'admin_task_rebind_role',
actor: buildAdminActionActor(session),
})
if (!result?.task) {
throw createHttpError('当前任务不支持换绑角色', {
statusCode: 409,
errorCode: 'admin_task_rebind_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
claimUrl: String(result.claimUrl || ''),
token: String(result.token || ''),
}
}
export async function returnNumberAdminTaskKuaishouCloudFulfillment(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限退还号码', {
statusCode: 403,
errorCode: 'admin_task_return_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
// admin 退号 = 清理虚拟号占位,不核销电子凭证
const result = await returnFulfillmentNumber(task, {
source: 'admin_task_return_number',
actor: buildAdminActionActor(session),
consumeIndustryVoucher: false,
})
if (!result?.task) {
throw createHttpError('当前任务不支持退还号码', {
statusCode: 409,
errorCode: 'admin_task_return_not_supported',
})
}
return {
task: mapTaskActionPayload(result.task),
}
}
export async function resendAdminTaskKuaishouIndustryVoucherCode(
taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canOperateKuaishouIndustryVoucher) {
throw createHttpError('当前账号没有权限重发电子凭证发码回调', {
statusCode: 403,
errorCode: 'admin_task_kuaishou_industry_resend_forbidden',
})
}
const voucher = await getRequiredIndustryVoucherForTask(task)
const order = await getOrderById(task.order_id)
const result = await resendKuaishouIndustryVoucherSendCallback({
voucherCode: voucher.voucher_code,
oid: voucher.oid,
preferredTotalGoodsValue: Number(order?.total_amount || 0) || 0,
})
const updatedVoucher = result.voucher || voucher
if (!result.success) {
const message = String(result.error || '电子凭证发码回调重发失败').trim()
const failedTask = await updateTask(task.id, {
task_status: TASK_STATUS.MANUAL_REVIEW,
result_code: 'kuaishou_industry_send_callback_resend_failed',
result_message: message,
last_error: message,
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_industry_send_callback_resend_failed',
{
oid: voucher.oid,
voucherCode: voucher.voucher_code,
errorMessage: message,
actor: buildAdminActionActor(session),
},
now,
)
void failedTask
throw createHttpError(message, {
statusCode: 409,
errorCode: 'admin_task_kuaishou_industry_resend_failed',
})
}
const attachedTask = await attachKuaishouIndustryVoucherToTask(task, updatedVoucher, {
source: 'admin_resend_kuaishou_industry_code',
now,
})
const resendTargetTask = attachedTask || task
const refreshedTask = await updateTask(resendTargetTask.id, {
result_code: 'kuaishou_industry_send_callback_resent',
result_message: '电子凭证发码回调已重发成功',
last_error: '',
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_industry_send_callback_resent',
{
oid: voucher.oid,
voucherCode: voucher.voucher_code,
actor: buildAdminActionActor(session),
},
now,
)
return { task: mapTaskActionPayload(refreshedTask || resendTargetTask) }
}
export async function resendAdminOrderKuaishouIndustryVoucherCodes(
orderId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null,
): Promise<{ success: boolean; resentCount: number; failedCount: number; errorMessage: string }> {
const order = await getOrderById(Number(orderId))
const viewerContext = createAdminViewerContext(session)
const now = nowIso()
if (!viewerContext.canOperateKuaishouIndustryVoucher) {
throw createHttpError('当前账号没有权限重发电子凭证发码回调', {
statusCode: 403,
errorCode: 'admin_order_kuaishou_industry_resend_forbidden',
})
}
if (!order) {
throw createHttpError('订单不存在', {
statusCode: 404,
errorCode: 'admin_order_not_found',
})
}
const vouchers = await resolveIndustryVouchersForOrder(order.platform_order_id)
if (vouchers.length === 0) {
throw createHttpError('当前订单没有关联电子凭证,无法重发发码回调', {
statusCode: 409,
errorCode: 'admin_order_kuaishou_industry_voucher_missing',
})
}
let resentCount = 0
let failedCount = 0
let errorMessage = ''
for (const voucher of vouchers) {
const result = await resendKuaishouIndustryVoucherSendCallback({
voucherCode: voucher.voucher_code,
oid: voucher.oid,
preferredTotalGoodsValue: Number(order.total_amount || 0) || 0,
})
if (result.success) {
resentCount += 1
continue
}
failedCount += 1
errorMessage = errorMessage || String(result.error || '电子凭证发码回调重发失败').trim()
}
if (failedCount > 0) {
throw createHttpError(errorMessage || '电子凭证发码回调重发失败', {
statusCode: 409,
errorCode: 'admin_order_kuaishou_industry_resend_failed',
context: { resentCount, failedCount },
})
}
const tasks = await listTasksByOrderId(order.id)
await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, {
source: 'admin_order_resend_kuaishou_industry_code',
now,
})
return {
success: failedCount === 0,
resentCount,
failedCount,
errorMessage,
}
}
export async function consumeAdminTaskKuaishouIndustryVoucher(
taskId: AdminEntityIdInput,
payload: AdminTaskKuaishouIndustryConsumeInput = {},
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canOperateKuaishouIndustryVoucher) {
throw createHttpError('当前账号没有权限执行电子凭证核销', {
statusCode: 403,
errorCode: 'admin_task_kuaishou_industry_consume_forbidden',
})
}
const voucher = await getRequiredIndustryVoucherForTask(task)
const consumeType = String(payload.consumeType || '').trim()
const result = await consumeKuaishouIndustryVoucher(voucher, {
source: 'admin_manual_consume',
task,
token: voucher.token,
consumeTime: Date.now(),
...(consumeType ? { consumeType } : {}),
...(payload.storeName ? { storeName: payload.storeName } : {}),
...(payload.storeAddress ? { storeAddress: payload.storeAddress } : {}),
...(payload.expressCode ? { expressCode: payload.expressCode } : {}),
...(payload.expressNo ? { expressNo: payload.expressNo } : {}),
})
if (!result.ok || !result.voucher) {
const message = String(result.errorMessage || '电子凭证核销失败').trim()
const failedTask = await updateTask(task.id, {
task_status: TASK_STATUS.MANUAL_REVIEW,
result_code: 'kuaishou_industry_manual_consume_failed',
result_message: message,
last_error: message,
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_industry_manual_consume_failed',
{
oid: voucher.oid,
voucherCode: voucher.voucher_code,
errorMessage: message,
actor: buildAdminActionActor(session),
},
now,
)
void failedTask
throw createHttpError(message, {
statusCode: 409,
errorCode: 'admin_task_kuaishou_industry_consume_failed',
})
}
const attachedConsumeTask = await attachKuaishouIndustryVoucherToTask(task, result.voucher, {
source: 'admin_manual_consume',
now,
})
const consumeTargetTask = attachedConsumeTask || task
const taskContext = parseTaskContext(consumeTargetTask)
const hasCloudFulfillmentContext = Boolean(
taskContext.kuaishouCloudFulfillment &&
typeof taskContext.kuaishouCloudFulfillment === 'object' &&
!Array.isArray(taskContext.kuaishouCloudFulfillment),
)
const flow = hasCloudFulfillmentContext
? normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
: null
const nextContext = {
...taskContext,
...(flow
? {
kuaishouCloudFulfillment: {
...flow,
consume: {
...flow.consume,
status: 'success',
consumedAt: result.voucher.consumed_at || now,
errorMessage: '',
},
},
}
: {}),
}
const updatedTask = await updateTask(consumeTargetTask.id, {
task_status: TASK_STATUS.COMPLETED,
delivery_status: 'delivered',
result_code: 'kuaishou_industry_manual_consumed',
result_message: '电子凭证已手动核销完成',
redeemed_at: result.voucher.consumed_at || now,
last_error: '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_industry_manual_consumed',
{
oid: result.voucher.oid,
voucherCode: result.voucher.voucher_code,
consumeSerialNum: result.voucher.consume_serial_num,
actor: buildAdminActionActor(session),
},
now,
)
return { task: mapTaskActionPayload(updatedTask || consumeTargetTask) }
}
async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<KuaishouIndustryVoucherRow> {
const vouchers = await listKuaishouIndustryVouchersByTaskId(task.id)
const firstVoucher = vouchers[0]
if (firstVoucher) {
return firstVoucher
}
const taskContext = parseTaskContext(task)
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
if (voucherCode) {
const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id)
if (voucher) {
return voucher
}
}
const oidVouchers = await resolveIndustryVouchersForOrder(task.platform_order_id)
if (voucherCode) {
const matchedByCode = oidVouchers.find(
(voucher) => String(voucher.voucher_code || '').trim() === voucherCode,
)
if (matchedByCode) {
return matchedByCode
}
}
const unitIndex = Number(task.unit_index || 0) || 0
if (unitIndex > 0) {
const matchedByUnit = oidVouchers.find(
(voucher) => Number(voucher.unit_index || 0) === unitIndex,
)
if (matchedByUnit) {
return matchedByUnit
}
}
if (oidVouchers.length === 1 && oidVouchers[0]) {
return oidVouchers[0]
}
throw createHttpError('当前任务没有关联电子凭证,无法执行该操作', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_industry_voucher_missing',
})
}
function normalizeAdminTaskIndustryVoucherContext(value: unknown): JsonObject {
return value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject
: {}
}
async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise<KuaishouIndustryVoucherRow[]> {
const normalizedOid = String(platformOrderId || '').trim()
if (!normalizedOid) {
return []
}
return listKuaishouIndustryVouchersByOid(normalizedOid)
}
function buildAdminActionActor(session: AdminViewerSessionInput | null) {
return session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: null
}