完整流程-调试

This commit is contained in:
yml
2026-05-03 18:44:44 +08:00
parent 063c3fd8a1
commit 90cb7aa154
25 changed files with 2337 additions and 27 deletions
+5 -5
View File
@@ -1,11 +1,11 @@
{ {
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z", "syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
"lastRunStartedAt": "2026-05-03T09:15:36.131Z", "lastRunStartedAt": "2026-05-03T10:44:18.857Z",
"lastRunFinishedAt": "2026-05-03T09:15:37.160Z", "lastRunFinishedAt": "2026-05-03T10:44:19.569Z",
"lastRunStatus": "success", "lastRunStatus": "success",
"lastErrorMessage": "", "lastErrorMessage": "",
"fetchedCount": 40, "fetchedCount": 40,
"syncedCount": 0, "syncedCount": 1,
"ignoredCount": 40, "ignoredCount": 39,
"lastOrderCreatedAt": "2026-05-03T09:15:32.000Z" "lastOrderCreatedAt": "2026-05-03T10:44:07.000Z"
} }
@@ -0,0 +1,4 @@
{
"enabled": true,
"items": []
}
@@ -7,7 +7,7 @@
"kshopName": "洋洋运营", "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", "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", "userAvatar": "https://p2-pro.a.yximgs.com/uhead/AB/2024/07/23/17/BMjAyNDA3MjMxNzA1MThfNDI2OTI3Njc2Ml8yX2hkNDg1XzgxNQ==_s.jpg",
"enabled": true "enabled": false
}, },
{ {
"shopId": "4269276762", "shopId": "4269276762",
@@ -3,9 +3,9 @@
"provider": "khhao", "provider": "khhao",
"platform": "kuaishou", "platform": "kuaishou",
"shopId": "10", "shopId": "10",
"skuCode": "测试快手1", "skuCode": "套装-浪漫天命",
"skuName": "测试1", "skuName": "套装-浪漫天命",
"profileKey": "manual_review", "profileKey": "kuaishou_ct_assisted",
"enabled": true, "enabled": true,
"priority": 100, "priority": 100,
"config": {}, "config": {},
@@ -14,7 +14,7 @@
"externalItemId": "830", "externalItemId": "830",
"externalSkuName": "测试1", "externalSkuName": "测试1",
"config": { "config": {
"resolvedSkuName": "测试1" "resolvedSkuName": "套装-浪漫天命"
} }
} }
}, },
@@ -11,6 +11,7 @@ import {
getAdminCloudtentaclesAsset, getAdminCloudtentaclesAsset,
getAdminCloudtentaclesBindUrl, getAdminCloudtentaclesBindUrl,
getAdminCloudtentaclesCategories, getAdminCloudtentaclesCategories,
getAdminKuaishouCloudFulfillmentConfig,
getAdminKuaishouEticketSourceConfig, getAdminKuaishouEticketSourceConfig,
getAdminCloudtentaclesKnapsack, getAdminCloudtentaclesKnapsack,
getAdminCloudtentaclesSourceConfig, getAdminCloudtentaclesSourceConfig,
@@ -30,6 +31,7 @@ import {
syncAdminKhhaoOrders, syncAdminKhhaoOrders,
testAdminCloudtentaclesLogin, testAdminCloudtentaclesLogin,
testAdminKhhaoLogin, testAdminKhhaoLogin,
updateAdminKuaishouCloudFulfillmentConfig,
updateAdminKuaishouEticketSourceConfig, updateAdminKuaishouEticketSourceConfig,
updateAdminCloudtentaclesSourceConfig, updateAdminCloudtentaclesSourceConfig,
updateAdminAgisoShopConfigs, 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').AdminKuaishouEticketConsumeRouteBody} AdminKuaishouEticketConsumeRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketShopInfoRouteBody} AdminKuaishouEticketShopInfoRouteBody */ /** @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').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').AdminCloudtentaclesSendSmsCodeRouteBody} AdminCloudtentaclesSendSmsCodeRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesTestLoginRouteBody} AdminCloudtentaclesTestLoginRouteBody */ /** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesTestLoginRouteBody} AdminCloudtentaclesTestLoginRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesValidateSessionRouteBody} AdminCloudtentaclesValidateSessionRouteBody */ /** @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 export default router
+83
View File
@@ -6,11 +6,14 @@ import {
closeAdminTask, closeAdminTask,
completeAdminTaskManualDispatch, completeAdminTaskManualDispatch,
confirmAdminTaskAssistedRole, confirmAdminTaskAssistedRole,
dispatchAdminTaskKuaishouCloudFulfillment,
markAdminTaskManualReview, markAdminTaskManualReview,
prepareAdminTaskKuaishouCloudFulfillment,
regenerateAdminTaskClaimLink, regenerateAdminTaskClaimLink,
redeemAdminTaskAssisted, redeemAdminTaskAssisted,
releaseAdminTaskInventoryBinding, releaseAdminTaskInventoryBinding,
releaseAdminTaskInventory, releaseAdminTaskInventory,
returnNumberAdminTaskKuaishouCloudFulfillment,
retryAdminTask, retryAdminTask,
} from '../../services/admin/admin-write-service.js' } from '../../services/admin/admin-write-service.js'
import { import {
@@ -21,6 +24,7 @@ import {
import { createFileHandler, createJsonHandler, requireAdminRoles } from './shared.js' import { createFileHandler, createJsonHandler, requireAdminRoles } from './shared.js'
/** @typedef {import('../../types/admin-route-inputs.js').AdminRouteAdminSession} AdminRouteAdminSession */ /** @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').AdminTaskManualDispatchRouteBody} AdminTaskManualDispatchRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminTaskRouteParams} AdminTaskRouteParams */ /** @typedef {import('../../types/admin-route-inputs.js').AdminTaskRouteParams} AdminTaskRouteParams */
/** @typedef {import('../../types/admin-route-inputs.js').AdminTaskRouteQuery} AdminTaskRouteQuery */ /** @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 export default router
@@ -68,6 +68,11 @@ import {
getOrderFulfillmentBindingsFilePath, getOrderFulfillmentBindingsFilePath,
saveOrderFulfillmentBindingConfigs, saveOrderFulfillmentBindingConfigs,
} from '../order/fulfillment-binding-config-service.js' } 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 { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
import { formatFenToAmount } from '../../utils/money.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').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginInput */ /** @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').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').AdminCloudtentaclesSendSmsCodeInput} AdminCloudtentaclesSendSmsCodeInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesTestLoginInput} AdminCloudtentaclesTestLoginInput */ /** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesTestLoginInput} AdminCloudtentaclesTestLoginInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesValidateSessionInput} AdminCloudtentaclesValidateSessionInput */ /** @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] */ /** @param {AdminFulfillmentBindingLookupInput} [payload] */
export async function lookupAdminFulfillmentBindingOrder(payload = /** @type {AdminFulfillmentBindingLookupInput} */ ({})) { export async function lookupAdminFulfillmentBindingOrder(payload = /** @type {AdminFulfillmentBindingLookupInput} */ ({})) {
const provider = String(payload.provider || 'agiso').trim() || 'agiso' const provider = String(payload.provider || 'agiso').trim() || 'agiso'
@@ -27,6 +27,7 @@ import {
getTaskPrimaryInventoryItemId, getTaskPrimaryInventoryItemId,
isAssistedClaimTask, isAssistedClaimTask,
isManualDispatchTask, isManualDispatchTask,
mapKuaishouCloudFulfillmentContext,
mapManualDispatchContext, mapManualDispatchContext,
mapRedeemResolutionContext, mapRedeemResolutionContext,
parseTaskContext, parseTaskContext,
@@ -278,6 +279,7 @@ export async function getAdminTaskDetail(taskId, session = null) {
roleName: String(taskState.reviewRoleName || '').trim() || '', roleName: String(taskState.reviewRoleName || '').trim() || '',
}, },
redeemResolution: mapRedeemResolutionContext(taskContext.redeemResolution), redeemResolution: mapRedeemResolutionContext(taskContext.redeemResolution),
kuaishouCloudFulfillment: mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment),
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext), manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
events: taskEvents.map(mapAdminTaskEvent), events: taskEvents.map(mapAdminTaskEvent),
operations: { operations: {
@@ -289,6 +291,15 @@ export async function getAdminTaskDetail(taskId, session = null) {
canClose: canViewerCloseTask(task, viewerContext), canClose: canViewerCloseTask(task, viewerContext),
canMarkManualReview: viewerContext.canManageTaskLifecycle && !['redeemed', 'closed', 'manual_review'].includes(task.task_status), canMarkManualReview: viewerContext.canManageTaskLifecycle && !['redeemed', 'closed', 'manual_review'].includes(task.task_status),
canCompleteManualDispatch: viewerContext.canManageTaskLifecycle && isManualDispatchTask(task) && !['redeemed', 'closed'].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), canSupportConfirmRole: canViewerConfirmAssistedRole(task, viewerContext),
canSupportRedeem: canViewerRedeemAssistedTask(task, viewerContext), canSupportRedeem: canViewerRedeemAssistedTask(task, viewerContext),
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData, 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) { export function mapRedeemResolutionContext(value) {
if (!value || typeof value !== 'object') { if (!value || typeof value !== 'object') {
return null return null
@@ -85,7 +146,7 @@ export function canViewerCloseTask(task, viewerContext) {
return false 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) { export function resolveDisplayShopName(provider, shopId, shopName) {
@@ -175,6 +236,10 @@ export function resolveOrderItemDeliveryMode(tasks, orderItemId) {
return 'manual_dispatch' 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) { if (task.requires_claim || getTaskPrimaryClaimTokenId(task) || task.primary_claim_token || task.claim_token) {
return 'claim_link' return 'claim_link'
} }
@@ -61,15 +61,18 @@ export {
completeAdminTaskManualDispatch, completeAdminTaskManualDispatch,
confirmAdminTaskAssistedRole, confirmAdminTaskAssistedRole,
createAdminInventoryItem, createAdminInventoryItem,
dispatchAdminTaskKuaishouCloudFulfillment,
importAdminInventoryItems, importAdminInventoryItems,
invalidateAdminInventoryItem, invalidateAdminInventoryItem,
markAdminTaskManualReview, markAdminTaskManualReview,
prepareAdminTaskKuaishouCloudFulfillment,
redeemAdminTaskAssisted, redeemAdminTaskAssisted,
regenerateAdminTaskClaimLink, regenerateAdminTaskClaimLink,
releaseAdminInventoryItem, releaseAdminInventoryItem,
releaseAdminTaskInventory, releaseAdminTaskInventory,
releaseAdminTaskInventoryBinding, releaseAdminTaskInventoryBinding,
replayAdminWebhookEvent, replayAdminWebhookEvent,
returnNumberAdminTaskKuaishouCloudFulfillment,
retryAdminTask, retryAdminTask,
} from './admin-write-service.js' } from './admin-write-service.js'
@@ -348,6 +348,22 @@ function buildTaskBindingState(task) {
return { systemBindingStatus: 'waiting_inventory', userBindingStatus: 'not_started' } 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') { if (normalizedStatus === 'manual_review') {
return { systemBindingStatus: 'manual_review', userBindingStatus: 'binding_exception' } return { systemBindingStatus: 'manual_review', userBindingStatus: 'binding_exception' }
} }
@@ -22,6 +22,23 @@ import { confirmClaimRoleForAdminTask, redeemClaimTaskForAdminTask } from '../cl
import { reserveInventoryForTask } from '../order/inventory-service.js' import { reserveInventoryForTask } from '../order/inventory-service.js'
import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js' import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js'
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-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 { closeTencentBrowserSession } from '../session/session.js'
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.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').AdminTaskBindingReleaseResponse} AdminTaskBindingReleaseResponse */
/** @typedef {import('../../types/admin-write-models.js').AdminTaskManualDispatchResponse} AdminTaskManualDispatchResponse */ /** @typedef {import('../../types/admin-write-models.js').AdminTaskManualDispatchResponse} AdminTaskManualDispatchResponse */
/** @typedef {import('../../types/admin-write-models.js').AdminWebhookReplayResponse} AdminWebhookReplayResponse */ /** @typedef {import('../../types/admin-write-models.js').AdminWebhookReplayResponse} AdminWebhookReplayResponse */
/** @typedef {import('../../types/admin-write-inputs.js').AdminTaskKuaishouCloudDispatchInput} AdminTaskKuaishouCloudDispatchInput */
/** @returns {Promise<AdminInventoryMutationResponse>} */ /** @returns {Promise<AdminInventoryMutationResponse>} */
/** @param {AdminInventoryCreateInput} [payload] */ /** @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 * @param {AdminInventoryImportInput} payload
* @returns {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>} * @returns {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>}
@@ -725,6 +1089,116 @@ function normalizeInventoryImportRows(payload) {
return dedupeRows(rows) 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 * @param {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>} rows
*/ */
@@ -797,14 +1271,3 @@ function normalizeManualDispatchOutcome(value) {
function getTaskClaimExpiresAt(task) { function getTaskClaimExpiresAt(task) {
return task?.claim_expires_at || task?.primary_claim_expires_at || null 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 { normalizeProductName } from '../order/product-match-service.js'
import { query } from '../../db/client.js' import { query } from '../../db/client.js'
import { getOrderFulfillmentBindingConfigs } from '../order/fulfillment-binding-config-service.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' import { nowIso } from '../../utils/time.js'
const CORE_PROFILES = [ 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() { export async function ensureFulfillmentCatalogBootstrapped() {
@@ -93,7 +106,10 @@ async function ensureCoreProfiles() {
export async function syncConfiguredFulfillmentBindings(profileMap = {}) { export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
const timestamp = nowIso() const timestamp = nowIso()
const bindingsToApply = getOrderFulfillmentBindingConfigs() const bindingsToApply = [
...getOrderFulfillmentBindingConfigs(),
...mapKuaishouCloudFulfillmentItemsToBindings(getKuaishouCloudFulfillmentConfig()),
]
await query('DELETE FROM product_match_rules') await query('DELETE FROM product_match_rules')
await query('DELETE FROM sku_fulfillment_bindings') 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 requirements = await listFulfillmentProfileRequirements(profile.profile_id || profile.id)
const primaryRequirement = requirements.find((requirement) => requirement.is_required !== false) || requirements[0] || null const primaryRequirement = requirements.find((requirement) => requirement.is_required !== false) || requirements[0] || null
const quantity = Math.max(1, Number(item.quantity || 1)) const quantity = Math.max(1, Number(item.quantity || 1))
const fulfillmentConfig = parseJsonObject(profile.config_json)
for (let index = 0; index < quantity; index += 1) { for (let index = 0; index < quantity; index += 1) {
const createdAt = nowIso() const createdAt = nowIso()
@@ -84,6 +85,58 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
credentialType: String(primaryRequirement.credential_type || primaryRequirement.credentialType || 'tencent_code'), credentialType: String(primaryRequirement.credential_type || primaryRequirement.credentialType || 'tencent_code'),
} }
: null, : 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, createdAt,
updatedAt: createdAt, updatedAt: createdAt,
@@ -114,6 +167,27 @@ async function preparePaidTask(task) {
return 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') { if (!task.requires_claim || String(task.executor_key || '').trim() === 'manual_dispatch') {
return updateTask(task.id, { return updateTask(task.id, {
task_status: 'manual_review', task_status: 'manual_review',
@@ -182,6 +256,10 @@ function resolvePaidTaskStatus(profile) {
return 'paid' return 'paid'
} }
if (isKuaishouCloudExecutor(profile?.executor_key)) {
return 'pending_binding_prepare'
}
if (String(profile?.executor_key || '').trim() === 'manual_dispatch') { if (String(profile?.executor_key || '').trim() === 'manual_dispatch') {
return 'manual_review' return 'manual_review'
} }
@@ -206,3 +284,24 @@ function parseTaskContext(task) {
return {} 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').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigRouteBody
*/ */
/**
* @typedef {import('./admin-write-inputs.js').AdminKuaishouCloudFulfillmentConfigInput} AdminKuaishouCloudFulfillmentConfigRouteBody
*/
/** /**
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSendSmsCodeInput} AdminCloudtentaclesSendSmsCodeRouteBody * @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').AdminTaskManualDispatchInput} AdminTaskManualDispatchRouteBody
*/ */
/**
* @typedef {import('./admin-write-inputs.js').AdminTaskKuaishouCloudDispatchInput} AdminTaskKuaishouCloudDispatchRouteBody
*/
/** /**
* @typedef {import('./admin-write-inputs.js').AdminManualRedeemCreateInput} AdminManualRedeemCreateRouteBody * @typedef {import('./admin-write-inputs.js').AdminManualRedeemCreateInput} AdminManualRedeemCreateRouteBody
*/ */
@@ -67,6 +67,40 @@ export {}
* }} AdminAgisoShopConfigSaveInput * }} 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 {{ * @typedef {{
* enabled?: boolean * enabled?: boolean
@@ -302,6 +336,12 @@ export {}
* }} AdminTaskManualDispatchInput * }} AdminTaskManualDispatchInput
*/ */
/**
* @typedef {{
* ticketCode?: string
* }} AdminTaskKuaishouCloudDispatchInput
*/
/** /**
* @typedef {{ * @typedef {{
* proofValue?: string * proofValue?: string
@@ -8,15 +8,15 @@ const props = defineProps<{
function resolveTone(status: string) { function resolveTone(status: string) {
const normalized = String(status || '').trim().toLowerCase() 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' 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' 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' return 'warning'
} }
@@ -71,7 +71,11 @@ function resolveLabel(status: string) {
processing: '处理中', processing: '处理中',
system_bound: '系统已绑定', system_bound: '系统已绑定',
pending_binding: '待系统绑定', pending_binding: '待系统绑定',
pending_binding_prepare: '待准备绑定',
not_started: '未开始', not_started: '未开始',
waiting_binding: '待客户绑定',
dispatched_pending_return: '待退还号码',
completed: '已完成',
waiting_user_claim: '待用户打开链接', waiting_user_claim: '待用户打开链接',
link_opened: '已打开链接', link_opened: '已打开链接',
binding_confirmed: '已提交绑定', binding_confirmed: '已提交绑定',
+5
View File
@@ -93,6 +93,11 @@ const router = createRouter({
meta: { allowedRoles: ['admin'] }, meta: { allowedRoles: ['admin'] },
component: () => import('@/views/admin/AdminFulfillmentBindingsView.vue'), component: () => import('@/views/admin/AdminFulfillmentBindingsView.vue'),
}, },
{
path: 'platform-kuaishou-cloud-fulfillment',
meta: { allowedRoles: ['admin'] },
component: () => import('@/views/admin/AdminKuaishouCloudFulfillmentView.vue'),
},
{ {
path: 'webhook-events/:eventId', path: 'webhook-events/:eventId',
meta: { allowedRoles: ['admin', 'operator'] }, meta: { allowedRoles: ['admin', 'operator'] },
+32
View File
@@ -17,6 +17,7 @@ import type {
AdminKhhaoSyncState, AdminKhhaoSyncState,
AdminKhhaoOrderSyncResult, AdminKhhaoOrderSyncResult,
AdminKuaishouEticketConsumeResult, AdminKuaishouEticketConsumeResult,
AdminKuaishouCloudFulfillmentConfig,
AdminKuaishouEticketDetailResult, AdminKuaishouEticketDetailResult,
AdminKuaishouEticketShopInfoResult, AdminKuaishouEticketShopInfoResult,
AdminKuaishouEticketSourceConfig, AdminKuaishouEticketSourceConfig,
@@ -418,6 +419,20 @@ export function saveAdminFulfillmentBindingConfigs(payload: { bindings: Array<Re
}>('/api/v1/admin/platform-config/fulfillment-bindings', payload) }>('/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>) { export function fetchAdminOrders(params?: Record<string, unknown>) {
return apiGet<{ items: AdminOrderListItem[]; pagination: AdminPagination }>('/api/v1/admin/orders', params) 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) 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>) { export function fetchAdminInventoryItems(params?: Record<string, unknown>) {
return apiGet<{ items: AdminInventoryItemListItem[]; pagination: AdminPagination }>('/api/v1/admin/inventory', params) return apiGet<{ items: AdminInventoryItemListItem[]; pagination: AdminPagination }>('/api/v1/admin/inventory', params)
} }
+80
View File
@@ -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 { export interface AdminObservedProductItem {
provider: string provider: string
platform: string platform: string
@@ -497,6 +527,9 @@ export interface AdminTaskOperations {
canClose: boolean canClose: boolean
canMarkManualReview: boolean canMarkManualReview: boolean
canCompleteManualDispatch: boolean canCompleteManualDispatch: boolean
canPrepareKuaishouCloudFulfillment: boolean
canDispatchKuaishouCloudFulfillment: boolean
canReturnKuaishouCloudFulfillment: boolean
canSupportConfirmRole: boolean canSupportConfirmRole: boolean
canSupportRedeem: boolean canSupportRedeem: boolean
canViewSensitiveTaskData: boolean canViewSensitiveTaskData: boolean
@@ -652,6 +685,53 @@ export interface AdminTaskDetail {
resultMessage: string 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 | { manualDispatch: null | {
outcome: string outcome: string
deliveryReference: string deliveryReference: string
@@ -65,6 +65,7 @@ type ValidationState = {
const PROFILE_OPTIONS = [ const PROFILE_OPTIONS = [
{ label: '腾讯领取兑换', value: 'tencent_claim_redeem' }, { label: '腾讯领取兑换', value: 'tencent_claim_redeem' },
{ label: '腾讯领取兑换(半自动+人工)', value: 'tencent_claim_assisted' }, { label: '腾讯领取兑换(半自动+人工)', value: 'tencent_claim_assisted' },
{ label: '快手 Cloud 履约', value: 'kuaishou_ct_assisted' },
{ label: '人工发货', value: 'manual_review' }, { label: '人工发货', value: 'manual_review' },
] ]
@@ -102,6 +103,7 @@ const ruleMetrics = computed(() => {
const completedCount = bindings.value.filter(isEditableBindingComplete).length const completedCount = bindings.value.filter(isEditableBindingComplete).length
const manualCount = bindings.value.filter((item) => item.profileKey.trim() === 'manual_review').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 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 { return {
totalCount: bindings.value.length, totalCount: bindings.value.length,
@@ -109,6 +111,7 @@ const ruleMetrics = computed(() => {
draftCount: Math.max(bindings.value.length - completedCount, 0), draftCount: Math.max(bindings.value.length - completedCount, 0),
manualCount, manualCount,
assistedCount, assistedCount,
kuaishouCloudCount,
} }
}) })
@@ -536,7 +539,7 @@ onMounted(loadConfigs)
<header class="panel-header"> <header class="panel-header">
<div class="panel-header-copy"> <div class="panel-header-copy">
<h1>履约配置</h1> <h1>履约配置</h1>
<p>把外部商品统一映射到内部履约 SKU并指定最终走自动半自动还是人工处理</p> <p>把外部商品统一映射到内部履约 SKU并指定最终走腾讯领取快手 Cloud 人工处理</p>
</div> </div>
<div class="header-tags"> <div class="header-tags">
<span class="header-tag">外部条件命中 1 个即可</span> <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.draftCount }}</span>
<span class="mini-stat-chip">人工 {{ ruleMetrics.manualCount }}</span> <span class="mini-stat-chip">人工 {{ ruleMetrics.manualCount }}</span>
<span class="mini-stat-chip">半自动 {{ ruleMetrics.assistedCount }}</span> <span class="mini-stat-chip">半自动 {{ ruleMetrics.assistedCount }}</span>
<span class="mini-stat-chip">快手 Cloud {{ ruleMetrics.kuaishouCloudCount }}</span>
</div> </div>
<div class="section-action-group"> <div class="section-action-group">
<el-button round @click="addBinding">新增规则</el-button> <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.splice(1, 0, { to: '/admin/users', label: '用户' })
baseItems.push({ to: '/admin/platform-shops', label: '平台配置' }) baseItems.push({ to: '/admin/platform-shops', label: '平台配置' })
baseItems.push({ to: '/admin/platform-fulfillment', label: '履约配置' }) baseItems.push({ to: '/admin/platform-fulfillment', label: '履约配置' })
baseItems.push({ to: '/admin/platform-kuaishou-cloud-fulfillment', label: '新履约配置' })
baseItems.push({ to: '/admin/audit-logs', label: '审计' }) baseItems.push({ to: '/admin/audit-logs', label: '审计' })
} }
@@ -133,6 +134,7 @@ async function submitLogout() {
margin-top: 28px; margin-top: 28px;
display: grid; display: grid;
gap: 10px; gap: 10px;
align-content: start;
} }
.admin-meta { .admin-meta {
@@ -8,12 +8,15 @@ import {
closeAdminTask, closeAdminTask,
confirmAdminTaskAssistedRole, confirmAdminTaskAssistedRole,
completeAdminTaskManualDispatch, completeAdminTaskManualDispatch,
dispatchAdminTaskKuaishouCloudFulfillment,
redeemAdminTaskAssisted, redeemAdminTaskAssisted,
fetchAdminTaskScreenshot, fetchAdminTaskScreenshot,
fetchAdminTaskDetail, fetchAdminTaskDetail,
markAdminTaskManualReview, markAdminTaskManualReview,
prepareAdminTaskKuaishouCloudFulfillment,
regenerateAdminTaskClaimLink, regenerateAdminTaskClaimLink,
releaseAdminTaskInventoryBinding, releaseAdminTaskInventoryBinding,
returnNumberAdminTaskKuaishouCloudFulfillment,
retryAdminTask, retryAdminTask,
} from '@/services/admin' } from '@/services/admin'
import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin' import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
@@ -33,9 +36,14 @@ const manualDispatchForm = reactive({
deliveredCredential: '', deliveredCredential: '',
resultMessage: '', resultMessage: '',
}) })
const kuaishouCloudForm = reactive({
ticketCode: '',
})
const canManageTaskLifecycle = computed(() => hasAdminRole('operator')) const canManageTaskLifecycle = computed(() => hasAdminRole('operator'))
const canCloseTasks = computed(() => hasAdminRole('support')) const canCloseTasks = computed(() => hasAdminRole('support'))
const canViewSensitiveTaskData = computed(() => Boolean(detail.value?.operations.canViewSensitiveTaskData)) 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 claimUrl = computed(() => {
const tokenStatus = String(detail.value?.claimToken?.status || '').trim() const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
const taskStatus = String(detail.value?.task.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 || '')) const response = await fetchAdminTaskDetail(String(route.params.taskId || ''))
detail.value = response.data detail.value = response.data
syncManualDispatchForm(response.data) syncManualDispatchForm(response.data)
syncKuaishouCloudForm(response.data)
await loadScreenshotPreview(response.data) await loadScreenshotPreview(response.data)
} catch (error) { } catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取任务详情失败' errorMessage.value = error instanceof Error ? error.message : '读取任务详情失败'
@@ -133,6 +142,10 @@ function syncManualDispatchForm(taskDetail: AdminTaskDetail) {
manualDispatchForm.resultMessage = taskDetail.manualDispatch?.resultMessage || taskDetail.task.resultMessage || '' manualDispatchForm.resultMessage = taskDetail.manualDispatch?.resultMessage || taskDetail.task.resultMessage || ''
} }
function syncKuaishouCloudForm(taskDetail: AdminTaskDetail) {
kuaishouCloudForm.ticketCode = taskDetail.kuaishouCloudFulfillment?.ticket.code || ''
}
async function submitManualDispatch(outcome: 'delivered' | 'failed') { async function submitManualDispatch(outcome: 'delivered' | 'failed') {
if (!detail.value) { if (!detail.value) {
return 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>) { function formatTaskEventPayload(payload: Record<string, unknown>) {
const parts = [ const parts = [
payload.outcome ? `结果 ${payload.outcome}` : '', payload.outcome ? `结果 ${payload.outcome}` : '',
@@ -166,11 +241,19 @@ function formatTaskEventPayload(payload: Record<string, unknown>) {
payload.inventoryItemId ? `库存项 #${payload.inventoryItemId}` : '', payload.inventoryItemId ? `库存项 #${payload.inventoryItemId}` : '',
payload.previousInventoryItemId ? `旧库存 #${payload.previousInventoryItemId}` : '', payload.previousInventoryItemId ? `旧库存 #${payload.previousInventoryItemId}` : '',
payload.nextInventoryItemId ? `新库存 #${payload.nextInventoryItemId}` : '', 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.previousOutcome ? `切换原因 ${formatRedeemOutcomeLabel(String(payload.previousOutcome || ''))}` : '',
payload.deliveryReference ? `单号 ${payload.deliveryReference}` : '', payload.deliveryReference ? `单号 ${payload.deliveryReference}` : '',
payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : '', payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : '',
payload.trigger ? `触发 ${formatAutoDeliveryTrigger(String(payload.trigger || ''))}` : '', payload.trigger ? `触发 ${formatAutoDeliveryTrigger(String(payload.trigger || ''))}` : '',
payload.reason ? `原因 ${payload.reason}` : '', payload.reason ? `原因 ${payload.reason}` : '',
payload.purchaseTriggered === true ? '已触发购买' : '',
payload.usedKnapsack === true ? '使用背包库存' : '',
payload.responseStatus ? `HTTP ${payload.responseStatus}` : '', payload.responseStatus ? `HTTP ${payload.responseStatus}` : '',
payload.requestId ? `请求 ${payload.requestId}` : '', payload.requestId ? `请求 ${payload.requestId}` : '',
payload.errorMessage ? `错误 ${payload.errorMessage}` : '', payload.errorMessage ? `错误 ${payload.errorMessage}` : '',
@@ -349,6 +432,33 @@ onBeforeUnmount(clearScreenshotPreview)
> >
客服开始兑换 客服开始兑换
</el-button> </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 <el-button
v-if="canManageTaskLifecycle" v-if="canManageTaskLifecycle"
:disabled="!detail.operations.canMarkManualReview" :disabled="!detail.operations.canMarkManualReview"
@@ -423,6 +533,122 @@ onBeforeUnmount(clearScreenshotPreview)
</table> </table>
</section> </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 <section
v-if="detail.order?.provider === 'agiso' && detail.order?.platform === 'xianyu'" v-if="detail.order?.provider === 'agiso' && detail.order?.platform === 'xianyu'"
class="table-card" 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 { 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 { margin: 0 0 8px; color: #334155; }
.manual-summary p:last-child { margin-bottom: 0; } .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-copy { color: #334155; }
.event-time { margin-top: 6px; color: #64748b; font-size: 12px; } .event-time { margin-top: 6px; color: #64748b; font-size: 12px; }
.screenshot-link { display: inline-block; margin-bottom: 12px; color: #175cd3; text-decoration: none; } .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); border: 1px solid rgba(86,108,138,.12);
box-shadow: 0 20px 48px rgba(15,23,42,.08); box-shadow: 0 20px 48px rgba(15,23,42,.08);
} }
@media (max-width: 720px) {
.flow-card-head,
.flow-head-actions {
display: grid;
}
}
</style> </style>