将 task-finalization 拆为 dispatch/stock/return/redeem/confirm 等用例, claim 与 admin 经 executor registry 调用;admin 退号仅清理虚拟号不核销。
564 lines
18 KiB
TypeScript
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
|
|
}
|