完整流程-调试
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
|
||||
"lastRunStartedAt": "2026-05-03T09:15:36.131Z",
|
||||
"lastRunFinishedAt": "2026-05-03T09:15:37.160Z",
|
||||
"lastRunStartedAt": "2026-05-03T10:44:18.857Z",
|
||||
"lastRunFinishedAt": "2026-05-03T10:44:19.569Z",
|
||||
"lastRunStatus": "success",
|
||||
"lastErrorMessage": "",
|
||||
"fetchedCount": 40,
|
||||
"syncedCount": 0,
|
||||
"ignoredCount": 40,
|
||||
"lastOrderCreatedAt": "2026-05-03T09:15:32.000Z"
|
||||
"syncedCount": 1,
|
||||
"ignoredCount": 39,
|
||||
"lastOrderCreatedAt": "2026-05-03T10:44:07.000Z"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"items": []
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"kshopName": "洋洋运营",
|
||||
"cookie": "_did=web_412376696871A458; did=web_pdd5bx2mm4sdx78w7zg3oj1uya9cqs59; bUserId=1000598999789; userId=140000112682050; merchantSellerId=4269276762; sid=kshop.api; kshop.api_st=Cgxrc2hvcC5hcGkuc3QSoAEHQpSWH5NWp5ReO9nWzyqJIZ9NsB7YhqywKcjVzUAf1YBJODvE9HHSA5jNrO1kyoYtQmK7hlUmD7FCkDjvzTNoDJCd5mGiu_oFbUr-JYJ2--q_QNhHlY13he_NQSPQ7EYDf54NBMk1jKQNarlUnB-1WosXNgV7G5YNeZXr8CDMWkDwKUaDnG-2JGmPTsouINryXjteKsj4YZEzwWHR2GTZGhJex1zz60qNCRAHidwVN1PhX4wiIN3oIIsuly3Zub__D_EnvOhbCP_Ma363nNk4hmpKm_NpKAUwAQ; kshop.api_ph=fabd3673d2238257e37fc892abf2e8e73fd4; KS-CSRF-Token=179C60C8C34FB066A71221C9EE64B5CE; merchantSessionKey=1777795509898_209a40cd7ad55a7e842151ee83de41bb",
|
||||
"userAvatar": "https://p2-pro.a.yximgs.com/uhead/AB/2024/07/23/17/BMjAyNDA3MjMxNzA1MThfNDI2OTI3Njc2Ml8yX2hkNDg1XzgxNQ==_s.jpg",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"shopId": "4269276762",
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"provider": "khhao",
|
||||
"platform": "kuaishou",
|
||||
"shopId": "10",
|
||||
"skuCode": "测试快手1",
|
||||
"skuName": "测试1",
|
||||
"profileKey": "manual_review",
|
||||
"skuCode": "套装-浪漫天命",
|
||||
"skuName": "套装-浪漫天命",
|
||||
"profileKey": "kuaishou_ct_assisted",
|
||||
"enabled": true,
|
||||
"priority": 100,
|
||||
"config": {},
|
||||
@@ -14,7 +14,7 @@
|
||||
"externalItemId": "830",
|
||||
"externalSkuName": "测试1",
|
||||
"config": {
|
||||
"resolvedSkuName": "测试1"
|
||||
"resolvedSkuName": "套装-浪漫天命"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getAdminCloudtentaclesAsset,
|
||||
getAdminCloudtentaclesBindUrl,
|
||||
getAdminCloudtentaclesCategories,
|
||||
getAdminKuaishouCloudFulfillmentConfig,
|
||||
getAdminKuaishouEticketSourceConfig,
|
||||
getAdminCloudtentaclesKnapsack,
|
||||
getAdminCloudtentaclesSourceConfig,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
syncAdminKhhaoOrders,
|
||||
testAdminCloudtentaclesLogin,
|
||||
testAdminKhhaoLogin,
|
||||
updateAdminKuaishouCloudFulfillmentConfig,
|
||||
updateAdminKuaishouEticketSourceConfig,
|
||||
updateAdminCloudtentaclesSourceConfig,
|
||||
updateAdminAgisoShopConfigs,
|
||||
@@ -51,6 +53,7 @@ import { createJsonHandler, requireAdminRoles } from './shared.js'
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketConsumeRouteBody} AdminKuaishouEticketConsumeRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketShopInfoRouteBody} AdminKuaishouEticketShopInfoRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSourceConfigRouteBody} AdminCloudtentaclesSourceConfigRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouCloudFulfillmentConfigRouteBody} AdminKuaishouCloudFulfillmentConfigRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSendSmsCodeRouteBody} AdminCloudtentaclesSendSmsCodeRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesTestLoginRouteBody} AdminCloudtentaclesTestLoginRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesValidateSessionRouteBody} AdminCloudtentaclesValidateSessionRouteBody */
|
||||
@@ -579,4 +582,34 @@ router.post('/platform-config/fulfillment-bindings', createJsonHandler(
|
||||
},
|
||||
))
|
||||
|
||||
router.get('/platform-config/kuaishou-cloud-fulfillment', createJsonHandler(
|
||||
() => getAdminKuaishouCloudFulfillmentConfig(),
|
||||
{
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取新履约配置失败',
|
||||
scope: '[admin/platform-config/kuaishou-cloud-fulfillment]',
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/platform-config/kuaishou-cloud-fulfillment', createJsonHandler(
|
||||
(req) => updateAdminKuaishouCloudFulfillmentConfig(/** @type {AdminKuaishouCloudFulfillmentConfigRouteBody} */ (req.body)),
|
||||
{
|
||||
successMessage: '新履约配置已保存',
|
||||
errorMessage: '保存新履约配置失败',
|
||||
scope: '[admin/platform-config/kuaishou-cloud-fulfillment]',
|
||||
audit: (_req, data) => {
|
||||
const result = /** @type {{ filePath?: string, source?: { items?: unknown[] } }} */ (data)
|
||||
return {
|
||||
action: 'platform_kuaishou_cloud_fulfillment_updated',
|
||||
targetType: 'platform_config',
|
||||
targetId: 'kuaishou_cloud_fulfillment',
|
||||
data: {
|
||||
itemCount: Array.isArray(result.source?.items) ? result.source.items.length : 0,
|
||||
filePath: String(result.filePath || '').trim(),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
export default router
|
||||
|
||||
@@ -6,11 +6,14 @@ import {
|
||||
closeAdminTask,
|
||||
completeAdminTaskManualDispatch,
|
||||
confirmAdminTaskAssistedRole,
|
||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||
markAdminTaskManualReview,
|
||||
prepareAdminTaskKuaishouCloudFulfillment,
|
||||
regenerateAdminTaskClaimLink,
|
||||
redeemAdminTaskAssisted,
|
||||
releaseAdminTaskInventoryBinding,
|
||||
releaseAdminTaskInventory,
|
||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||
retryAdminTask,
|
||||
} from '../../services/admin/admin-write-service.js'
|
||||
import {
|
||||
@@ -21,6 +24,7 @@ import {
|
||||
import { createFileHandler, createJsonHandler, requireAdminRoles } from './shared.js'
|
||||
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminRouteAdminSession} AdminRouteAdminSession */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminTaskKuaishouCloudDispatchRouteBody} AdminTaskKuaishouCloudDispatchRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminTaskManualDispatchRouteBody} AdminTaskManualDispatchRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminTaskRouteParams} AdminTaskRouteParams */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminTaskRouteQuery} AdminTaskRouteQuery */
|
||||
@@ -270,4 +274,83 @@ router.post('/tasks/:taskId/complete-manual-dispatch', requireAdminRoles(['admin
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/tasks/:taskId/kuaishou-cloud/prepare', requireAdminRoles(['admin', 'operator']), createJsonHandler(
|
||||
(req) => prepareAdminTaskKuaishouCloudFulfillment(
|
||||
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
|
||||
/** @type {AdminRouteAdminSession | null} */ (req.adminSession),
|
||||
),
|
||||
{
|
||||
successMessage: '绑定资源已准备完成',
|
||||
errorMessage: '准备绑定资源失败',
|
||||
scope: '[admin/tasks/:taskId/kuaishou-cloud/prepare]',
|
||||
audit: (req, data) => {
|
||||
const result = /** @type {AdminTaskActionResponse} */ (data)
|
||||
return {
|
||||
action: 'task_kuaishou_cloud_prepare',
|
||||
targetType: 'task',
|
||||
targetId: String((/** @type {AdminTaskRouteParams} */ (req.params)).taskId),
|
||||
data: {
|
||||
taskId: result.task.taskId,
|
||||
taskNo: result.task.taskNo,
|
||||
status: result.task.status,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/tasks/:taskId/kuaishou-cloud/dispatch', requireAdminRoles(['admin', 'operator']), createJsonHandler(
|
||||
(req) => dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
|
||||
/** @type {AdminTaskKuaishouCloudDispatchRouteBody} */ (req.body),
|
||||
/** @type {AdminRouteAdminSession | null} */ (req.adminSession),
|
||||
),
|
||||
{
|
||||
successMessage: '已完成绑定确认并发货',
|
||||
errorMessage: '执行发货失败',
|
||||
scope: '[admin/tasks/:taskId/kuaishou-cloud/dispatch]',
|
||||
audit: (req, data) => {
|
||||
const result = /** @type {AdminTaskActionResponse} */ (data)
|
||||
const body = /** @type {AdminTaskKuaishouCloudDispatchRouteBody} */ (req.body)
|
||||
return {
|
||||
action: 'task_kuaishou_cloud_dispatch',
|
||||
targetType: 'task',
|
||||
targetId: String((/** @type {AdminTaskRouteParams} */ (req.params)).taskId),
|
||||
data: {
|
||||
taskId: result.task.taskId,
|
||||
taskNo: result.task.taskNo,
|
||||
status: result.task.status,
|
||||
deliveryStatus: result.task.deliveryStatus,
|
||||
ticketCodeProvided: Boolean(String(body.ticketCode || '').trim()),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/tasks/:taskId/kuaishou-cloud/return-number', requireAdminRoles(['admin', 'operator']), createJsonHandler(
|
||||
(req) => returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
|
||||
/** @type {AdminRouteAdminSession | null} */ (req.adminSession),
|
||||
),
|
||||
{
|
||||
successMessage: '号码已退还',
|
||||
errorMessage: '退还号码失败',
|
||||
scope: '[admin/tasks/:taskId/kuaishou-cloud/return-number]',
|
||||
audit: (req, data) => {
|
||||
const result = /** @type {AdminTaskActionResponse} */ (data)
|
||||
return {
|
||||
action: 'task_kuaishou_cloud_return_number',
|
||||
targetType: 'task',
|
||||
targetId: String((/** @type {AdminTaskRouteParams} */ (req.params)).taskId),
|
||||
data: {
|
||||
taskId: result.task.taskId,
|
||||
taskNo: result.task.taskNo,
|
||||
status: result.task.status,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
export default router
|
||||
|
||||
@@ -68,6 +68,11 @@ import {
|
||||
getOrderFulfillmentBindingsFilePath,
|
||||
saveOrderFulfillmentBindingConfigs,
|
||||
} from '../order/fulfillment-binding-config-service.js'
|
||||
import {
|
||||
getKuaishouCloudFulfillmentConfig,
|
||||
getKuaishouCloudFulfillmentFilePath,
|
||||
saveKuaishouCloudFulfillmentConfig,
|
||||
} from '../order/kuaishou-cloud-fulfillment-config-service.js'
|
||||
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { formatFenToAmount } from '../../utils/money.js'
|
||||
@@ -84,6 +89,7 @@ import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouCloudFulfillmentConfigInput} AdminKuaishouCloudFulfillmentConfigInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSendSmsCodeInput} AdminCloudtentaclesSendSmsCodeInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesTestLoginInput} AdminCloudtentaclesTestLoginInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesValidateSessionInput} AdminCloudtentaclesValidateSessionInput */
|
||||
@@ -778,6 +784,84 @@ export async function getAdminFulfillmentBindingConfigs() {
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminKuaishouCloudFulfillmentConfig() {
|
||||
const config = getKuaishouCloudFulfillmentConfig()
|
||||
|
||||
return {
|
||||
filePath: getKuaishouCloudFulfillmentFilePath(),
|
||||
source: {
|
||||
enabled: config.enabled !== false,
|
||||
items: (Array.isArray(config.items) ? config.items : []).map((item) => ({
|
||||
id: String(item.id || '').trim(),
|
||||
enabled: item.enabled !== false,
|
||||
priority: Number(item.priority || 100) || 100,
|
||||
provider: String(item.provider || 'khhao').trim() || 'khhao',
|
||||
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
|
||||
shopId: String(item.shopId || '').trim(),
|
||||
internalSkuCode: String(item.internalSkuCode || '').trim(),
|
||||
internalSkuName: String(item.internalSkuName || '').trim(),
|
||||
externalSkuCode: String(item.externalSkuCode || '').trim(),
|
||||
externalItemId: String(item.externalItemId || '').trim(),
|
||||
externalSkuName: String(item.externalSkuName || '').trim(),
|
||||
resolvedSkuName: String(item.resolvedSkuName || '').trim(),
|
||||
cloudSourceKey: String(item.cloudSourceKey || 'default').trim() || 'default',
|
||||
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
|
||||
cloudSkuName: String(item.cloudSkuName || '').trim(),
|
||||
vnKey: String(item.vnKey || '').trim(),
|
||||
autoBuyEnabled: item.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(item.minAssetReserve || 0) || 0,
|
||||
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
|
||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
||||
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
|
||||
notes: String(item.notes || '').trim(),
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {AdminKuaishouCloudFulfillmentConfigInput} [payload] */
|
||||
export async function updateAdminKuaishouCloudFulfillmentConfig(
|
||||
payload = /** @type {AdminKuaishouCloudFulfillmentConfigInput} */ ({}),
|
||||
) {
|
||||
const saved = saveKuaishouCloudFulfillmentConfig({
|
||||
enabled: payload.enabled !== false,
|
||||
items: Array.isArray(payload.items) ? payload.items : [],
|
||||
})
|
||||
|
||||
await syncConfiguredFulfillmentBindings()
|
||||
|
||||
return {
|
||||
filePath: getKuaishouCloudFulfillmentFilePath(),
|
||||
source: {
|
||||
enabled: saved.enabled !== false,
|
||||
items: (Array.isArray(saved.items) ? saved.items : []).map((item) => ({
|
||||
id: String(item.id || '').trim(),
|
||||
enabled: item.enabled !== false,
|
||||
priority: Number(item.priority || 100) || 100,
|
||||
provider: String(item.provider || 'khhao').trim() || 'khhao',
|
||||
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
|
||||
shopId: String(item.shopId || '').trim(),
|
||||
internalSkuCode: String(item.internalSkuCode || '').trim(),
|
||||
internalSkuName: String(item.internalSkuName || '').trim(),
|
||||
externalSkuCode: String(item.externalSkuCode || '').trim(),
|
||||
externalItemId: String(item.externalItemId || '').trim(),
|
||||
externalSkuName: String(item.externalSkuName || '').trim(),
|
||||
resolvedSkuName: String(item.resolvedSkuName || '').trim(),
|
||||
cloudSourceKey: String(item.cloudSourceKey || 'default').trim() || 'default',
|
||||
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
|
||||
cloudSkuName: String(item.cloudSkuName || '').trim(),
|
||||
vnKey: String(item.vnKey || '').trim(),
|
||||
autoBuyEnabled: item.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(item.minAssetReserve || 0) || 0,
|
||||
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
|
||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
||||
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
|
||||
notes: String(item.notes || '').trim(),
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {AdminFulfillmentBindingLookupInput} [payload] */
|
||||
export async function lookupAdminFulfillmentBindingOrder(payload = /** @type {AdminFulfillmentBindingLookupInput} */ ({})) {
|
||||
const provider = String(payload.provider || 'agiso').trim() || 'agiso'
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
getTaskPrimaryInventoryItemId,
|
||||
isAssistedClaimTask,
|
||||
isManualDispatchTask,
|
||||
mapKuaishouCloudFulfillmentContext,
|
||||
mapManualDispatchContext,
|
||||
mapRedeemResolutionContext,
|
||||
parseTaskContext,
|
||||
@@ -278,6 +279,7 @@ export async function getAdminTaskDetail(taskId, session = null) {
|
||||
roleName: String(taskState.reviewRoleName || '').trim() || '',
|
||||
},
|
||||
redeemResolution: mapRedeemResolutionContext(taskContext.redeemResolution),
|
||||
kuaishouCloudFulfillment: mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment),
|
||||
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
|
||||
events: taskEvents.map(mapAdminTaskEvent),
|
||||
operations: {
|
||||
@@ -289,6 +291,15 @@ export async function getAdminTaskDetail(taskId, session = null) {
|
||||
canClose: canViewerCloseTask(task, viewerContext),
|
||||
canMarkManualReview: viewerContext.canManageTaskLifecycle && !['redeemed', 'closed', 'manual_review'].includes(task.task_status),
|
||||
canCompleteManualDispatch: viewerContext.canManageTaskLifecycle && isManualDispatchTask(task) && !['redeemed', 'closed'].includes(task.task_status),
|
||||
canPrepareKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
&& ['pending_binding_prepare', 'manual_review', 'failed'].includes(String(task.task_status || '').trim()),
|
||||
canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
&& ['waiting_binding'].includes(String(task.task_status || '').trim()),
|
||||
canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
&& ['dispatched_pending_return'].includes(String(task.task_status || '').trim()),
|
||||
canSupportConfirmRole: canViewerConfirmAssistedRole(task, viewerContext),
|
||||
canSupportRedeem: canViewerRedeemAssistedTask(task, viewerContext),
|
||||
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData,
|
||||
|
||||
@@ -25,6 +25,67 @@ export function mapManualDispatchContext(value, viewerContext = createAdminViewe
|
||||
}
|
||||
}
|
||||
|
||||
export function mapKuaishouCloudFulfillmentContext(value) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const ticket = value.ticket && typeof value.ticket === 'object' ? value.ticket : {}
|
||||
const binding = value.binding && typeof value.binding === 'object' ? value.binding : {}
|
||||
const purchase = value.purchase && typeof value.purchase === 'object' ? value.purchase : {}
|
||||
const dispatch = value.dispatch && typeof value.dispatch === 'object' ? value.dispatch : {}
|
||||
const returnNumber = value.returnNumber && typeof value.returnNumber === 'object' ? value.returnNumber : {}
|
||||
const consume = value.consume && typeof value.consume === 'object' ? value.consume : {}
|
||||
|
||||
return {
|
||||
flowType: String(value.flowType || '').trim(),
|
||||
configId: String(value.configId || '').trim(),
|
||||
internalSkuCode: String(value.internalSkuCode || '').trim(),
|
||||
internalSkuName: String(value.internalSkuName || '').trim(),
|
||||
ticket: {
|
||||
code: String(ticket.code || '').trim(),
|
||||
capturedAt: ticket.capturedAt || null,
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
cloudSourceKey: String(binding.cloudSourceKey || '').trim(),
|
||||
skuId: Number(binding.skuId || 0) || 0,
|
||||
skuName: String(binding.skuName || '').trim(),
|
||||
vnKey: String(binding.vnKey || '').trim(),
|
||||
vnId: Number(binding.vnId || 0) || 0,
|
||||
vnPhone: String(binding.vnPhone || '').trim(),
|
||||
bindUrl: String(binding.bindUrl || '').trim(),
|
||||
bindPreparedAt: binding.bindPreparedAt || null,
|
||||
},
|
||||
purchase: {
|
||||
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
|
||||
usedKnapsack: purchase.usedKnapsack === true,
|
||||
purchaseTriggered: purchase.purchaseTriggered === true,
|
||||
assetBefore: Number(purchase.assetBefore || 0) || 0,
|
||||
assetAfter: Number(purchase.assetAfter || 0) || 0,
|
||||
purchaseAt: purchase.purchaseAt || null,
|
||||
},
|
||||
dispatch: {
|
||||
status: String(dispatch.status || 'pending').trim() || 'pending',
|
||||
dispatchAt: dispatch.dispatchAt || null,
|
||||
sendType: Number(dispatch.sendType || 0) || 0,
|
||||
note: String(dispatch.note || '').trim(),
|
||||
},
|
||||
returnNumber: {
|
||||
status: String(returnNumber.status || 'pending').trim() || 'pending',
|
||||
returnedAt: returnNumber.returnedAt || null,
|
||||
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
|
||||
},
|
||||
consume: {
|
||||
status: String(consume.status || 'pending').trim() || 'pending',
|
||||
shopId: String(consume.shopId || '').trim(),
|
||||
autoConsumeEnabled: consume.autoConsumeEnabled === true,
|
||||
},
|
||||
notes: String(value.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
export function mapRedeemResolutionContext(value) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null
|
||||
@@ -85,7 +146,7 @@ export function canViewerCloseTask(task, viewerContext) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !['redeemed', 'closed'].includes(String(task?.task_status || '').trim())
|
||||
return !['redeemed', 'completed', 'closed'].includes(String(task?.task_status || '').trim())
|
||||
}
|
||||
|
||||
export function resolveDisplayShopName(provider, shopId, shopName) {
|
||||
@@ -175,6 +236,10 @@ export function resolveOrderItemDeliveryMode(tasks, orderItemId) {
|
||||
return 'manual_dispatch'
|
||||
}
|
||||
|
||||
if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
|
||||
return 'kuaishou_cloud'
|
||||
}
|
||||
|
||||
if (task.requires_claim || getTaskPrimaryClaimTokenId(task) || task.primary_claim_token || task.claim_token) {
|
||||
return 'claim_link'
|
||||
}
|
||||
|
||||
@@ -61,15 +61,18 @@ export {
|
||||
completeAdminTaskManualDispatch,
|
||||
confirmAdminTaskAssistedRole,
|
||||
createAdminInventoryItem,
|
||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||
importAdminInventoryItems,
|
||||
invalidateAdminInventoryItem,
|
||||
markAdminTaskManualReview,
|
||||
prepareAdminTaskKuaishouCloudFulfillment,
|
||||
redeemAdminTaskAssisted,
|
||||
regenerateAdminTaskClaimLink,
|
||||
releaseAdminInventoryItem,
|
||||
releaseAdminTaskInventory,
|
||||
releaseAdminTaskInventoryBinding,
|
||||
replayAdminWebhookEvent,
|
||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||
retryAdminTask,
|
||||
} from './admin-write-service.js'
|
||||
|
||||
|
||||
@@ -348,6 +348,22 @@ function buildTaskBindingState(task) {
|
||||
return { systemBindingStatus: 'waiting_inventory', userBindingStatus: 'not_started' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'pending_binding_prepare') {
|
||||
return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'waiting_binding') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'dispatched_pending_return') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'completed') {
|
||||
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'manual_review') {
|
||||
return { systemBindingStatus: 'manual_review', userBindingStatus: 'binding_exception' }
|
||||
}
|
||||
|
||||
@@ -22,6 +22,23 @@ import { confirmClaimRoleForAdminTask, redeemClaimTaskForAdminTask } from '../cl
|
||||
import { reserveInventoryForTask } from '../order/inventory-service.js'
|
||||
import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js'
|
||||
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-service.js'
|
||||
import { getCloudtentaclesSourceConfig } from '../platforms/cloudtentacles/source-config-service.js'
|
||||
import { getCloudtentaclesSessionState } from '../platforms/cloudtentacles/session-state-service.js'
|
||||
import {
|
||||
buyCloudtentaclesSku,
|
||||
getCloudtentaclesAsset,
|
||||
listCloudtentaclesSku,
|
||||
useCloudtentaclesSku,
|
||||
} from '../platforms/cloudtentacles/catalog-service.js'
|
||||
import { getCloudtentaclesKnapsack } from '../platforms/cloudtentacles/knapsack-service.js'
|
||||
import {
|
||||
appointCloudtentaclesVirtualNumber,
|
||||
backCloudtentaclesVirtualNumber,
|
||||
fetchCloudtentaclesVirtualNumberCode,
|
||||
generateCloudtentaclesLoginCode,
|
||||
getCloudtentaclesBindUrl,
|
||||
verifyCloudtentaclesLoginCode,
|
||||
} from '../platforms/cloudtentacles/virtual-number-service.js'
|
||||
import { closeTencentBrowserSession } from '../session/session.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
@@ -56,6 +73,7 @@ import {
|
||||
/** @typedef {import('../../types/admin-write-models.js').AdminTaskBindingReleaseResponse} AdminTaskBindingReleaseResponse */
|
||||
/** @typedef {import('../../types/admin-write-models.js').AdminTaskManualDispatchResponse} AdminTaskManualDispatchResponse */
|
||||
/** @typedef {import('../../types/admin-write-models.js').AdminWebhookReplayResponse} AdminWebhookReplayResponse */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminTaskKuaishouCloudDispatchInput} AdminTaskKuaishouCloudDispatchInput */
|
||||
|
||||
/** @returns {Promise<AdminInventoryMutationResponse>} */
|
||||
/** @param {AdminInventoryCreateInput} [payload] */
|
||||
@@ -675,6 +693,352 @@ export async function completeAdminTaskManualDispatch(taskId, payload = {}, sess
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {Promise<AdminTaskActionResponse>} */
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
/** @param {AdminViewerSessionInput | null} [session] */
|
||||
export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session = null) {
|
||||
const task = await getRequiredTask(taskId)
|
||||
const now = nowIso()
|
||||
const viewerContext = createAdminViewerContext(session)
|
||||
|
||||
if (!viewerContext.canManageTaskLifecycle) {
|
||||
throw createHttpError('当前账号没有权限准备绑定资源', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_task_prepare_kuaishou_cloud_forbidden',
|
||||
})
|
||||
}
|
||||
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是快手 cloud 履约任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_not_kuaishou_cloud',
|
||||
})
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
const cloudContext = resolvePersistedCloudtentaclesContext()
|
||||
|
||||
if (!flow.binding.skuId || !flow.binding.vnKey) {
|
||||
throw createHttpError('当前任务缺少 cloud SKU 或 VN Key 配置', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_kuaishou_cloud_missing_binding_config',
|
||||
})
|
||||
}
|
||||
|
||||
const knapsack = await getCloudtentaclesKnapsack(cloudContext)
|
||||
const knapsackItem = (Array.isArray(knapsack.items) ? knapsack.items : []).find((item) => Number(item.id || 0) === flow.binding.skuId) || null
|
||||
let usedKnapsack = Number(knapsackItem?.count || 0) > 0
|
||||
let purchaseTriggered = false
|
||||
let assetBefore = 0
|
||||
let assetAfter = 0
|
||||
|
||||
if (!usedKnapsack) {
|
||||
if (!flow.purchase.autoBuyEnabled) {
|
||||
throw createHttpError('背包中没有现成库存,且当前配置未开启自动购买', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_kuaishou_cloud_auto_buy_disabled',
|
||||
})
|
||||
}
|
||||
|
||||
const [asset, skuList] = await Promise.all([
|
||||
getCloudtentaclesAsset(cloudContext),
|
||||
listCloudtentaclesSku(cloudContext),
|
||||
])
|
||||
const targetSku = (Array.isArray(skuList.items) ? skuList.items : []).find((item) => Number(item.id || 0) === flow.binding.skuId) || null
|
||||
|
||||
if (!targetSku) {
|
||||
throw createHttpError(`cloudtentacles 未找到 SKU ${flow.binding.skuId}`, {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_task_kuaishou_cloud_sku_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
assetBefore = Number(asset.asset || 0) || 0
|
||||
const targetPrice = Number(targetSku.price || 0) || 0
|
||||
const requiredAsset = targetPrice + flow.purchase.minAssetReserve
|
||||
if (assetBefore < requiredAsset) {
|
||||
throw createHttpError(`余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_kuaishou_cloud_asset_not_enough',
|
||||
})
|
||||
}
|
||||
|
||||
await buyCloudtentaclesSku({
|
||||
...cloudContext,
|
||||
id: flow.binding.skuId,
|
||||
count: 1,
|
||||
})
|
||||
purchaseTriggered = true
|
||||
|
||||
const assetResult = await getCloudtentaclesAsset(cloudContext)
|
||||
assetAfter = Number(assetResult.asset || 0) || 0
|
||||
}
|
||||
|
||||
const appointed = await appointCloudtentaclesVirtualNumber({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
})
|
||||
const vnId = Number(appointed.item?.id || 0)
|
||||
const vnPhone = String(appointed.item?.phone || '').trim()
|
||||
|
||||
if (!vnId || !vnPhone) {
|
||||
throw createHttpError('申请虚拟号成功但返回数据不完整', {
|
||||
statusCode: 502,
|
||||
errorCode: 'admin_task_kuaishou_cloud_invalid_vn',
|
||||
})
|
||||
}
|
||||
|
||||
await generateCloudtentaclesLoginCode({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
id: vnId,
|
||||
})
|
||||
|
||||
const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
phone: vnPhone,
|
||||
})
|
||||
|
||||
await verifyCloudtentaclesLoginCode({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
id: vnId,
|
||||
code: fetchedCode.code,
|
||||
})
|
||||
|
||||
const bindUrlResult = await getCloudtentaclesBindUrl({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
id: vnId,
|
||||
})
|
||||
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
binding: {
|
||||
...flow.binding,
|
||||
prepareStatus: 'ready',
|
||||
vnId,
|
||||
vnPhone,
|
||||
bindUrl: bindUrlResult.bindUrl,
|
||||
bindPreparedAt: now,
|
||||
},
|
||||
purchase: {
|
||||
...flow.purchase,
|
||||
usedKnapsack,
|
||||
purchaseTriggered,
|
||||
assetBefore,
|
||||
assetAfter,
|
||||
purchaseAt: purchaseTriggered ? now : flow.purchase.purchaseAt,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: 'waiting_binding',
|
||||
inventory_status: 'not_required',
|
||||
user_action_status: 'pending',
|
||||
last_error: '',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_binding_prepared', {
|
||||
skuId: flow.binding.skuId,
|
||||
vnKey: flow.binding.vnKey,
|
||||
vnId,
|
||||
vnPhoneMasked: maskPhone(vnPhone),
|
||||
bindUrl: bindUrlResult.bindUrl,
|
||||
purchaseTriggered,
|
||||
usedKnapsack,
|
||||
}, now)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {Promise<AdminTaskActionResponse>} */
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
/** @param {AdminTaskKuaishouCloudDispatchInput} [payload] */
|
||||
/** @param {AdminViewerSessionInput | null} [session] */
|
||||
export async function dispatchAdminTaskKuaishouCloudFulfillment(taskId, payload = {}, session = null) {
|
||||
const task = await getRequiredTask(taskId)
|
||||
const now = nowIso()
|
||||
const viewerContext = createAdminViewerContext(session)
|
||||
|
||||
if (!viewerContext.canManageTaskLifecycle) {
|
||||
throw createHttpError('当前账号没有权限执行发货', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_task_dispatch_kuaishou_cloud_forbidden',
|
||||
})
|
||||
}
|
||||
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是快手 cloud 履约任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_not_kuaishou_cloud',
|
||||
})
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
const cloudContext = resolvePersistedCloudtentaclesContext()
|
||||
|
||||
if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) {
|
||||
throw createHttpError('当前任务还没有准备好绑定资源,请先准备绑定资源', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_kuaishou_cloud_not_prepared',
|
||||
})
|
||||
}
|
||||
|
||||
const ticketCode = String(payload.ticketCode || '').trim()
|
||||
const dispatchResult = await useCloudtentaclesSku({
|
||||
...cloudContext,
|
||||
id: flow.binding.skuId,
|
||||
virtualNumberId: flow.binding.vnId,
|
||||
phone: flow.binding.vnPhone,
|
||||
})
|
||||
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
ticket: {
|
||||
...flow.ticket,
|
||||
code: ticketCode || flow.ticket.code,
|
||||
capturedAt: ticketCode ? now : flow.ticket.capturedAt,
|
||||
capturedBy: ticketCode && session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: flow.ticket.capturedBy,
|
||||
},
|
||||
dispatch: {
|
||||
...flow.dispatch,
|
||||
status: 'success',
|
||||
dispatchAt: now,
|
||||
dispatchBy: session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
sendType: Number(dispatchResult.sendType || 0) || 0,
|
||||
note: String(dispatchResult.note || dispatchResult.responseMessage || '').trim(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: 'dispatched_pending_return',
|
||||
delivery_status: 'delivered',
|
||||
result_code: 'kuaishou_cloud_dispatched',
|
||||
result_message: String(dispatchResult.responseMessage || dispatchResult.note || 'cloudtentacles 发货成功').trim(),
|
||||
last_error: '',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_dispatched', {
|
||||
ticketCodeMasked: maskCode(ticketCode),
|
||||
skuId: flow.binding.skuId,
|
||||
vnId: flow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||
sendType: dispatchResult.sendType,
|
||||
note: dispatchResult.note,
|
||||
}, now)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {Promise<AdminTaskActionResponse>} */
|
||||
/** @param {AdminEntityIdInput} taskId */
|
||||
/** @param {AdminViewerSessionInput | null} [session] */
|
||||
export async function returnNumberAdminTaskKuaishouCloudFulfillment(taskId, session = null) {
|
||||
const task = await getRequiredTask(taskId)
|
||||
const now = nowIso()
|
||||
const viewerContext = createAdminViewerContext(session)
|
||||
|
||||
if (!viewerContext.canManageTaskLifecycle) {
|
||||
throw createHttpError('当前账号没有权限退还号码', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_task_return_kuaishou_cloud_forbidden',
|
||||
})
|
||||
}
|
||||
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是快手 cloud 履约任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_not_kuaishou_cloud',
|
||||
})
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
const cloudContext = resolvePersistedCloudtentaclesContext()
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||
throw createHttpError('当前任务缺少可退还的虚拟号信息', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_kuaishou_cloud_missing_return_context',
|
||||
})
|
||||
}
|
||||
|
||||
await backCloudtentaclesVirtualNumber({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
id: flow.binding.vnId,
|
||||
})
|
||||
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
returnNumber: {
|
||||
...flow.returnNumber,
|
||||
status: 'success',
|
||||
returnedAt: now,
|
||||
returnedBy: session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: 'completed',
|
||||
delivery_status: 'delivered',
|
||||
result_code: 'kuaishou_cloud_completed',
|
||||
result_message: 'cloudtentacles 发货并退号完成',
|
||||
redeemed_at: now,
|
||||
last_error: '',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_number_returned', {
|
||||
vnId: flow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||
}, now)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AdminInventoryImportInput} payload
|
||||
* @returns {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>}
|
||||
@@ -725,6 +1089,116 @@ function normalizeInventoryImportRows(payload) {
|
||||
return dedupeRows(rows)
|
||||
}
|
||||
|
||||
function isKuaishouCloudTask(task) {
|
||||
return String(task?.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
}
|
||||
|
||||
function resolvePersistedCloudtentaclesContext() {
|
||||
const source = getCloudtentaclesSourceConfig()
|
||||
const session = getCloudtentaclesSessionState()
|
||||
const token = String(session.token || '').trim()
|
||||
|
||||
if (!token) {
|
||||
throw createHttpError('当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_kuaishou_cloud_missing_cloud_token',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl: String(session.baseUrl || source.baseUrl || '').trim() || 'https://123.207.217.176',
|
||||
token,
|
||||
deviceId: String(session.deviceId || source.deviceId || '-').trim() || '-',
|
||||
deviceType: Number(session.deviceType ?? source.deviceType ?? 0),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKuaishouCloudFlow(value) {
|
||||
const source = value && typeof value === 'object' ? value : {}
|
||||
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {}
|
||||
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
|
||||
const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
|
||||
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {}
|
||||
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
|
||||
|
||||
return {
|
||||
...source,
|
||||
configId: String(source.configId || '').trim(),
|
||||
internalSkuCode: String(source.internalSkuCode || '').trim(),
|
||||
internalSkuName: String(source.internalSkuName || '').trim(),
|
||||
ticket: {
|
||||
code: String(ticket.code || '').trim(),
|
||||
capturedAt: ticket.capturedAt || null,
|
||||
capturedBy: ticket.capturedBy || null,
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
cloudSourceKey: String(binding.cloudSourceKey || 'default').trim() || 'default',
|
||||
skuId: Number(binding.skuId || 0) || 0,
|
||||
skuName: String(binding.skuName || '').trim(),
|
||||
vnKey: String(binding.vnKey || '').trim(),
|
||||
vnId: Number(binding.vnId || 0) || 0,
|
||||
vnPhone: String(binding.vnPhone || '').trim(),
|
||||
bindUrl: String(binding.bindUrl || '').trim(),
|
||||
bindPreparedAt: binding.bindPreparedAt || null,
|
||||
},
|
||||
purchase: {
|
||||
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
|
||||
usedKnapsack: purchase.usedKnapsack === true,
|
||||
purchaseTriggered: purchase.purchaseTriggered === true,
|
||||
assetBefore: Number(purchase.assetBefore || 0) || 0,
|
||||
assetAfter: Number(purchase.assetAfter || 0) || 0,
|
||||
purchaseAt: purchase.purchaseAt || null,
|
||||
},
|
||||
dispatch: {
|
||||
status: String(dispatch.status || 'pending').trim() || 'pending',
|
||||
dispatchAt: dispatch.dispatchAt || null,
|
||||
dispatchBy: dispatch.dispatchBy || null,
|
||||
sendType: Number(dispatch.sendType || 0) || 0,
|
||||
note: String(dispatch.note || '').trim(),
|
||||
},
|
||||
returnNumber: {
|
||||
status: String(returnNumber.status || 'pending').trim() || 'pending',
|
||||
returnedAt: returnNumber.returnedAt || null,
|
||||
returnedBy: returnNumber.returnedBy || null,
|
||||
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
|
||||
},
|
||||
consume: {
|
||||
status: String(consume.status || 'pending').trim() || 'pending',
|
||||
shopId: String(consume.shopId || '').trim(),
|
||||
autoConsumeEnabled: consume.autoConsumeEnabled === true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function maskPhone(value) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (text.length <= 7) {
|
||||
return `${text.slice(0, 2)}***${text.slice(-2)}`
|
||||
}
|
||||
|
||||
return `${text.slice(0, 3)}****${text.slice(-4)}`
|
||||
}
|
||||
|
||||
function maskCode(value) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (text.length <= 8) {
|
||||
return `${text.slice(0, 2)}***${text.slice(-2)}`
|
||||
}
|
||||
|
||||
return `${text.slice(0, 4)}****${text.slice(-4)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>} rows
|
||||
*/
|
||||
@@ -797,14 +1271,3 @@ function normalizeManualDispatchOutcome(value) {
|
||||
function getTaskClaimExpiresAt(task) {
|
||||
return task?.claim_expires_at || task?.primary_claim_expires_at || null
|
||||
}
|
||||
|
||||
function maskCode(value) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
if (text.length <= 8) {
|
||||
return `${text.slice(0, 2)}****${text.slice(-2)}`
|
||||
}
|
||||
return `${text.slice(0, 4)}****${text.slice(-4)}`
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import { upsertProductMatchRule } from '../../repositories/product-match-rule-re
|
||||
import { normalizeProductName } from '../order/product-match-service.js'
|
||||
import { query } from '../../db/client.js'
|
||||
import { getOrderFulfillmentBindingConfigs } from '../order/fulfillment-binding-config-service.js'
|
||||
import {
|
||||
getKuaishouCloudFulfillmentConfig,
|
||||
mapKuaishouCloudFulfillmentItemsToBindings,
|
||||
} from '../order/kuaishou-cloud-fulfillment-config-service.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
const CORE_PROFILES = [
|
||||
@@ -53,6 +57,15 @@ const CORE_PROFILES = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
profileKey: 'kuaishou_ct_assisted',
|
||||
name: '快手 cloud 履约',
|
||||
executorKey: 'kuaishou_ct_assisted',
|
||||
requiresClaim: false,
|
||||
autoDispatch: false,
|
||||
inventoryStrategy: 'external_platform',
|
||||
requirements: [],
|
||||
},
|
||||
]
|
||||
|
||||
export async function ensureFulfillmentCatalogBootstrapped() {
|
||||
@@ -93,7 +106,10 @@ async function ensureCoreProfiles() {
|
||||
|
||||
export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
|
||||
const timestamp = nowIso()
|
||||
const bindingsToApply = getOrderFulfillmentBindingConfigs()
|
||||
const bindingsToApply = [
|
||||
...getOrderFulfillmentBindingConfigs(),
|
||||
...mapKuaishouCloudFulfillmentItemsToBindings(getKuaishouCloudFulfillmentConfig()),
|
||||
]
|
||||
|
||||
await query('DELETE FROM product_match_rules')
|
||||
await query('DELETE FROM sku_fulfillment_bindings')
|
||||
|
||||
@@ -41,6 +41,7 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
const requirements = await listFulfillmentProfileRequirements(profile.profile_id || profile.id)
|
||||
const primaryRequirement = requirements.find((requirement) => requirement.is_required !== false) || requirements[0] || null
|
||||
const quantity = Math.max(1, Number(item.quantity || 1))
|
||||
const fulfillmentConfig = parseJsonObject(profile.config_json)
|
||||
|
||||
for (let index = 0; index < quantity; index += 1) {
|
||||
const createdAt = nowIso()
|
||||
@@ -84,6 +85,58 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
credentialType: String(primaryRequirement.credential_type || primaryRequirement.credentialType || 'tencent_code'),
|
||||
}
|
||||
: null,
|
||||
kuaishouCloudFulfillment: isKuaishouCloudExecutor(profile.executor_key)
|
||||
? {
|
||||
flowType: 'kuaishou_cloud_fulfillment',
|
||||
configId: String(fulfillmentConfig.configId || '').trim(),
|
||||
internalSkuCode: item.sku_code,
|
||||
internalSkuName: item.sku_name,
|
||||
ticket: {
|
||||
code: '',
|
||||
capturedAt: null,
|
||||
capturedBy: null,
|
||||
},
|
||||
binding: {
|
||||
prepareStatus: 'pending',
|
||||
cloudSourceKey: String(fulfillmentConfig.cloudtentacles?.cloudSourceKey || 'default').trim() || 'default',
|
||||
skuId: Number(fulfillmentConfig.cloudtentacles?.skuId || 0) || 0,
|
||||
skuName: String(fulfillmentConfig.cloudtentacles?.skuName || '').trim(),
|
||||
vnKey: String(fulfillmentConfig.cloudtentacles?.vnKey || '').trim(),
|
||||
vnId: 0,
|
||||
vnPhone: '',
|
||||
bindUrl: '',
|
||||
bindPreparedAt: null,
|
||||
},
|
||||
purchase: {
|
||||
autoBuyEnabled: fulfillmentConfig.cloudtentacles?.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(fulfillmentConfig.cloudtentacles?.minAssetReserve || 0) || 0,
|
||||
usedKnapsack: false,
|
||||
purchaseTriggered: false,
|
||||
assetBefore: 0,
|
||||
assetAfter: 0,
|
||||
purchaseAt: null,
|
||||
},
|
||||
dispatch: {
|
||||
status: 'pending',
|
||||
dispatchAt: null,
|
||||
dispatchBy: null,
|
||||
sendType: 0,
|
||||
note: '',
|
||||
},
|
||||
returnNumber: {
|
||||
status: 'pending',
|
||||
returnedAt: null,
|
||||
returnedBy: null,
|
||||
autoReturnEnabled: fulfillmentConfig.cloudtentacles?.autoReturnNumberAfterDispatch === true,
|
||||
},
|
||||
consume: {
|
||||
status: 'pending',
|
||||
shopId: String(fulfillmentConfig.kuaishouConsume?.shopId || '').trim(),
|
||||
autoConsumeEnabled: fulfillmentConfig.kuaishouConsume?.autoConsumeAfterDispatch === true,
|
||||
},
|
||||
notes: String(fulfillmentConfig.notes || '').trim(),
|
||||
}
|
||||
: null,
|
||||
}),
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
@@ -114,6 +167,27 @@ async function preparePaidTask(task) {
|
||||
return task
|
||||
}
|
||||
|
||||
if (isKuaishouCloudExecutor(task.executor_key)) {
|
||||
if ([
|
||||
'pending_binding_prepare',
|
||||
'waiting_binding',
|
||||
'dispatched_pending_return',
|
||||
'completed',
|
||||
'manual_review',
|
||||
'failed',
|
||||
].includes(String(task.task_status || '').trim())) {
|
||||
return task
|
||||
}
|
||||
|
||||
return updateTask(task.id, {
|
||||
task_status: 'pending_binding_prepare',
|
||||
inventory_status: 'not_required',
|
||||
user_action_status: 'pending',
|
||||
last_error: task.last_error || '等待准备绑定资源',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
if (!task.requires_claim || String(task.executor_key || '').trim() === 'manual_dispatch') {
|
||||
return updateTask(task.id, {
|
||||
task_status: 'manual_review',
|
||||
@@ -182,6 +256,10 @@ function resolvePaidTaskStatus(profile) {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (isKuaishouCloudExecutor(profile?.executor_key)) {
|
||||
return 'pending_binding_prepare'
|
||||
}
|
||||
|
||||
if (String(profile?.executor_key || '').trim() === 'manual_dispatch') {
|
||||
return 'manual_review'
|
||||
}
|
||||
@@ -206,3 +284,24 @@ function parseTaskContext(task) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonObject(value) {
|
||||
if (!value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '{}'))
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function isKuaishouCloudExecutor(value) {
|
||||
return String(value || '').trim() === 'kuaishou_ct_assisted'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||
|
||||
const KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-cloud-fulfillment.json')
|
||||
|
||||
export function getKuaishouCloudFulfillmentFilePath() {
|
||||
return KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH
|
||||
}
|
||||
|
||||
export function getKuaishouCloudFulfillmentConfig() {
|
||||
return loadKuaishouCloudFulfillmentConfigFromFile()
|
||||
}
|
||||
|
||||
export function saveKuaishouCloudFulfillmentConfig(rawValue) {
|
||||
const normalized = normalizeKuaishouCloudFulfillmentConfig(rawValue)
|
||||
fs.mkdirSync(path.dirname(KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH,
|
||||
`${JSON.stringify(normalized, null, 2)}\n`,
|
||||
'utf8',
|
||||
)
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function mapKuaishouCloudFulfillmentItemsToBindings(config = {}) {
|
||||
const items = Array.isArray(config.items) ? config.items : []
|
||||
|
||||
return items
|
||||
.filter((item) => item && item.enabled !== false)
|
||||
.map((item) => ({
|
||||
provider: String(item.provider || 'khhao').trim() || 'khhao',
|
||||
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
|
||||
shopId: String(item.shopId || '').trim(),
|
||||
skuCode: String(item.internalSkuCode || '').trim(),
|
||||
skuName: String(item.internalSkuName || '').trim(),
|
||||
profileKey: 'kuaishou_ct_assisted',
|
||||
enabled: item.enabled !== false,
|
||||
priority: normalizePriority(item.priority),
|
||||
config: {
|
||||
flowType: 'kuaishou_cloud_fulfillment',
|
||||
configId: String(item.id || '').trim(),
|
||||
cloudtentacles: {
|
||||
cloudSourceKey: String(item.cloudSourceKey || 'default').trim() || 'default',
|
||||
skuId: normalizePositiveInteger(item.cloudSkuId),
|
||||
skuName: String(item.cloudSkuName || '').trim(),
|
||||
vnKey: String(item.vnKey || '').trim(),
|
||||
autoBuyEnabled: item.autoBuyEnabled !== false,
|
||||
minAssetReserve: normalizeNonNegativeInteger(item.minAssetReserve, 0),
|
||||
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
|
||||
},
|
||||
kuaishouConsume: {
|
||||
shopId: String(item.kuaishouConsumeShopId || '').trim(),
|
||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
||||
},
|
||||
notes: String(item.notes || '').trim(),
|
||||
},
|
||||
match: {
|
||||
externalSkuCode: String(item.externalSkuCode || '').trim(),
|
||||
externalItemId: String(item.externalItemId || '').trim(),
|
||||
externalSkuName: String(item.externalSkuName || '').trim(),
|
||||
config: {
|
||||
resolvedSkuName: String(item.resolvedSkuName || item.internalSkuName || '').trim(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
.filter((item) => item.skuCode && (item.match.externalSkuCode || item.match.externalItemId || item.match.externalSkuName))
|
||||
}
|
||||
|
||||
function loadKuaishouCloudFulfillmentConfigFromFile() {
|
||||
if (!fs.existsSync(KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH)) {
|
||||
return normalizeKuaishouCloudFulfillmentConfig({})
|
||||
}
|
||||
|
||||
try {
|
||||
const rawText = fs.readFileSync(KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH, 'utf8')
|
||||
const parsed = JSON.parse(rawText)
|
||||
return normalizeKuaishouCloudFulfillmentConfig(parsed)
|
||||
} catch {
|
||||
return normalizeKuaishouCloudFulfillmentConfig({})
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKuaishouCloudFulfillmentConfig(rawValue) {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
return {
|
||||
enabled: source.enabled !== false,
|
||||
items: Array.isArray(source.items)
|
||||
? source.items.map((item) => normalizeKuaishouCloudFulfillmentItem(item)).filter(Boolean)
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKuaishouCloudFulfillmentItem(rawValue) {
|
||||
if (!isPlainObject(rawValue)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const internalSkuCode = String(rawValue.internalSkuCode || '').trim()
|
||||
const cloudSkuId = normalizePositiveInteger(rawValue.cloudSkuId)
|
||||
const vnKey = String(rawValue.vnKey || '').trim()
|
||||
const externalSkuCode = String(rawValue.externalSkuCode || '').trim()
|
||||
const externalItemId = String(rawValue.externalItemId || '').trim()
|
||||
const externalSkuName = String(rawValue.externalSkuName || '').trim()
|
||||
|
||||
if (!internalSkuCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!cloudSkuId || !vnKey) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!externalSkuCode && !externalItemId && !externalSkuName) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(rawValue.id || internalSkuCode).trim() || internalSkuCode,
|
||||
enabled: rawValue.enabled !== false,
|
||||
priority: normalizePriority(rawValue.priority),
|
||||
provider: String(rawValue.provider || 'khhao').trim() || 'khhao',
|
||||
platform: String(rawValue.platform || 'kuaishou').trim() || 'kuaishou',
|
||||
shopId: String(rawValue.shopId || '').trim(),
|
||||
internalSkuCode,
|
||||
internalSkuName: String(rawValue.internalSkuName || '').trim() || internalSkuCode,
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
externalSkuName,
|
||||
resolvedSkuName: String(rawValue.resolvedSkuName || rawValue.internalSkuName || '').trim(),
|
||||
cloudSourceKey: String(rawValue.cloudSourceKey || 'default').trim() || 'default',
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(rawValue.cloudSkuName || '').trim(),
|
||||
vnKey,
|
||||
autoBuyEnabled: rawValue.autoBuyEnabled !== false,
|
||||
minAssetReserve: normalizeNonNegativeInteger(rawValue.minAssetReserve, 0),
|
||||
autoReturnNumberAfterDispatch: rawValue.autoReturnNumberAfterDispatch === true,
|
||||
autoConsumeAfterDispatch: rawValue.autoConsumeAfterDispatch === true,
|
||||
kuaishouConsumeShopId: String(rawValue.kuaishouConsumeShopId || '').trim(),
|
||||
notes: String(rawValue.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0
|
||||
}
|
||||
|
||||
function normalizeNonNegativeInteger(value, fallback) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback
|
||||
}
|
||||
|
||||
function normalizePriority(value) {
|
||||
const parsed = Number(value)
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return 100
|
||||
}
|
||||
|
||||
return Math.max(1, Math.round(parsed))
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -66,6 +66,10 @@ export {}
|
||||
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKuaishouCloudFulfillmentConfigInput} AdminKuaishouCloudFulfillmentConfigRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSendSmsCodeInput} AdminCloudtentaclesSendSmsCodeRouteBody
|
||||
*/
|
||||
@@ -126,6 +130,10 @@ export {}
|
||||
* @typedef {import('./admin-write-inputs.js').AdminTaskManualDispatchInput} AdminTaskManualDispatchRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminTaskKuaishouCloudDispatchInput} AdminTaskKuaishouCloudDispatchRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminManualRedeemCreateInput} AdminManualRedeemCreateRouteBody
|
||||
*/
|
||||
|
||||
@@ -67,6 +67,40 @@ export {}
|
||||
* }} AdminAgisoShopConfigSaveInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* id?: string
|
||||
* enabled?: boolean
|
||||
* priority?: number | string
|
||||
* provider?: string
|
||||
* platform?: string
|
||||
* shopId?: string
|
||||
* internalSkuCode?: string
|
||||
* internalSkuName?: string
|
||||
* externalSkuCode?: string
|
||||
* externalItemId?: string
|
||||
* externalSkuName?: string
|
||||
* resolvedSkuName?: string
|
||||
* cloudSourceKey?: string
|
||||
* cloudSkuId?: number | string
|
||||
* cloudSkuName?: string
|
||||
* vnKey?: string
|
||||
* autoBuyEnabled?: boolean
|
||||
* minAssetReserve?: number | string
|
||||
* autoReturnNumberAfterDispatch?: boolean
|
||||
* autoConsumeAfterDispatch?: boolean
|
||||
* kuaishouConsumeShopId?: string
|
||||
* notes?: string
|
||||
* }} AdminKuaishouCloudFulfillmentItemInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* enabled?: boolean
|
||||
* items?: AdminKuaishouCloudFulfillmentItemInput[]
|
||||
* }} AdminKuaishouCloudFulfillmentConfigInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* enabled?: boolean
|
||||
@@ -302,6 +336,12 @@ export {}
|
||||
* }} AdminTaskManualDispatchInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* ticketCode?: string
|
||||
* }} AdminTaskKuaishouCloudDispatchInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* proofValue?: string
|
||||
|
||||
@@ -8,15 +8,15 @@ const props = defineProps<{
|
||||
function resolveTone(status: string) {
|
||||
const normalized = String(status || '').trim().toLowerCase()
|
||||
|
||||
if (['paid', 'link_generated', 'redeemed', 'available', 'delivered', 'success', 'active', 'system_bound', 'binding_completed'].includes(normalized)) {
|
||||
if (['paid', 'link_generated', 'redeemed', 'available', 'delivered', 'success', 'active', 'system_bound', 'binding_completed', 'completed'].includes(normalized)) {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (['claimed', 'role_confirmed', 'redeeming', 'reserved', 'processing', 'admin', 'support', 'waiting_user_claim', 'link_opened', 'binding_confirmed', 'binding_in_progress', 'user_binding', 'pending', 'waiting'].includes(normalized)) {
|
||||
if (['claimed', 'role_confirmed', 'redeeming', 'reserved', 'processing', 'admin', 'support', 'waiting_user_claim', 'link_opened', 'binding_confirmed', 'binding_in_progress', 'user_binding', 'pending', 'waiting', 'waiting_binding'].includes(normalized)) {
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
if (['waiting_inventory', 'retry_pending', 'manual_review', 'invalid', 'expired', 'operator', 'pending_binding', 'binding_exception', 'not_started', 'consumed', 'skipped'].includes(normalized)) {
|
||||
if (['waiting_inventory', 'retry_pending', 'manual_review', 'invalid', 'expired', 'operator', 'pending_binding', 'binding_exception', 'not_started', 'consumed', 'skipped', 'pending_binding_prepare', 'dispatched_pending_return'].includes(normalized)) {
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
@@ -71,7 +71,11 @@ function resolveLabel(status: string) {
|
||||
processing: '处理中',
|
||||
system_bound: '系统已绑定',
|
||||
pending_binding: '待系统绑定',
|
||||
pending_binding_prepare: '待准备绑定',
|
||||
not_started: '未开始',
|
||||
waiting_binding: '待客户绑定',
|
||||
dispatched_pending_return: '待退还号码',
|
||||
completed: '已完成',
|
||||
waiting_user_claim: '待用户打开链接',
|
||||
link_opened: '已打开链接',
|
||||
binding_confirmed: '已提交绑定',
|
||||
|
||||
@@ -93,6 +93,11 @@ const router = createRouter({
|
||||
meta: { allowedRoles: ['admin'] },
|
||||
component: () => import('@/views/admin/AdminFulfillmentBindingsView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'platform-kuaishou-cloud-fulfillment',
|
||||
meta: { allowedRoles: ['admin'] },
|
||||
component: () => import('@/views/admin/AdminKuaishouCloudFulfillmentView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'webhook-events/:eventId',
|
||||
meta: { allowedRoles: ['admin', 'operator'] },
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
AdminKhhaoSyncState,
|
||||
AdminKhhaoOrderSyncResult,
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouCloudFulfillmentConfig,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
AdminKuaishouEticketSourceConfig,
|
||||
@@ -418,6 +419,20 @@ export function saveAdminFulfillmentBindingConfigs(payload: { bindings: Array<Re
|
||||
}>('/api/v1/admin/platform-config/fulfillment-bindings', payload)
|
||||
}
|
||||
|
||||
export function fetchAdminKuaishouCloudFulfillmentConfig() {
|
||||
return apiGet<{
|
||||
filePath: string
|
||||
source: AdminKuaishouCloudFulfillmentConfig
|
||||
}>('/api/v1/admin/platform-config/kuaishou-cloud-fulfillment')
|
||||
}
|
||||
|
||||
export function saveAdminKuaishouCloudFulfillmentConfig(payload: AdminKuaishouCloudFulfillmentConfig) {
|
||||
return apiPost<{
|
||||
filePath: string
|
||||
source: AdminKuaishouCloudFulfillmentConfig
|
||||
}>('/api/v1/admin/platform-config/kuaishou-cloud-fulfillment', payload as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
export function fetchAdminOrders(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminOrderListItem[]; pagination: AdminPagination }>('/api/v1/admin/orders', params)
|
||||
}
|
||||
@@ -485,6 +500,23 @@ export function completeAdminTaskManualDispatch(
|
||||
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/complete-manual-dispatch`, payload)
|
||||
}
|
||||
|
||||
export function prepareAdminTaskKuaishouCloudFulfillment(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/prepare`, {})
|
||||
}
|
||||
|
||||
export function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
taskId: number | string,
|
||||
payload: {
|
||||
ticketCode?: string
|
||||
},
|
||||
) {
|
||||
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/dispatch`, payload)
|
||||
}
|
||||
|
||||
export function returnNumberAdminTaskKuaishouCloudFulfillment(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/return-number`, {})
|
||||
}
|
||||
|
||||
export function fetchAdminInventoryItems(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminInventoryItemListItem[]; pagination: AdminPagination }>('/api/v1/admin/inventory', params)
|
||||
}
|
||||
|
||||
@@ -335,6 +335,36 @@ export interface AdminFulfillmentBindingConfigItem {
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminKuaishouCloudFulfillmentItem {
|
||||
id: string
|
||||
enabled: boolean
|
||||
priority: number
|
||||
provider: string
|
||||
platform: string
|
||||
shopId: string
|
||||
internalSkuCode: string
|
||||
internalSkuName: string
|
||||
externalSkuCode: string
|
||||
externalItemId: string
|
||||
externalSkuName: string
|
||||
resolvedSkuName: string
|
||||
cloudSourceKey: string
|
||||
cloudSkuId: number
|
||||
cloudSkuName: string
|
||||
vnKey: string
|
||||
autoBuyEnabled: boolean
|
||||
minAssetReserve: number
|
||||
autoReturnNumberAfterDispatch: boolean
|
||||
autoConsumeAfterDispatch: boolean
|
||||
kuaishouConsumeShopId: string
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouCloudFulfillmentConfig {
|
||||
enabled: boolean
|
||||
items: AdminKuaishouCloudFulfillmentItem[]
|
||||
}
|
||||
|
||||
export interface AdminObservedProductItem {
|
||||
provider: string
|
||||
platform: string
|
||||
@@ -497,6 +527,9 @@ export interface AdminTaskOperations {
|
||||
canClose: boolean
|
||||
canMarkManualReview: boolean
|
||||
canCompleteManualDispatch: boolean
|
||||
canPrepareKuaishouCloudFulfillment: boolean
|
||||
canDispatchKuaishouCloudFulfillment: boolean
|
||||
canReturnKuaishouCloudFulfillment: boolean
|
||||
canSupportConfirmRole: boolean
|
||||
canSupportRedeem: boolean
|
||||
canViewSensitiveTaskData: boolean
|
||||
@@ -652,6 +685,53 @@ export interface AdminTaskDetail {
|
||||
resultMessage: string
|
||||
}>
|
||||
}
|
||||
kuaishouCloudFulfillment: null | {
|
||||
flowType: string
|
||||
configId: string
|
||||
internalSkuCode: string
|
||||
internalSkuName: string
|
||||
ticket: {
|
||||
code: string
|
||||
capturedAt: string | null
|
||||
}
|
||||
binding: {
|
||||
prepareStatus: string
|
||||
cloudSourceKey: string
|
||||
skuId: number
|
||||
skuName: string
|
||||
vnKey: string
|
||||
vnId: number
|
||||
vnPhone: string
|
||||
bindUrl: string
|
||||
bindPreparedAt: string | null
|
||||
}
|
||||
purchase: {
|
||||
autoBuyEnabled: boolean
|
||||
minAssetReserve: number
|
||||
usedKnapsack: boolean
|
||||
purchaseTriggered: boolean
|
||||
assetBefore: number
|
||||
assetAfter: number
|
||||
purchaseAt: string | null
|
||||
}
|
||||
dispatch: {
|
||||
status: string
|
||||
dispatchAt: string | null
|
||||
sendType: number
|
||||
note: string
|
||||
}
|
||||
returnNumber: {
|
||||
status: string
|
||||
returnedAt: string | null
|
||||
autoReturnEnabled: boolean
|
||||
}
|
||||
consume: {
|
||||
status: string
|
||||
shopId: string
|
||||
autoConsumeEnabled: boolean
|
||||
}
|
||||
notes: string
|
||||
}
|
||||
manualDispatch: null | {
|
||||
outcome: string
|
||||
deliveryReference: string
|
||||
|
||||
@@ -65,6 +65,7 @@ type ValidationState = {
|
||||
const PROFILE_OPTIONS = [
|
||||
{ label: '腾讯领取兑换', value: 'tencent_claim_redeem' },
|
||||
{ label: '腾讯领取兑换(半自动+人工)', value: 'tencent_claim_assisted' },
|
||||
{ label: '快手 Cloud 履约', value: 'kuaishou_ct_assisted' },
|
||||
{ label: '人工发货', value: 'manual_review' },
|
||||
]
|
||||
|
||||
@@ -102,6 +103,7 @@ const ruleMetrics = computed(() => {
|
||||
const completedCount = bindings.value.filter(isEditableBindingComplete).length
|
||||
const manualCount = bindings.value.filter((item) => item.profileKey.trim() === 'manual_review').length
|
||||
const assistedCount = bindings.value.filter((item) => item.profileKey.trim() === 'tencent_claim_assisted').length
|
||||
const kuaishouCloudCount = bindings.value.filter((item) => item.profileKey.trim() === 'kuaishou_ct_assisted').length
|
||||
|
||||
return {
|
||||
totalCount: bindings.value.length,
|
||||
@@ -109,6 +111,7 @@ const ruleMetrics = computed(() => {
|
||||
draftCount: Math.max(bindings.value.length - completedCount, 0),
|
||||
manualCount,
|
||||
assistedCount,
|
||||
kuaishouCloudCount,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -536,7 +539,7 @@ onMounted(loadConfigs)
|
||||
<header class="panel-header">
|
||||
<div class="panel-header-copy">
|
||||
<h1>履约配置</h1>
|
||||
<p>把外部商品统一映射到内部履约 SKU,并指定最终走自动、半自动还是人工处理。</p>
|
||||
<p>把外部商品统一映射到内部履约 SKU,并指定最终走腾讯领取、快手 Cloud 或人工处理。</p>
|
||||
</div>
|
||||
<div class="header-tags">
|
||||
<span class="header-tag">外部条件命中 1 个即可</span>
|
||||
@@ -603,6 +606,7 @@ onMounted(loadConfigs)
|
||||
<span class="mini-stat-chip">待完善 {{ ruleMetrics.draftCount }}</span>
|
||||
<span class="mini-stat-chip">人工 {{ ruleMetrics.manualCount }}</span>
|
||||
<span class="mini-stat-chip">半自动 {{ ruleMetrics.assistedCount }}</span>
|
||||
<span class="mini-stat-chip">快手 Cloud {{ ruleMetrics.kuaishouCloudCount }}</span>
|
||||
</div>
|
||||
<div class="section-action-group">
|
||||
<el-button round @click="addBinding">新增规则</el-button>
|
||||
|
||||
@@ -0,0 +1,802 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
fetchAdminKuaishouCloudFulfillmentConfig,
|
||||
saveAdminKuaishouCloudFulfillmentConfig,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminKuaishouCloudFulfillmentConfig,
|
||||
AdminKuaishouCloudFulfillmentItem,
|
||||
} from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
|
||||
type EditableItem = AdminKuaishouCloudFulfillmentItem & {
|
||||
localId: string
|
||||
}
|
||||
|
||||
type ValidationState = {
|
||||
localId: string
|
||||
message: string
|
||||
} | null
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const validationState = ref<ValidationState>(null)
|
||||
const filePath = ref('')
|
||||
const enabled = ref(true)
|
||||
const items = ref<EditableItem[]>([])
|
||||
const collapsedIds = ref<string[]>([])
|
||||
|
||||
const metrics = computed(() => {
|
||||
const readyCount = items.value.filter(isItemComplete).length
|
||||
const enabledCount = items.value.filter((item) => item.enabled).length
|
||||
return {
|
||||
total: items.value.length,
|
||||
readyCount,
|
||||
enabledCount,
|
||||
draftCount: Math.max(items.value.length - readyCount, 0),
|
||||
}
|
||||
})
|
||||
|
||||
function createEmptyItem(): EditableItem {
|
||||
return {
|
||||
localId: crypto.randomUUID(),
|
||||
id: '',
|
||||
enabled: true,
|
||||
priority: 100,
|
||||
provider: 'khhao',
|
||||
platform: 'kuaishou',
|
||||
shopId: '',
|
||||
internalSkuCode: '',
|
||||
internalSkuName: '',
|
||||
externalSkuCode: '',
|
||||
externalItemId: '',
|
||||
externalSkuName: '',
|
||||
resolvedSkuName: '',
|
||||
cloudSourceKey: 'default',
|
||||
cloudSkuId: 0,
|
||||
cloudSkuName: '',
|
||||
vnKey: '',
|
||||
autoBuyEnabled: true,
|
||||
minAssetReserve: 0,
|
||||
autoReturnNumberAfterDispatch: false,
|
||||
autoConsumeAfterDispatch: false,
|
||||
kuaishouConsumeShopId: '',
|
||||
notes: '',
|
||||
}
|
||||
}
|
||||
|
||||
function mapEditableItem(item: AdminKuaishouCloudFulfillmentItem): EditableItem {
|
||||
return {
|
||||
localId: crypto.randomUUID(),
|
||||
id: item.id || crypto.randomUUID(),
|
||||
enabled: item.enabled !== false,
|
||||
priority: item.priority || 100,
|
||||
provider: item.provider || 'khhao',
|
||||
platform: item.platform || 'kuaishou',
|
||||
shopId: item.shopId || '',
|
||||
internalSkuCode: item.internalSkuCode || '',
|
||||
internalSkuName: item.internalSkuName || '',
|
||||
externalSkuCode: item.externalSkuCode || '',
|
||||
externalItemId: item.externalItemId || '',
|
||||
externalSkuName: item.externalSkuName || '',
|
||||
resolvedSkuName: item.resolvedSkuName || '',
|
||||
cloudSourceKey: item.cloudSourceKey || 'default',
|
||||
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
|
||||
cloudSkuName: item.cloudSkuName || '',
|
||||
vnKey: item.vnKey || '',
|
||||
autoBuyEnabled: item.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(item.minAssetReserve || 0) || 0,
|
||||
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
|
||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
||||
kuaishouConsumeShopId: item.kuaishouConsumeShopId || '',
|
||||
notes: item.notes || '',
|
||||
}
|
||||
}
|
||||
|
||||
function mapSaveItem(item: EditableItem): AdminKuaishouCloudFulfillmentItem {
|
||||
return {
|
||||
id: item.id.trim() || item.internalSkuCode.trim() || item.localId,
|
||||
enabled: item.enabled,
|
||||
priority: Number(item.priority || 100) || 100,
|
||||
provider: item.provider.trim() || 'khhao',
|
||||
platform: item.platform.trim() || 'kuaishou',
|
||||
shopId: item.shopId.trim(),
|
||||
internalSkuCode: item.internalSkuCode.trim(),
|
||||
internalSkuName: item.internalSkuName.trim(),
|
||||
externalSkuCode: item.externalSkuCode.trim(),
|
||||
externalItemId: item.externalItemId.trim(),
|
||||
externalSkuName: item.externalSkuName.trim(),
|
||||
resolvedSkuName: item.resolvedSkuName.trim(),
|
||||
cloudSourceKey: item.cloudSourceKey.trim() || 'default',
|
||||
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
|
||||
cloudSkuName: item.cloudSkuName.trim(),
|
||||
vnKey: item.vnKey.trim(),
|
||||
autoBuyEnabled: item.autoBuyEnabled,
|
||||
minAssetReserve: Math.max(0, Number(item.minAssetReserve || 0) || 0),
|
||||
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch,
|
||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch,
|
||||
kuaishouConsumeShopId: item.kuaishouConsumeShopId.trim(),
|
||||
notes: item.notes.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function hasExternalMatch(item: Pick<EditableItem, 'externalSkuCode' | 'externalItemId' | 'externalSkuName'>) {
|
||||
return Boolean(item.externalSkuCode.trim() || item.externalItemId.trim() || item.externalSkuName.trim())
|
||||
}
|
||||
|
||||
function hasMeaningfulContent(item: EditableItem) {
|
||||
return Boolean(
|
||||
item.internalSkuCode.trim()
|
||||
|| item.internalSkuName.trim()
|
||||
|| item.externalSkuCode.trim()
|
||||
|| item.externalItemId.trim()
|
||||
|| item.externalSkuName.trim()
|
||||
|| item.resolvedSkuName.trim()
|
||||
|| item.cloudSourceKey.trim() !== 'default'
|
||||
|| Number(item.cloudSkuId || 0) > 0
|
||||
|| item.cloudSkuName.trim()
|
||||
|| item.vnKey.trim()
|
||||
|| item.shopId.trim()
|
||||
|| item.notes.trim()
|
||||
|| item.provider.trim() !== 'khhao'
|
||||
|| item.platform.trim() !== 'kuaishou'
|
||||
|| item.priority !== 100
|
||||
|| item.autoBuyEnabled !== true
|
||||
|| item.minAssetReserve !== 0
|
||||
|| item.autoReturnNumberAfterDispatch
|
||||
|| item.autoConsumeAfterDispatch
|
||||
|| item.enabled !== true,
|
||||
)
|
||||
}
|
||||
|
||||
function isItemComplete(item: EditableItem) {
|
||||
return Boolean(
|
||||
item.internalSkuCode.trim()
|
||||
&& Number(item.cloudSkuId || 0) > 0
|
||||
&& item.vnKey.trim()
|
||||
&& hasExternalMatch(item),
|
||||
)
|
||||
}
|
||||
|
||||
function resolveValidation(item: EditableItem, index: number): ValidationState {
|
||||
if (!hasMeaningfulContent(item)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const title = `第 ${index + 1} 条规则`
|
||||
|
||||
if (!item.internalSkuCode.trim()) {
|
||||
return { localId: item.localId, message: `${title} 缺少内部 SKU 编码` }
|
||||
}
|
||||
|
||||
if (Number(item.cloudSkuId || 0) <= 0) {
|
||||
return { localId: item.localId, message: `${title} 需要填写 cloud SKU ID` }
|
||||
}
|
||||
|
||||
if (!item.vnKey.trim()) {
|
||||
return { localId: item.localId, message: `${title} 需要填写虚拟号 VN Key` }
|
||||
}
|
||||
|
||||
if (!hasExternalMatch(item)) {
|
||||
return { localId: item.localId, message: `${title} 至少填写一种外部匹配条件` }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function getCardTitle(item: EditableItem, index: number) {
|
||||
return item.internalSkuName.trim() || item.externalSkuName.trim() || item.internalSkuCode.trim() || `规则 ${index + 1}`
|
||||
}
|
||||
|
||||
function getCardSummary(item: EditableItem) {
|
||||
const parts = [
|
||||
item.provider.trim() || 'khhao',
|
||||
item.platform.trim() || 'kuaishou',
|
||||
item.shopId.trim() || '跨店铺',
|
||||
item.cloudSkuId > 0 ? `cloud#${item.cloudSkuId}` : '待填 cloud SKU',
|
||||
]
|
||||
return parts.join(' / ')
|
||||
}
|
||||
|
||||
function isCollapsed(localId: string) {
|
||||
return collapsedIds.value.includes(localId)
|
||||
}
|
||||
|
||||
function setCollapsed(localId: string, collapsed: boolean) {
|
||||
const next = new Set(collapsedIds.value)
|
||||
if (collapsed) {
|
||||
next.add(localId)
|
||||
} else {
|
||||
next.delete(localId)
|
||||
}
|
||||
collapsedIds.value = Array.from(next)
|
||||
}
|
||||
|
||||
function toggleCollapsed(localId: string) {
|
||||
setCollapsed(localId, !isCollapsed(localId))
|
||||
}
|
||||
|
||||
function rebuildCollapsedState() {
|
||||
collapsedIds.value = items.value.filter(isItemComplete).map((item) => item.localId)
|
||||
}
|
||||
|
||||
async function loadConfigs() {
|
||||
if (!hasAdminRole('admin')) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminKuaishouCloudFulfillmentConfig()
|
||||
filePath.value = response.data.filePath
|
||||
enabled.value = response.data.source.enabled !== false
|
||||
items.value = (response.data.source.items || []).map(mapEditableItem)
|
||||
rebuildCollapsedState()
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取新履约配置失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
validationState.value = null
|
||||
const next = createEmptyItem()
|
||||
items.value.unshift(next)
|
||||
setCollapsed(next.localId, false)
|
||||
}
|
||||
|
||||
function removeItem(localId: string) {
|
||||
if (validationState.value?.localId === localId) {
|
||||
validationState.value = null
|
||||
}
|
||||
items.value = items.value.filter((item) => item.localId !== localId)
|
||||
setCollapsed(localId, false)
|
||||
}
|
||||
|
||||
async function saveConfigs() {
|
||||
const invalid = items.value.reduce<ValidationState>((state, item, index) => state || resolveValidation(item, index), null)
|
||||
if (invalid) {
|
||||
validationState.value = invalid
|
||||
setCollapsed(invalid.localId, false)
|
||||
return
|
||||
}
|
||||
|
||||
const payloadItems = items.value.filter(hasMeaningfulContent).map(mapSaveItem)
|
||||
const payload: AdminKuaishouCloudFulfillmentConfig = {
|
||||
enabled: enabled.value,
|
||||
items: payloadItems,
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
validationState.value = null
|
||||
|
||||
try {
|
||||
const response = await saveAdminKuaishouCloudFulfillmentConfig(payload)
|
||||
filePath.value = response.data.filePath
|
||||
enabled.value = response.data.source.enabled !== false
|
||||
items.value = (response.data.source.items || []).map(mapEditableItem)
|
||||
rebuildCollapsedState()
|
||||
showSuccess('新履约配置已保存')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '保存新履约配置失败'
|
||||
validationState.value = { localId: '', message }
|
||||
showError(message)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadConfigs)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-panel">
|
||||
<header class="panel-header hero-card">
|
||||
<div class="hero-copy">
|
||||
<h1>新履约配置</h1>
|
||||
<p>独立维护快手下单后经 cloudtentacles 完成绑定、发货与退号的新链路,不影响旧履约规则。</p>
|
||||
</div>
|
||||
<div class="hero-chips">
|
||||
<span>独立新流程</span>
|
||||
<span>保存后同步到任务匹配</span>
|
||||
<span>支持折叠维护</span>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<el-switch v-model="enabled" active-text="启用整条配置" />
|
||||
<el-button round @click="loadConfigs">刷新</el-button>
|
||||
<el-button round type="primary" :loading="saving" @click="saveConfigs">保存配置</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以维护新履约配置。</div>
|
||||
|
||||
<template v-else>
|
||||
<section class="overview-card">
|
||||
<div class="overview-file">
|
||||
<span>配置文件</span>
|
||||
<code>{{ filePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="overview-stats">
|
||||
<article>
|
||||
<span>规则总数</span>
|
||||
<strong>{{ metrics.total }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>已完成</span>
|
||||
<strong>{{ metrics.readyCount }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>草稿中</span>
|
||||
<strong>{{ metrics.draftCount }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>已启用</span>
|
||||
<strong>{{ metrics.enabledCount }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
<div class="overview-notes">
|
||||
<span>外部商品命中后会映射到 `kuaishou_ct_assisted`</span>
|
||||
<span>客服只保留“确认绑定完成并发货”一个动作</span>
|
||||
<span>退还号码作为最终收口动作</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
|
||||
<p v-if="validationState?.message" class="validation-copy">{{ validationState.message }}</p>
|
||||
<div v-if="loading" class="empty-block">新履约配置加载中</div>
|
||||
|
||||
<template v-else>
|
||||
<section class="toolbar-card">
|
||||
<div>
|
||||
<h3>规则列表</h3>
|
||||
<p>每一条规则描述“快手外部商品”如何映射到“内部 SKU + cloud 资源”。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button round @click="addItem">新增规则</el-button>
|
||||
<el-button round type="primary" :loading="saving" @click="saveConfigs">保存全部</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="items.length === 0" class="empty-block">当前还没有新履约规则,先新增一条。</div>
|
||||
|
||||
<article
|
||||
v-for="(item, index) in items"
|
||||
:key="item.localId"
|
||||
class="rule-card"
|
||||
:data-invalid="validationState?.localId === item.localId"
|
||||
>
|
||||
<button class="rule-head" type="button" @click="toggleCollapsed(item.localId)">
|
||||
<div>
|
||||
<strong>{{ getCardTitle(item, index) }}</strong>
|
||||
<p>{{ getCardSummary(item) }}</p>
|
||||
</div>
|
||||
<div class="rule-head-meta">
|
||||
<span class="status-pill" :data-tone="isItemComplete(item) ? 'success' : 'warning'">
|
||||
{{ isItemComplete(item) ? '可用' : '待完善' }}
|
||||
</span>
|
||||
<span class="collapse-indicator">{{ isCollapsed(item.localId) ? '展开' : '收起' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="!isCollapsed(item.localId)" class="rule-body">
|
||||
<div class="grid cols-4">
|
||||
<label class="field-block">
|
||||
<span>内部 SKU</span>
|
||||
<el-input v-model="item.internalSkuCode" maxlength="80" placeholder="必填,用于生成任务" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>内部商品名</span>
|
||||
<el-input v-model="item.internalSkuName" maxlength="120" placeholder="展示用,可选但建议填" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>优先级</span>
|
||||
<el-input-number v-model="item.priority" :min="1" :step="1" controls-position="right" />
|
||||
</label>
|
||||
<label class="field-block field-block--switch">
|
||||
<span>规则开关</span>
|
||||
<el-switch v-model="item.enabled" active-text="启用" inactive-text="停用" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-4">
|
||||
<label class="field-block">
|
||||
<span>来源 provider</span>
|
||||
<el-input v-model="item.provider" maxlength="32" placeholder="默认 khhao" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>来源 platform</span>
|
||||
<el-input v-model="item.platform" maxlength="32" placeholder="默认 kuaishou" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>店铺 ID</span>
|
||||
<el-input v-model="item.shopId" maxlength="80" placeholder="留空表示跨店铺共用" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>解析商品名</span>
|
||||
<el-input v-model="item.resolvedSkuName" maxlength="120" placeholder="可作为商品名兜底" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="section-split">
|
||||
<div>
|
||||
<h4>外部匹配条件</h4>
|
||||
<p>外部 SKU、商品 ID、商品名三选一即可命中,建议尽量多填提高稳定性。</p>
|
||||
</div>
|
||||
<div class="grid cols-3">
|
||||
<label class="field-block">
|
||||
<span>外部 SKU</span>
|
||||
<el-input v-model="item.externalSkuCode" maxlength="120" placeholder="例如 khhao 的 skuCode" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>外部商品 ID</span>
|
||||
<el-input v-model="item.externalItemId" maxlength="120" placeholder="平台商品 ID" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>外部商品名</span>
|
||||
<el-input v-model="item.externalSkuName" maxlength="200" placeholder="用于名称匹配" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-split">
|
||||
<div>
|
||||
<h4>cloud 资源</h4>
|
||||
<p>这里决定是否走背包、自动购买、申请虚拟号,以及后续发货动作会使用哪一个 cloud SKU。</p>
|
||||
</div>
|
||||
<div class="grid cols-4">
|
||||
<label class="field-block">
|
||||
<span>cloudSourceKey</span>
|
||||
<el-input v-model="item.cloudSourceKey" maxlength="60" placeholder="默认 default" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>cloud SKU ID</span>
|
||||
<el-input-number v-model="item.cloudSkuId" :min="0" :step="1" controls-position="right" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>cloud SKU 名称</span>
|
||||
<el-input v-model="item.cloudSkuName" maxlength="120" placeholder="可选,方便辨认" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>虚拟号 VN Key</span>
|
||||
<el-input v-model="item.vnKey" maxlength="40" placeholder="必填,例如 1" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-4">
|
||||
<label class="field-block field-block--switch">
|
||||
<span>自动购买</span>
|
||||
<el-switch v-model="item.autoBuyEnabled" active-text="开启" inactive-text="关闭" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>最低保留余额</span>
|
||||
<el-input-number v-model="item.minAssetReserve" :min="0" :step="1" controls-position="right" />
|
||||
</label>
|
||||
<label class="field-block field-block--switch">
|
||||
<span>发货后自动退号</span>
|
||||
<el-switch v-model="item.autoReturnNumberAfterDispatch" active-text="开启" inactive-text="关闭" />
|
||||
</label>
|
||||
<label class="field-block field-block--switch">
|
||||
<span>发货后自动核销</span>
|
||||
<el-switch v-model="item.autoConsumeAfterDispatch" active-text="开启" inactive-text="关闭" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-2">
|
||||
<label class="field-block">
|
||||
<span>快手核销店铺 ID</span>
|
||||
<el-input v-model="item.kuaishouConsumeShopId" maxlength="80" placeholder="预留给后续自动核销" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>备注</span>
|
||||
<el-input
|
||||
v-model="item.notes"
|
||||
maxlength="400"
|
||||
placeholder="补充说明,例如绑定场景、客服注意事项"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="rule-footer">
|
||||
<span>配置 ID:{{ item.id || '保存后自动生成' }}</span>
|
||||
<el-button round text type="danger" @click="removeItem(item.localId)">删除规则</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-panel {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.overview-card,
|
||||
.toolbar-card,
|
||||
.rule-card,
|
||||
.empty-block,
|
||||
.error-copy,
|
||||
.validation-copy {
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(86, 108, 138, 0.12);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(26, 127, 255, 0.12), transparent 24%),
|
||||
radial-gradient(circle at bottom left, rgba(16, 185, 129, 0.12), transparent 28%),
|
||||
rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.hero-copy h1,
|
||||
.toolbar-card h3,
|
||||
.section-split h4 {
|
||||
margin: 0;
|
||||
color: #17324d;
|
||||
}
|
||||
|
||||
.hero-copy p,
|
||||
.toolbar-card p,
|
||||
.section-split p {
|
||||
margin: 8px 0 0;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero-chips,
|
||||
.hero-actions,
|
||||
.overview-notes,
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.hero-chips {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.hero-chips span,
|
||||
.overview-notes span {
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(17, 72, 153, 0.08);
|
||||
color: #175cd3;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
margin-top: 18px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.overview-card,
|
||||
.toolbar-card,
|
||||
.empty-block,
|
||||
.error-copy,
|
||||
.validation-copy {
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.overview-file {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.overview-file span {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.overview-file code {
|
||||
color: #17324d;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.overview-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.overview-stats article {
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
background: #f8fbff;
|
||||
border: 1px solid rgba(86, 108, 138, 0.08);
|
||||
}
|
||||
|
||||
.overview-stats span {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.overview-stats strong {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #17324d;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.error-copy,
|
||||
.validation-copy {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.toolbar-card {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rule-card[data-invalid='true'] {
|
||||
border-color: rgba(180, 35, 24, 0.25);
|
||||
box-shadow: 0 18px 48px rgba(180, 35, 24, 0.08);
|
||||
}
|
||||
|
||||
.rule-head {
|
||||
width: 100%;
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border: 0;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.98), rgba(243, 247, 252, 0.98));
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rule-head strong {
|
||||
display: block;
|
||||
color: #17324d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rule-head p {
|
||||
margin: 6px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rule-head-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-pill[data-tone='success'] {
|
||||
color: #0f766e;
|
||||
background: #ecfdf3;
|
||||
}
|
||||
|
||||
.status-pill[data-tone='warning'] {
|
||||
color: #b54708;
|
||||
background: #fffaeb;
|
||||
}
|
||||
|
||||
.collapse-indicator {
|
||||
color: #175cd3;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rule-body {
|
||||
padding: 0 20px 20px;
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.section-split {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: rgba(248, 250, 252, 0.9);
|
||||
border: 1px solid rgba(86, 108, 138, 0.08);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.field-block {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-block span {
|
||||
color: #17324d;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-block--switch {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.rule-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.cols-4,
|
||||
.cols-3,
|
||||
.cols-2 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.hero-actions,
|
||||
.toolbar-card,
|
||||
.rule-head,
|
||||
.rule-footer {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.cols-4,
|
||||
.cols-3,
|
||||
.cols-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -40,6 +40,7 @@ const navItems = computed(() => {
|
||||
baseItems.splice(1, 0, { to: '/admin/users', label: '用户' })
|
||||
baseItems.push({ to: '/admin/platform-shops', label: '平台配置' })
|
||||
baseItems.push({ to: '/admin/platform-fulfillment', label: '履约配置' })
|
||||
baseItems.push({ to: '/admin/platform-kuaishou-cloud-fulfillment', label: '新履约配置' })
|
||||
baseItems.push({ to: '/admin/audit-logs', label: '审计' })
|
||||
}
|
||||
|
||||
@@ -133,6 +134,7 @@ async function submitLogout() {
|
||||
margin-top: 28px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.admin-meta {
|
||||
|
||||
@@ -8,12 +8,15 @@ import {
|
||||
closeAdminTask,
|
||||
confirmAdminTaskAssistedRole,
|
||||
completeAdminTaskManualDispatch,
|
||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||
redeemAdminTaskAssisted,
|
||||
fetchAdminTaskScreenshot,
|
||||
fetchAdminTaskDetail,
|
||||
markAdminTaskManualReview,
|
||||
prepareAdminTaskKuaishouCloudFulfillment,
|
||||
regenerateAdminTaskClaimLink,
|
||||
releaseAdminTaskInventoryBinding,
|
||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||
retryAdminTask,
|
||||
} from '@/services/admin'
|
||||
import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
|
||||
@@ -33,9 +36,14 @@ const manualDispatchForm = reactive({
|
||||
deliveredCredential: '',
|
||||
resultMessage: '',
|
||||
})
|
||||
const kuaishouCloudForm = reactive({
|
||||
ticketCode: '',
|
||||
})
|
||||
const canManageTaskLifecycle = computed(() => hasAdminRole('operator'))
|
||||
const canCloseTasks = computed(() => hasAdminRole('support'))
|
||||
const canViewSensitiveTaskData = computed(() => Boolean(detail.value?.operations.canViewSensitiveTaskData))
|
||||
const kuaishouCloudFlow = computed(() => detail.value?.kuaishouCloudFulfillment || null)
|
||||
const isKuaishouCloudTask = computed(() => detail.value?.task.executorKey === 'kuaishou_ct_assisted')
|
||||
const claimUrl = computed(() => {
|
||||
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
|
||||
const taskStatus = String(detail.value?.task.status || '').trim()
|
||||
@@ -66,6 +74,7 @@ async function loadDetail() {
|
||||
const response = await fetchAdminTaskDetail(String(route.params.taskId || ''))
|
||||
detail.value = response.data
|
||||
syncManualDispatchForm(response.data)
|
||||
syncKuaishouCloudForm(response.data)
|
||||
await loadScreenshotPreview(response.data)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取任务详情失败'
|
||||
@@ -133,6 +142,10 @@ function syncManualDispatchForm(taskDetail: AdminTaskDetail) {
|
||||
manualDispatchForm.resultMessage = taskDetail.manualDispatch?.resultMessage || taskDetail.task.resultMessage || ''
|
||||
}
|
||||
|
||||
function syncKuaishouCloudForm(taskDetail: AdminTaskDetail) {
|
||||
kuaishouCloudForm.ticketCode = taskDetail.kuaishouCloudFulfillment?.ticket.code || ''
|
||||
}
|
||||
|
||||
async function submitManualDispatch(outcome: 'delivered' | 'failed') {
|
||||
if (!detail.value) {
|
||||
return
|
||||
@@ -155,6 +168,68 @@ async function submitManualDispatch(outcome: 'delivered' | 'failed') {
|
||||
)
|
||||
}
|
||||
|
||||
async function submitKuaishouCloudPrepare() {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await runAction(
|
||||
() => prepareAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId),
|
||||
'绑定资源已准备完成',
|
||||
`确认开始为任务 ${detail.value.task.taskNo} 准备绑定资源吗?系统会自动检查背包、余额并申请虚拟号。`,
|
||||
)
|
||||
}
|
||||
|
||||
async function submitKuaishouCloudDispatch() {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await runAction(
|
||||
() => dispatchAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId, {
|
||||
ticketCode: kuaishouCloudForm.ticketCode,
|
||||
}),
|
||||
'已完成绑定确认并发货',
|
||||
`确认客户已经完成绑定,并立即为任务 ${detail.value.task.taskNo} 执行发货吗?这个动作会把“确认绑定完成”和“发货”合并为一步。`,
|
||||
)
|
||||
}
|
||||
|
||||
async function submitKuaishouCloudReturnNumber() {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await runAction(
|
||||
() => returnNumberAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId),
|
||||
'号码已退还',
|
||||
`确认退还任务 ${detail.value.task.taskNo} 当前使用的虚拟号吗?退号后该流程会正式收口。`,
|
||||
)
|
||||
}
|
||||
|
||||
async function copyText(value: string, successMessage: string) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
showError('当前没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
showSuccess(successMessage)
|
||||
} catch {
|
||||
showError('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
function openExternalLink(value: string) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
window.open(text, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
function formatTaskEventPayload(payload: Record<string, unknown>) {
|
||||
const parts = [
|
||||
payload.outcome ? `结果 ${payload.outcome}` : '',
|
||||
@@ -166,11 +241,19 @@ function formatTaskEventPayload(payload: Record<string, unknown>) {
|
||||
payload.inventoryItemId ? `库存项 #${payload.inventoryItemId}` : '',
|
||||
payload.previousInventoryItemId ? `旧库存 #${payload.previousInventoryItemId}` : '',
|
||||
payload.nextInventoryItemId ? `新库存 #${payload.nextInventoryItemId}` : '',
|
||||
payload.ticketCodeMasked ? `卡券号 ${payload.ticketCodeMasked}` : '',
|
||||
payload.vnId ? `虚拟号 #${payload.vnId}` : '',
|
||||
payload.vnPhoneMasked ? `号码 ${payload.vnPhoneMasked}` : '',
|
||||
payload.bindUrl ? `绑定链接 ${payload.bindUrl}` : '',
|
||||
payload.sendType ? `发送类型 ${payload.sendType}` : '',
|
||||
payload.note ? `备注 ${payload.note}` : '',
|
||||
payload.previousOutcome ? `切换原因 ${formatRedeemOutcomeLabel(String(payload.previousOutcome || ''))}` : '',
|
||||
payload.deliveryReference ? `单号 ${payload.deliveryReference}` : '',
|
||||
payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : '',
|
||||
payload.trigger ? `触发 ${formatAutoDeliveryTrigger(String(payload.trigger || ''))}` : '',
|
||||
payload.reason ? `原因 ${payload.reason}` : '',
|
||||
payload.purchaseTriggered === true ? '已触发购买' : '',
|
||||
payload.usedKnapsack === true ? '使用背包库存' : '',
|
||||
payload.responseStatus ? `HTTP ${payload.responseStatus}` : '',
|
||||
payload.requestId ? `请求 ${payload.requestId}` : '',
|
||||
payload.errorMessage ? `错误 ${payload.errorMessage}` : '',
|
||||
@@ -349,6 +432,33 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
>
|
||||
客服开始兑换
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canPrepareKuaishouCloudFulfillment"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="primary"
|
||||
@click="submitKuaishouCloudPrepare"
|
||||
>
|
||||
准备绑定资源
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canDispatchKuaishouCloudFulfillment"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="success"
|
||||
@click="submitKuaishouCloudDispatch"
|
||||
>
|
||||
确认绑定完成并发货
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canReturnKuaishouCloudFulfillment"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="warning"
|
||||
@click="submitKuaishouCloudReturnNumber"
|
||||
>
|
||||
退还号码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle"
|
||||
:disabled="!detail.operations.canMarkManualReview"
|
||||
@@ -423,6 +533,122 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section v-if="isKuaishouCloudTask && kuaishouCloudFlow" class="table-card flow-card">
|
||||
<div class="flow-card-head">
|
||||
<div>
|
||||
<h3>快手 Cloud 履约面板</h3>
|
||||
<p class="section-copy">这里集中处理绑定链接、客户卡券号、发货动作与退号收口。</p>
|
||||
</div>
|
||||
<div class="flow-head-actions">
|
||||
<el-button
|
||||
v-if="kuaishouCloudFlow.binding.bindUrl"
|
||||
round
|
||||
@click="copyText(kuaishouCloudFlow.binding.bindUrl, '绑定链接已复制')"
|
||||
>
|
||||
复制绑定链接
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="kuaishouCloudFlow.binding.bindUrl"
|
||||
round
|
||||
type="primary"
|
||||
plain
|
||||
@click="openExternalLink(kuaishouCloudFlow.binding.bindUrl)"
|
||||
>
|
||||
打开绑定页
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="redeem-summary-grid">
|
||||
<article class="redeem-summary-card">
|
||||
<span>准备状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="kuaishouCloudFlow.binding.prepareStatus" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>发货状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="kuaishouCloudFlow.dispatch.status" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>退号状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="kuaishouCloudFlow.returnNumber.status" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>当前任务状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="detail.task.status" />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="flow-grid">
|
||||
<article class="flow-block">
|
||||
<h4>客户绑定</h4>
|
||||
<p>客户下单后需要先完成外部绑定,客服只需要把链接发给客户,并在完成后点一次发货按钮。</p>
|
||||
<div class="flow-meta-list">
|
||||
<div><span>绑定链接</span><strong>{{ kuaishouCloudFlow.binding.bindUrl || '-' }}</strong></div>
|
||||
<div><span>准备时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.binding.bindPreparedAt) }}</strong></div>
|
||||
<div><span>虚拟号 ID</span><strong>{{ kuaishouCloudFlow.binding.vnId || '-' }}</strong></div>
|
||||
<div><span>虚拟号码</span><strong>{{ kuaishouCloudFlow.binding.vnPhone || '-' }}</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="flow-block">
|
||||
<h4>cloud 资源</h4>
|
||||
<div class="flow-meta-list">
|
||||
<div><span>cloudSourceKey</span><strong>{{ kuaishouCloudFlow.binding.cloudSourceKey || '-' }}</strong></div>
|
||||
<div><span>cloud SKU</span><strong>{{ kuaishouCloudFlow.binding.skuId ? `${kuaishouCloudFlow.binding.skuId} / ${kuaishouCloudFlow.binding.skuName || '-'}` : '-' }}</strong></div>
|
||||
<div><span>VN Key</span><strong>{{ kuaishouCloudFlow.binding.vnKey || '-' }}</strong></div>
|
||||
<div><span>自动购买</span><strong>{{ kuaishouCloudFlow.purchase.autoBuyEnabled ? '开启' : '关闭' }}</strong></div>
|
||||
<div><span>背包命中</span><strong>{{ kuaishouCloudFlow.purchase.usedKnapsack ? '是' : '否' }}</strong></div>
|
||||
<div><span>触发购买</span><strong>{{ kuaishouCloudFlow.purchase.purchaseTriggered ? '是' : '否' }}</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="flow-form-card">
|
||||
<label class="field-block">
|
||||
<span>客户提供的卡券号</span>
|
||||
<el-input
|
||||
v-model="kuaishouCloudForm.ticketCode"
|
||||
:disabled="!canManageTaskLifecycle"
|
||||
maxlength="120"
|
||||
placeholder="客户下单后提供的卡券号,可在发货时一并记录"
|
||||
/>
|
||||
</label>
|
||||
<p class="manual-hint">“确认绑定完成并发货”已经合并为一个按钮,不再拆成两步。</p>
|
||||
</div>
|
||||
|
||||
<div class="flow-grid">
|
||||
<article class="flow-block">
|
||||
<h4>发货结果</h4>
|
||||
<div class="flow-meta-list">
|
||||
<div><span>最近卡券号</span><strong>{{ kuaishouCloudFlow.ticket.code || '-' }}</strong></div>
|
||||
<div><span>记录时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.capturedAt) }}</strong></div>
|
||||
<div><span>发货时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.dispatch.dispatchAt) }}</strong></div>
|
||||
<div><span>发送类型</span><strong>{{ kuaishouCloudFlow.dispatch.sendType || '-' }}</strong></div>
|
||||
<div><span>结果说明</span><strong>{{ kuaishouCloudFlow.dispatch.note || detail.task.resultMessage || '-' }}</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="flow-block">
|
||||
<h4>流程收口</h4>
|
||||
<div class="flow-meta-list">
|
||||
<div><span>退号时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.returnNumber.returnedAt) }}</strong></div>
|
||||
<div><span>自动退号</span><strong>{{ kuaishouCloudFlow.returnNumber.autoReturnEnabled ? '开启' : '关闭' }}</strong></div>
|
||||
<div><span>自动核销</span><strong>{{ kuaishouCloudFlow.consume.autoConsumeEnabled ? '开启' : '关闭' }}</strong></div>
|
||||
<div><span>核销店铺</span><strong>{{ kuaishouCloudFlow.consume.shopId || '-' }}</strong></div>
|
||||
<div><span>备注</span><strong>{{ kuaishouCloudFlow.notes || '-' }}</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="detail.order?.provider === 'agiso' && detail.order?.platform === 'xianyu'"
|
||||
class="table-card"
|
||||
@@ -740,6 +966,63 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
.manual-summary { margin-top: 16px; padding: 14px; border-radius: 16px; background: rgba(248,250,252,.9); }
|
||||
.manual-summary p { margin: 0 0 8px; color: #334155; }
|
||||
.manual-summary p:last-child { margin-bottom: 0; }
|
||||
.flow-card {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(37, 99, 235, 0.08), transparent 28%),
|
||||
linear-gradient(180deg, rgba(255,255,255,.96), rgba(248,250,252,.96));
|
||||
}
|
||||
.flow-card-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
.flow-head-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.flow-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.flow-block,
|
||||
.flow-form-card {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(86,108,138,.1);
|
||||
background: rgba(248,250,252,.9);
|
||||
}
|
||||
.flow-block h4 {
|
||||
margin: 0;
|
||||
color: #1d3555;
|
||||
}
|
||||
.flow-block p {
|
||||
margin: 10px 0 0;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.flow-meta-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.flow-meta-list div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
.flow-meta-list span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
.flow-meta-list strong {
|
||||
color: #1d3555;
|
||||
word-break: break-all;
|
||||
}
|
||||
.event-copy { color: #334155; }
|
||||
.event-time { margin-top: 6px; color: #64748b; font-size: 12px; }
|
||||
.screenshot-link { display: inline-block; margin-bottom: 12px; color: #175cd3; text-decoration: none; }
|
||||
@@ -751,4 +1034,11 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
border: 1px solid rgba(86,108,138,.12);
|
||||
box-shadow: 0 20px 48px rgba(15,23,42,.08);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.flow-card-head,
|
||||
.flow-head-actions {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user