完整流程-调试

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
@@ -11,6 +11,7 @@ import {
getAdminCloudtentaclesAsset,
getAdminCloudtentaclesBindUrl,
getAdminCloudtentaclesCategories,
getAdminKuaishouCloudFulfillmentConfig,
getAdminKuaishouEticketSourceConfig,
getAdminCloudtentaclesKnapsack,
getAdminCloudtentaclesSourceConfig,
@@ -30,6 +31,7 @@ import {
syncAdminKhhaoOrders,
testAdminCloudtentaclesLogin,
testAdminKhhaoLogin,
updateAdminKuaishouCloudFulfillmentConfig,
updateAdminKuaishouEticketSourceConfig,
updateAdminCloudtentaclesSourceConfig,
updateAdminAgisoShopConfigs,
@@ -51,6 +53,7 @@ import { createJsonHandler, requireAdminRoles } from './shared.js'
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketConsumeRouteBody} AdminKuaishouEticketConsumeRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketShopInfoRouteBody} AdminKuaishouEticketShopInfoRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSourceConfigRouteBody} AdminCloudtentaclesSourceConfigRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouCloudFulfillmentConfigRouteBody} AdminKuaishouCloudFulfillmentConfigRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSendSmsCodeRouteBody} AdminCloudtentaclesSendSmsCodeRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesTestLoginRouteBody} AdminCloudtentaclesTestLoginRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesValidateSessionRouteBody} AdminCloudtentaclesValidateSessionRouteBody */
@@ -579,4 +582,34 @@ router.post('/platform-config/fulfillment-bindings', createJsonHandler(
},
))
router.get('/platform-config/kuaishou-cloud-fulfillment', createJsonHandler(
() => getAdminKuaishouCloudFulfillmentConfig(),
{
successMessage: 'ok',
errorMessage: '读取新履约配置失败',
scope: '[admin/platform-config/kuaishou-cloud-fulfillment]',
},
))
router.post('/platform-config/kuaishou-cloud-fulfillment', createJsonHandler(
(req) => updateAdminKuaishouCloudFulfillmentConfig(/** @type {AdminKuaishouCloudFulfillmentConfigRouteBody} */ (req.body)),
{
successMessage: '新履约配置已保存',
errorMessage: '保存新履约配置失败',
scope: '[admin/platform-config/kuaishou-cloud-fulfillment]',
audit: (_req, data) => {
const result = /** @type {{ filePath?: string, source?: { items?: unknown[] } }} */ (data)
return {
action: 'platform_kuaishou_cloud_fulfillment_updated',
targetType: 'platform_config',
targetId: 'kuaishou_cloud_fulfillment',
data: {
itemCount: Array.isArray(result.source?.items) ? result.source.items.length : 0,
filePath: String(result.filePath || '').trim(),
},
}
},
},
))
export default router
+83
View File
@@ -6,11 +6,14 @@ import {
closeAdminTask,
completeAdminTaskManualDispatch,
confirmAdminTaskAssistedRole,
dispatchAdminTaskKuaishouCloudFulfillment,
markAdminTaskManualReview,
prepareAdminTaskKuaishouCloudFulfillment,
regenerateAdminTaskClaimLink,
redeemAdminTaskAssisted,
releaseAdminTaskInventoryBinding,
releaseAdminTaskInventory,
returnNumberAdminTaskKuaishouCloudFulfillment,
retryAdminTask,
} from '../../services/admin/admin-write-service.js'
import {
@@ -21,6 +24,7 @@ import {
import { createFileHandler, createJsonHandler, requireAdminRoles } from './shared.js'
/** @typedef {import('../../types/admin-route-inputs.js').AdminRouteAdminSession} AdminRouteAdminSession */
/** @typedef {import('../../types/admin-route-inputs.js').AdminTaskKuaishouCloudDispatchRouteBody} AdminTaskKuaishouCloudDispatchRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminTaskManualDispatchRouteBody} AdminTaskManualDispatchRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminTaskRouteParams} AdminTaskRouteParams */
/** @typedef {import('../../types/admin-route-inputs.js').AdminTaskRouteQuery} AdminTaskRouteQuery */
@@ -270,4 +274,83 @@ router.post('/tasks/:taskId/complete-manual-dispatch', requireAdminRoles(['admin
},
))
router.post('/tasks/:taskId/kuaishou-cloud/prepare', requireAdminRoles(['admin', 'operator']), createJsonHandler(
(req) => prepareAdminTaskKuaishouCloudFulfillment(
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
/** @type {AdminRouteAdminSession | null} */ (req.adminSession),
),
{
successMessage: '绑定资源已准备完成',
errorMessage: '准备绑定资源失败',
scope: '[admin/tasks/:taskId/kuaishou-cloud/prepare]',
audit: (req, data) => {
const result = /** @type {AdminTaskActionResponse} */ (data)
return {
action: 'task_kuaishou_cloud_prepare',
targetType: 'task',
targetId: String((/** @type {AdminTaskRouteParams} */ (req.params)).taskId),
data: {
taskId: result.task.taskId,
taskNo: result.task.taskNo,
status: result.task.status,
},
}
},
},
))
router.post('/tasks/:taskId/kuaishou-cloud/dispatch', requireAdminRoles(['admin', 'operator']), createJsonHandler(
(req) => dispatchAdminTaskKuaishouCloudFulfillment(
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
/** @type {AdminTaskKuaishouCloudDispatchRouteBody} */ (req.body),
/** @type {AdminRouteAdminSession | null} */ (req.adminSession),
),
{
successMessage: '已完成绑定确认并发货',
errorMessage: '执行发货失败',
scope: '[admin/tasks/:taskId/kuaishou-cloud/dispatch]',
audit: (req, data) => {
const result = /** @type {AdminTaskActionResponse} */ (data)
const body = /** @type {AdminTaskKuaishouCloudDispatchRouteBody} */ (req.body)
return {
action: 'task_kuaishou_cloud_dispatch',
targetType: 'task',
targetId: String((/** @type {AdminTaskRouteParams} */ (req.params)).taskId),
data: {
taskId: result.task.taskId,
taskNo: result.task.taskNo,
status: result.task.status,
deliveryStatus: result.task.deliveryStatus,
ticketCodeProvided: Boolean(String(body.ticketCode || '').trim()),
},
}
},
},
))
router.post('/tasks/:taskId/kuaishou-cloud/return-number', requireAdminRoles(['admin', 'operator']), createJsonHandler(
(req) => returnNumberAdminTaskKuaishouCloudFulfillment(
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
/** @type {AdminRouteAdminSession | null} */ (req.adminSession),
),
{
successMessage: '号码已退还',
errorMessage: '退还号码失败',
scope: '[admin/tasks/:taskId/kuaishou-cloud/return-number]',
audit: (req, data) => {
const result = /** @type {AdminTaskActionResponse} */ (data)
return {
action: 'task_kuaishou_cloud_return_number',
targetType: 'task',
targetId: String((/** @type {AdminTaskRouteParams} */ (req.params)).taskId),
data: {
taskId: result.task.taskId,
taskNo: result.task.taskNo,
status: result.task.status,
},
}
},
},
))
export default router
@@ -68,6 +68,11 @@ import {
getOrderFulfillmentBindingsFilePath,
saveOrderFulfillmentBindingConfigs,
} from '../order/fulfillment-binding-config-service.js'
import {
getKuaishouCloudFulfillmentConfig,
getKuaishouCloudFulfillmentFilePath,
saveKuaishouCloudFulfillmentConfig,
} from '../order/kuaishou-cloud-fulfillment-config-service.js'
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
import { createHttpError } from '../../utils/http.js'
import { formatFenToAmount } from '../../utils/money.js'
@@ -84,6 +89,7 @@ import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouCloudFulfillmentConfigInput} AdminKuaishouCloudFulfillmentConfigInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSendSmsCodeInput} AdminCloudtentaclesSendSmsCodeInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesTestLoginInput} AdminCloudtentaclesTestLoginInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesValidateSessionInput} AdminCloudtentaclesValidateSessionInput */
@@ -778,6 +784,84 @@ export async function getAdminFulfillmentBindingConfigs() {
}
}
export function getAdminKuaishouCloudFulfillmentConfig() {
const config = getKuaishouCloudFulfillmentConfig()
return {
filePath: getKuaishouCloudFulfillmentFilePath(),
source: {
enabled: config.enabled !== false,
items: (Array.isArray(config.items) ? config.items : []).map((item) => ({
id: String(item.id || '').trim(),
enabled: item.enabled !== false,
priority: Number(item.priority || 100) || 100,
provider: String(item.provider || 'khhao').trim() || 'khhao',
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
shopId: String(item.shopId || '').trim(),
internalSkuCode: String(item.internalSkuCode || '').trim(),
internalSkuName: String(item.internalSkuName || '').trim(),
externalSkuCode: String(item.externalSkuCode || '').trim(),
externalItemId: String(item.externalItemId || '').trim(),
externalSkuName: String(item.externalSkuName || '').trim(),
resolvedSkuName: String(item.resolvedSkuName || '').trim(),
cloudSourceKey: String(item.cloudSourceKey || 'default').trim() || 'default',
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
cloudSkuName: String(item.cloudSkuName || '').trim(),
vnKey: String(item.vnKey || '').trim(),
autoBuyEnabled: item.autoBuyEnabled !== false,
minAssetReserve: Number(item.minAssetReserve || 0) || 0,
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
notes: String(item.notes || '').trim(),
})),
},
}
}
/** @param {AdminKuaishouCloudFulfillmentConfigInput} [payload] */
export async function updateAdminKuaishouCloudFulfillmentConfig(
payload = /** @type {AdminKuaishouCloudFulfillmentConfigInput} */ ({}),
) {
const saved = saveKuaishouCloudFulfillmentConfig({
enabled: payload.enabled !== false,
items: Array.isArray(payload.items) ? payload.items : [],
})
await syncConfiguredFulfillmentBindings()
return {
filePath: getKuaishouCloudFulfillmentFilePath(),
source: {
enabled: saved.enabled !== false,
items: (Array.isArray(saved.items) ? saved.items : []).map((item) => ({
id: String(item.id || '').trim(),
enabled: item.enabled !== false,
priority: Number(item.priority || 100) || 100,
provider: String(item.provider || 'khhao').trim() || 'khhao',
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
shopId: String(item.shopId || '').trim(),
internalSkuCode: String(item.internalSkuCode || '').trim(),
internalSkuName: String(item.internalSkuName || '').trim(),
externalSkuCode: String(item.externalSkuCode || '').trim(),
externalItemId: String(item.externalItemId || '').trim(),
externalSkuName: String(item.externalSkuName || '').trim(),
resolvedSkuName: String(item.resolvedSkuName || '').trim(),
cloudSourceKey: String(item.cloudSourceKey || 'default').trim() || 'default',
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
cloudSkuName: String(item.cloudSkuName || '').trim(),
vnKey: String(item.vnKey || '').trim(),
autoBuyEnabled: item.autoBuyEnabled !== false,
minAssetReserve: Number(item.minAssetReserve || 0) || 0,
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
notes: String(item.notes || '').trim(),
})),
},
}
}
/** @param {AdminFulfillmentBindingLookupInput} [payload] */
export async function lookupAdminFulfillmentBindingOrder(payload = /** @type {AdminFulfillmentBindingLookupInput} */ ({})) {
const provider = String(payload.provider || 'agiso').trim() || 'agiso'
@@ -27,6 +27,7 @@ import {
getTaskPrimaryInventoryItemId,
isAssistedClaimTask,
isManualDispatchTask,
mapKuaishouCloudFulfillmentContext,
mapManualDispatchContext,
mapRedeemResolutionContext,
parseTaskContext,
@@ -278,6 +279,7 @@ export async function getAdminTaskDetail(taskId, session = null) {
roleName: String(taskState.reviewRoleName || '').trim() || '',
},
redeemResolution: mapRedeemResolutionContext(taskContext.redeemResolution),
kuaishouCloudFulfillment: mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment),
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
events: taskEvents.map(mapAdminTaskEvent),
operations: {
@@ -289,6 +291,15 @@ export async function getAdminTaskDetail(taskId, session = null) {
canClose: canViewerCloseTask(task, viewerContext),
canMarkManualReview: viewerContext.canManageTaskLifecycle && !['redeemed', 'closed', 'manual_review'].includes(task.task_status),
canCompleteManualDispatch: viewerContext.canManageTaskLifecycle && isManualDispatchTask(task) && !['redeemed', 'closed'].includes(task.task_status),
canPrepareKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& ['pending_binding_prepare', 'manual_review', 'failed'].includes(String(task.task_status || '').trim()),
canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& ['waiting_binding'].includes(String(task.task_status || '').trim()),
canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
&& ['dispatched_pending_return'].includes(String(task.task_status || '').trim()),
canSupportConfirmRole: canViewerConfirmAssistedRole(task, viewerContext),
canSupportRedeem: canViewerRedeemAssistedTask(task, viewerContext),
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData,
@@ -25,6 +25,67 @@ export function mapManualDispatchContext(value, viewerContext = createAdminViewe
}
}
export function mapKuaishouCloudFulfillmentContext(value) {
if (!value || typeof value !== 'object') {
return null
}
const ticket = value.ticket && typeof value.ticket === 'object' ? value.ticket : {}
const binding = value.binding && typeof value.binding === 'object' ? value.binding : {}
const purchase = value.purchase && typeof value.purchase === 'object' ? value.purchase : {}
const dispatch = value.dispatch && typeof value.dispatch === 'object' ? value.dispatch : {}
const returnNumber = value.returnNumber && typeof value.returnNumber === 'object' ? value.returnNumber : {}
const consume = value.consume && typeof value.consume === 'object' ? value.consume : {}
return {
flowType: String(value.flowType || '').trim(),
configId: String(value.configId || '').trim(),
internalSkuCode: String(value.internalSkuCode || '').trim(),
internalSkuName: String(value.internalSkuName || '').trim(),
ticket: {
code: String(ticket.code || '').trim(),
capturedAt: ticket.capturedAt || null,
},
binding: {
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
cloudSourceKey: String(binding.cloudSourceKey || '').trim(),
skuId: Number(binding.skuId || 0) || 0,
skuName: String(binding.skuName || '').trim(),
vnKey: String(binding.vnKey || '').trim(),
vnId: Number(binding.vnId || 0) || 0,
vnPhone: String(binding.vnPhone || '').trim(),
bindUrl: String(binding.bindUrl || '').trim(),
bindPreparedAt: binding.bindPreparedAt || null,
},
purchase: {
autoBuyEnabled: purchase.autoBuyEnabled !== false,
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
usedKnapsack: purchase.usedKnapsack === true,
purchaseTriggered: purchase.purchaseTriggered === true,
assetBefore: Number(purchase.assetBefore || 0) || 0,
assetAfter: Number(purchase.assetAfter || 0) || 0,
purchaseAt: purchase.purchaseAt || null,
},
dispatch: {
status: String(dispatch.status || 'pending').trim() || 'pending',
dispatchAt: dispatch.dispatchAt || null,
sendType: Number(dispatch.sendType || 0) || 0,
note: String(dispatch.note || '').trim(),
},
returnNumber: {
status: String(returnNumber.status || 'pending').trim() || 'pending',
returnedAt: returnNumber.returnedAt || null,
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
},
consume: {
status: String(consume.status || 'pending').trim() || 'pending',
shopId: String(consume.shopId || '').trim(),
autoConsumeEnabled: consume.autoConsumeEnabled === true,
},
notes: String(value.notes || '').trim(),
}
}
export function mapRedeemResolutionContext(value) {
if (!value || typeof value !== 'object') {
return null
@@ -85,7 +146,7 @@ export function canViewerCloseTask(task, viewerContext) {
return false
}
return !['redeemed', 'closed'].includes(String(task?.task_status || '').trim())
return !['redeemed', 'completed', 'closed'].includes(String(task?.task_status || '').trim())
}
export function resolveDisplayShopName(provider, shopId, shopName) {
@@ -175,6 +236,10 @@ export function resolveOrderItemDeliveryMode(tasks, orderItemId) {
return 'manual_dispatch'
}
if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
return 'kuaishou_cloud'
}
if (task.requires_claim || getTaskPrimaryClaimTokenId(task) || task.primary_claim_token || task.claim_token) {
return 'claim_link'
}
@@ -61,15 +61,18 @@ export {
completeAdminTaskManualDispatch,
confirmAdminTaskAssistedRole,
createAdminInventoryItem,
dispatchAdminTaskKuaishouCloudFulfillment,
importAdminInventoryItems,
invalidateAdminInventoryItem,
markAdminTaskManualReview,
prepareAdminTaskKuaishouCloudFulfillment,
redeemAdminTaskAssisted,
regenerateAdminTaskClaimLink,
releaseAdminInventoryItem,
releaseAdminTaskInventory,
releaseAdminTaskInventoryBinding,
replayAdminWebhookEvent,
returnNumberAdminTaskKuaishouCloudFulfillment,
retryAdminTask,
} from './admin-write-service.js'
@@ -348,6 +348,22 @@ function buildTaskBindingState(task) {
return { systemBindingStatus: 'waiting_inventory', userBindingStatus: 'not_started' }
}
if (normalizedStatus === 'pending_binding_prepare') {
return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' }
}
if (normalizedStatus === 'waiting_binding') {
return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' }
}
if (normalizedStatus === 'dispatched_pending_return') {
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
}
if (normalizedStatus === 'completed') {
return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' }
}
if (normalizedStatus === 'manual_review') {
return { systemBindingStatus: 'manual_review', userBindingStatus: 'binding_exception' }
}
@@ -22,6 +22,23 @@ import { confirmClaimRoleForAdminTask, redeemClaimTaskForAdminTask } from '../cl
import { reserveInventoryForTask } from '../order/inventory-service.js'
import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js'
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-service.js'
import { getCloudtentaclesSourceConfig } from '../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionState } from '../platforms/cloudtentacles/session-state-service.js'
import {
buyCloudtentaclesSku,
getCloudtentaclesAsset,
listCloudtentaclesSku,
useCloudtentaclesSku,
} from '../platforms/cloudtentacles/catalog-service.js'
import { getCloudtentaclesKnapsack } from '../platforms/cloudtentacles/knapsack-service.js'
import {
appointCloudtentaclesVirtualNumber,
backCloudtentaclesVirtualNumber,
fetchCloudtentaclesVirtualNumberCode,
generateCloudtentaclesLoginCode,
getCloudtentaclesBindUrl,
verifyCloudtentaclesLoginCode,
} from '../platforms/cloudtentacles/virtual-number-service.js'
import { closeTencentBrowserSession } from '../session/session.js'
import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js'
@@ -56,6 +73,7 @@ import {
/** @typedef {import('../../types/admin-write-models.js').AdminTaskBindingReleaseResponse} AdminTaskBindingReleaseResponse */
/** @typedef {import('../../types/admin-write-models.js').AdminTaskManualDispatchResponse} AdminTaskManualDispatchResponse */
/** @typedef {import('../../types/admin-write-models.js').AdminWebhookReplayResponse} AdminWebhookReplayResponse */
/** @typedef {import('../../types/admin-write-inputs.js').AdminTaskKuaishouCloudDispatchInput} AdminTaskKuaishouCloudDispatchInput */
/** @returns {Promise<AdminInventoryMutationResponse>} */
/** @param {AdminInventoryCreateInput} [payload] */
@@ -675,6 +693,352 @@ export async function completeAdminTaskManualDispatch(taskId, payload = {}, sess
}
}
/** @returns {Promise<AdminTaskActionResponse>} */
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session = null) {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限准备绑定资源', {
statusCode: 403,
errorCode: 'admin_task_prepare_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是快手 cloud 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const cloudContext = resolvePersistedCloudtentaclesContext()
if (!flow.binding.skuId || !flow.binding.vnKey) {
throw createHttpError('当前任务缺少 cloud SKU 或 VN Key 配置', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_binding_config',
})
}
const knapsack = await getCloudtentaclesKnapsack(cloudContext)
const knapsackItem = (Array.isArray(knapsack.items) ? knapsack.items : []).find((item) => Number(item.id || 0) === flow.binding.skuId) || null
let usedKnapsack = Number(knapsackItem?.count || 0) > 0
let purchaseTriggered = false
let assetBefore = 0
let assetAfter = 0
if (!usedKnapsack) {
if (!flow.purchase.autoBuyEnabled) {
throw createHttpError('背包中没有现成库存,且当前配置未开启自动购买', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_auto_buy_disabled',
})
}
const [asset, skuList] = await Promise.all([
getCloudtentaclesAsset(cloudContext),
listCloudtentaclesSku(cloudContext),
])
const targetSku = (Array.isArray(skuList.items) ? skuList.items : []).find((item) => Number(item.id || 0) === flow.binding.skuId) || null
if (!targetSku) {
throw createHttpError(`cloudtentacles 未找到 SKU ${flow.binding.skuId}`, {
statusCode: 404,
errorCode: 'admin_task_kuaishou_cloud_sku_not_found',
})
}
assetBefore = Number(asset.asset || 0) || 0
const targetPrice = Number(targetSku.price || 0) || 0
const requiredAsset = targetPrice + flow.purchase.minAssetReserve
if (assetBefore < requiredAsset) {
throw createHttpError(`余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`, {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_asset_not_enough',
})
}
await buyCloudtentaclesSku({
...cloudContext,
id: flow.binding.skuId,
count: 1,
})
purchaseTriggered = true
const assetResult = await getCloudtentaclesAsset(cloudContext)
assetAfter = Number(assetResult.asset || 0) || 0
}
const appointed = await appointCloudtentaclesVirtualNumber({
...cloudContext,
key: flow.binding.vnKey,
})
const vnId = Number(appointed.item?.id || 0)
const vnPhone = String(appointed.item?.phone || '').trim()
if (!vnId || !vnPhone) {
throw createHttpError('申请虚拟号成功但返回数据不完整', {
statusCode: 502,
errorCode: 'admin_task_kuaishou_cloud_invalid_vn',
})
}
await generateCloudtentaclesLoginCode({
...cloudContext,
key: flow.binding.vnKey,
id: vnId,
})
const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({
...cloudContext,
key: flow.binding.vnKey,
phone: vnPhone,
})
await verifyCloudtentaclesLoginCode({
...cloudContext,
key: flow.binding.vnKey,
id: vnId,
code: fetchedCode.code,
})
const bindUrlResult = await getCloudtentaclesBindUrl({
...cloudContext,
key: flow.binding.vnKey,
id: vnId,
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
prepareStatus: 'ready',
vnId,
vnPhone,
bindUrl: bindUrlResult.bindUrl,
bindPreparedAt: now,
},
purchase: {
...flow.purchase,
usedKnapsack,
purchaseTriggered,
assetBefore,
assetAfter,
purchaseAt: purchaseTriggered ? now : flow.purchase.purchaseAt,
},
},
}
const updatedTask = await updateTask(task.id, {
task_status: 'waiting_binding',
inventory_status: 'not_required',
user_action_status: 'pending',
last_error: '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(task.id, 'kuaishou_cloud_binding_prepared', {
skuId: flow.binding.skuId,
vnKey: flow.binding.vnKey,
vnId,
vnPhoneMasked: maskPhone(vnPhone),
bindUrl: bindUrlResult.bindUrl,
purchaseTriggered,
usedKnapsack,
}, now)
return {
task: mapTaskActionPayload(updatedTask),
}
}
/** @returns {Promise<AdminTaskActionResponse>} */
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminTaskKuaishouCloudDispatchInput} [payload] */
/** @param {AdminViewerSessionInput | null} [session] */
export async function dispatchAdminTaskKuaishouCloudFulfillment(taskId, payload = {}, session = null) {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限执行发货', {
statusCode: 403,
errorCode: 'admin_task_dispatch_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是快手 cloud 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const cloudContext = resolvePersistedCloudtentaclesContext()
if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) {
throw createHttpError('当前任务还没有准备好绑定资源,请先准备绑定资源', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_not_prepared',
})
}
const ticketCode = String(payload.ticketCode || '').trim()
const dispatchResult = await useCloudtentaclesSku({
...cloudContext,
id: flow.binding.skuId,
virtualNumberId: flow.binding.vnId,
phone: flow.binding.vnPhone,
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
ticket: {
...flow.ticket,
code: ticketCode || flow.ticket.code,
capturedAt: ticketCode ? now : flow.ticket.capturedAt,
capturedBy: ticketCode && session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: flow.ticket.capturedBy,
},
dispatch: {
...flow.dispatch,
status: 'success',
dispatchAt: now,
dispatchBy: session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: null,
sendType: Number(dispatchResult.sendType || 0) || 0,
note: String(dispatchResult.note || dispatchResult.responseMessage || '').trim(),
},
},
}
const updatedTask = await updateTask(task.id, {
task_status: 'dispatched_pending_return',
delivery_status: 'delivered',
result_code: 'kuaishou_cloud_dispatched',
result_message: String(dispatchResult.responseMessage || dispatchResult.note || 'cloudtentacles 发货成功').trim(),
last_error: '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(task.id, 'kuaishou_cloud_dispatched', {
ticketCodeMasked: maskCode(ticketCode),
skuId: flow.binding.skuId,
vnId: flow.binding.vnId,
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
sendType: dispatchResult.sendType,
note: dispatchResult.note,
}, now)
return {
task: mapTaskActionPayload(updatedTask),
}
}
/** @returns {Promise<AdminTaskActionResponse>} */
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function returnNumberAdminTaskKuaishouCloudFulfillment(taskId, session = null) {
const task = await getRequiredTask(taskId)
const now = nowIso()
const viewerContext = createAdminViewerContext(session)
if (!viewerContext.canManageTaskLifecycle) {
throw createHttpError('当前账号没有权限退还号码', {
statusCode: 403,
errorCode: 'admin_task_return_kuaishou_cloud_forbidden',
})
}
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是快手 cloud 履约任务', {
statusCode: 409,
errorCode: 'admin_task_not_kuaishou_cloud',
})
}
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const cloudContext = resolvePersistedCloudtentaclesContext()
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError('当前任务缺少可退还的虚拟号信息', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_return_context',
})
}
await backCloudtentaclesVirtualNumber({
...cloudContext,
key: flow.binding.vnKey,
id: flow.binding.vnId,
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
returnNumber: {
...flow.returnNumber,
status: 'success',
returnedAt: now,
returnedBy: session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: null,
},
},
}
const updatedTask = await updateTask(task.id, {
task_status: 'completed',
delivery_status: 'delivered',
result_code: 'kuaishou_cloud_completed',
result_message: 'cloudtentacles 发货并退号完成',
redeemed_at: now,
last_error: '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(task.id, 'kuaishou_cloud_number_returned', {
vnId: flow.binding.vnId,
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
}, now)
return {
task: mapTaskActionPayload(updatedTask),
}
}
/**
* @param {AdminInventoryImportInput} payload
* @returns {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>}
@@ -725,6 +1089,116 @@ function normalizeInventoryImportRows(payload) {
return dedupeRows(rows)
}
function isKuaishouCloudTask(task) {
return String(task?.executor_key || '').trim() === 'kuaishou_ct_assisted'
}
function resolvePersistedCloudtentaclesContext() {
const source = getCloudtentaclesSourceConfig()
const session = getCloudtentaclesSessionState()
const token = String(session.token || '').trim()
if (!token) {
throw createHttpError('当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_cloud_token',
})
}
return {
baseUrl: String(session.baseUrl || source.baseUrl || '').trim() || 'https://123.207.217.176',
token,
deviceId: String(session.deviceId || source.deviceId || '-').trim() || '-',
deviceType: Number(session.deviceType ?? source.deviceType ?? 0),
}
}
function normalizeKuaishouCloudFlow(value) {
const source = value && typeof value === 'object' ? value : {}
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {}
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {}
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
return {
...source,
configId: String(source.configId || '').trim(),
internalSkuCode: String(source.internalSkuCode || '').trim(),
internalSkuName: String(source.internalSkuName || '').trim(),
ticket: {
code: String(ticket.code || '').trim(),
capturedAt: ticket.capturedAt || null,
capturedBy: ticket.capturedBy || null,
},
binding: {
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
cloudSourceKey: String(binding.cloudSourceKey || 'default').trim() || 'default',
skuId: Number(binding.skuId || 0) || 0,
skuName: String(binding.skuName || '').trim(),
vnKey: String(binding.vnKey || '').trim(),
vnId: Number(binding.vnId || 0) || 0,
vnPhone: String(binding.vnPhone || '').trim(),
bindUrl: String(binding.bindUrl || '').trim(),
bindPreparedAt: binding.bindPreparedAt || null,
},
purchase: {
autoBuyEnabled: purchase.autoBuyEnabled !== false,
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
usedKnapsack: purchase.usedKnapsack === true,
purchaseTriggered: purchase.purchaseTriggered === true,
assetBefore: Number(purchase.assetBefore || 0) || 0,
assetAfter: Number(purchase.assetAfter || 0) || 0,
purchaseAt: purchase.purchaseAt || null,
},
dispatch: {
status: String(dispatch.status || 'pending').trim() || 'pending',
dispatchAt: dispatch.dispatchAt || null,
dispatchBy: dispatch.dispatchBy || null,
sendType: Number(dispatch.sendType || 0) || 0,
note: String(dispatch.note || '').trim(),
},
returnNumber: {
status: String(returnNumber.status || 'pending').trim() || 'pending',
returnedAt: returnNumber.returnedAt || null,
returnedBy: returnNumber.returnedBy || null,
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
},
consume: {
status: String(consume.status || 'pending').trim() || 'pending',
shopId: String(consume.shopId || '').trim(),
autoConsumeEnabled: consume.autoConsumeEnabled === true,
},
}
}
function maskPhone(value) {
const text = String(value || '').trim()
if (!text) {
return ''
}
if (text.length <= 7) {
return `${text.slice(0, 2)}***${text.slice(-2)}`
}
return `${text.slice(0, 3)}****${text.slice(-4)}`
}
function maskCode(value) {
const text = String(value || '').trim()
if (!text) {
return ''
}
if (text.length <= 8) {
return `${text.slice(0, 2)}***${text.slice(-2)}`
}
return `${text.slice(0, 4)}****${text.slice(-4)}`
}
/**
* @param {Array<Required<Pick<AdminInventoryImportRowInput, 'skuCode' | 'displayValue' | 'batchNo' | 'credentialType' | 'inventoryGroupCode'>>>} rows
*/
@@ -797,14 +1271,3 @@ function normalizeManualDispatchOutcome(value) {
function getTaskClaimExpiresAt(task) {
return task?.claim_expires_at || task?.primary_claim_expires_at || null
}
function maskCode(value) {
const text = String(value || '').trim()
if (!text) {
return ''
}
if (text.length <= 8) {
return `${text.slice(0, 2)}****${text.slice(-2)}`
}
return `${text.slice(0, 4)}****${text.slice(-4)}`
}
@@ -9,6 +9,10 @@ import { upsertProductMatchRule } from '../../repositories/product-match-rule-re
import { normalizeProductName } from '../order/product-match-service.js'
import { query } from '../../db/client.js'
import { getOrderFulfillmentBindingConfigs } from '../order/fulfillment-binding-config-service.js'
import {
getKuaishouCloudFulfillmentConfig,
mapKuaishouCloudFulfillmentItemsToBindings,
} from '../order/kuaishou-cloud-fulfillment-config-service.js'
import { nowIso } from '../../utils/time.js'
const CORE_PROFILES = [
@@ -53,6 +57,15 @@ const CORE_PROFILES = [
},
],
},
{
profileKey: 'kuaishou_ct_assisted',
name: '快手 cloud 履约',
executorKey: 'kuaishou_ct_assisted',
requiresClaim: false,
autoDispatch: false,
inventoryStrategy: 'external_platform',
requirements: [],
},
]
export async function ensureFulfillmentCatalogBootstrapped() {
@@ -93,7 +106,10 @@ async function ensureCoreProfiles() {
export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
const timestamp = nowIso()
const bindingsToApply = getOrderFulfillmentBindingConfigs()
const bindingsToApply = [
...getOrderFulfillmentBindingConfigs(),
...mapKuaishouCloudFulfillmentItemsToBindings(getKuaishouCloudFulfillmentConfig()),
]
await query('DELETE FROM product_match_rules')
await query('DELETE FROM sku_fulfillment_bindings')
@@ -41,6 +41,7 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
const requirements = await listFulfillmentProfileRequirements(profile.profile_id || profile.id)
const primaryRequirement = requirements.find((requirement) => requirement.is_required !== false) || requirements[0] || null
const quantity = Math.max(1, Number(item.quantity || 1))
const fulfillmentConfig = parseJsonObject(profile.config_json)
for (let index = 0; index < quantity; index += 1) {
const createdAt = nowIso()
@@ -84,6 +85,58 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
credentialType: String(primaryRequirement.credential_type || primaryRequirement.credentialType || 'tencent_code'),
}
: null,
kuaishouCloudFulfillment: isKuaishouCloudExecutor(profile.executor_key)
? {
flowType: 'kuaishou_cloud_fulfillment',
configId: String(fulfillmentConfig.configId || '').trim(),
internalSkuCode: item.sku_code,
internalSkuName: item.sku_name,
ticket: {
code: '',
capturedAt: null,
capturedBy: null,
},
binding: {
prepareStatus: 'pending',
cloudSourceKey: String(fulfillmentConfig.cloudtentacles?.cloudSourceKey || 'default').trim() || 'default',
skuId: Number(fulfillmentConfig.cloudtentacles?.skuId || 0) || 0,
skuName: String(fulfillmentConfig.cloudtentacles?.skuName || '').trim(),
vnKey: String(fulfillmentConfig.cloudtentacles?.vnKey || '').trim(),
vnId: 0,
vnPhone: '',
bindUrl: '',
bindPreparedAt: null,
},
purchase: {
autoBuyEnabled: fulfillmentConfig.cloudtentacles?.autoBuyEnabled !== false,
minAssetReserve: Number(fulfillmentConfig.cloudtentacles?.minAssetReserve || 0) || 0,
usedKnapsack: false,
purchaseTriggered: false,
assetBefore: 0,
assetAfter: 0,
purchaseAt: null,
},
dispatch: {
status: 'pending',
dispatchAt: null,
dispatchBy: null,
sendType: 0,
note: '',
},
returnNumber: {
status: 'pending',
returnedAt: null,
returnedBy: null,
autoReturnEnabled: fulfillmentConfig.cloudtentacles?.autoReturnNumberAfterDispatch === true,
},
consume: {
status: 'pending',
shopId: String(fulfillmentConfig.kuaishouConsume?.shopId || '').trim(),
autoConsumeEnabled: fulfillmentConfig.kuaishouConsume?.autoConsumeAfterDispatch === true,
},
notes: String(fulfillmentConfig.notes || '').trim(),
}
: null,
}),
createdAt,
updatedAt: createdAt,
@@ -114,6 +167,27 @@ async function preparePaidTask(task) {
return task
}
if (isKuaishouCloudExecutor(task.executor_key)) {
if ([
'pending_binding_prepare',
'waiting_binding',
'dispatched_pending_return',
'completed',
'manual_review',
'failed',
].includes(String(task.task_status || '').trim())) {
return task
}
return updateTask(task.id, {
task_status: 'pending_binding_prepare',
inventory_status: 'not_required',
user_action_status: 'pending',
last_error: task.last_error || '等待准备绑定资源',
updated_at: now,
})
}
if (!task.requires_claim || String(task.executor_key || '').trim() === 'manual_dispatch') {
return updateTask(task.id, {
task_status: 'manual_review',
@@ -182,6 +256,10 @@ function resolvePaidTaskStatus(profile) {
return 'paid'
}
if (isKuaishouCloudExecutor(profile?.executor_key)) {
return 'pending_binding_prepare'
}
if (String(profile?.executor_key || '').trim() === 'manual_dispatch') {
return 'manual_review'
}
@@ -206,3 +284,24 @@ function parseTaskContext(task) {
return {}
}
}
function parseJsonObject(value) {
if (!value) {
return {}
}
if (typeof value === 'object' && !Array.isArray(value)) {
return value
}
try {
const parsed = JSON.parse(String(value || '{}'))
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
} catch {
return {}
}
}
function isKuaishouCloudExecutor(value) {
return String(value || '').trim() === 'kuaishou_ct_assisted'
}
@@ -0,0 +1,166 @@
import fs from 'node:fs'
import path from 'node:path'
import { PROJECT_ROOT } from '../../config/runtime.js'
const KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-cloud-fulfillment.json')
export function getKuaishouCloudFulfillmentFilePath() {
return KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH
}
export function getKuaishouCloudFulfillmentConfig() {
return loadKuaishouCloudFulfillmentConfigFromFile()
}
export function saveKuaishouCloudFulfillmentConfig(rawValue) {
const normalized = normalizeKuaishouCloudFulfillmentConfig(rawValue)
fs.mkdirSync(path.dirname(KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH), { recursive: true })
fs.writeFileSync(
KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH,
`${JSON.stringify(normalized, null, 2)}\n`,
'utf8',
)
return normalized
}
export function mapKuaishouCloudFulfillmentItemsToBindings(config = {}) {
const items = Array.isArray(config.items) ? config.items : []
return items
.filter((item) => item && item.enabled !== false)
.map((item) => ({
provider: String(item.provider || 'khhao').trim() || 'khhao',
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
shopId: String(item.shopId || '').trim(),
skuCode: String(item.internalSkuCode || '').trim(),
skuName: String(item.internalSkuName || '').trim(),
profileKey: 'kuaishou_ct_assisted',
enabled: item.enabled !== false,
priority: normalizePriority(item.priority),
config: {
flowType: 'kuaishou_cloud_fulfillment',
configId: String(item.id || '').trim(),
cloudtentacles: {
cloudSourceKey: String(item.cloudSourceKey || 'default').trim() || 'default',
skuId: normalizePositiveInteger(item.cloudSkuId),
skuName: String(item.cloudSkuName || '').trim(),
vnKey: String(item.vnKey || '').trim(),
autoBuyEnabled: item.autoBuyEnabled !== false,
minAssetReserve: normalizeNonNegativeInteger(item.minAssetReserve, 0),
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
},
kuaishouConsume: {
shopId: String(item.kuaishouConsumeShopId || '').trim(),
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
},
notes: String(item.notes || '').trim(),
},
match: {
externalSkuCode: String(item.externalSkuCode || '').trim(),
externalItemId: String(item.externalItemId || '').trim(),
externalSkuName: String(item.externalSkuName || '').trim(),
config: {
resolvedSkuName: String(item.resolvedSkuName || item.internalSkuName || '').trim(),
},
},
}))
.filter((item) => item.skuCode && (item.match.externalSkuCode || item.match.externalItemId || item.match.externalSkuName))
}
function loadKuaishouCloudFulfillmentConfigFromFile() {
if (!fs.existsSync(KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH)) {
return normalizeKuaishouCloudFulfillmentConfig({})
}
try {
const rawText = fs.readFileSync(KUAISHOU_CLOUD_FULFILLMENT_FILE_PATH, 'utf8')
const parsed = JSON.parse(rawText)
return normalizeKuaishouCloudFulfillmentConfig(parsed)
} catch {
return normalizeKuaishouCloudFulfillmentConfig({})
}
}
function normalizeKuaishouCloudFulfillmentConfig(rawValue) {
const source = isPlainObject(rawValue) ? rawValue : {}
return {
enabled: source.enabled !== false,
items: Array.isArray(source.items)
? source.items.map((item) => normalizeKuaishouCloudFulfillmentItem(item)).filter(Boolean)
: [],
}
}
function normalizeKuaishouCloudFulfillmentItem(rawValue) {
if (!isPlainObject(rawValue)) {
return null
}
const internalSkuCode = String(rawValue.internalSkuCode || '').trim()
const cloudSkuId = normalizePositiveInteger(rawValue.cloudSkuId)
const vnKey = String(rawValue.vnKey || '').trim()
const externalSkuCode = String(rawValue.externalSkuCode || '').trim()
const externalItemId = String(rawValue.externalItemId || '').trim()
const externalSkuName = String(rawValue.externalSkuName || '').trim()
if (!internalSkuCode) {
return null
}
if (!cloudSkuId || !vnKey) {
return null
}
if (!externalSkuCode && !externalItemId && !externalSkuName) {
return null
}
return {
id: String(rawValue.id || internalSkuCode).trim() || internalSkuCode,
enabled: rawValue.enabled !== false,
priority: normalizePriority(rawValue.priority),
provider: String(rawValue.provider || 'khhao').trim() || 'khhao',
platform: String(rawValue.platform || 'kuaishou').trim() || 'kuaishou',
shopId: String(rawValue.shopId || '').trim(),
internalSkuCode,
internalSkuName: String(rawValue.internalSkuName || '').trim() || internalSkuCode,
externalSkuCode,
externalItemId,
externalSkuName,
resolvedSkuName: String(rawValue.resolvedSkuName || rawValue.internalSkuName || '').trim(),
cloudSourceKey: String(rawValue.cloudSourceKey || 'default').trim() || 'default',
cloudSkuId,
cloudSkuName: String(rawValue.cloudSkuName || '').trim(),
vnKey,
autoBuyEnabled: rawValue.autoBuyEnabled !== false,
minAssetReserve: normalizeNonNegativeInteger(rawValue.minAssetReserve, 0),
autoReturnNumberAfterDispatch: rawValue.autoReturnNumberAfterDispatch === true,
autoConsumeAfterDispatch: rawValue.autoConsumeAfterDispatch === true,
kuaishouConsumeShopId: String(rawValue.kuaishouConsumeShopId || '').trim(),
notes: String(rawValue.notes || '').trim(),
}
}
function normalizePositiveInteger(value) {
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0
}
function normalizeNonNegativeInteger(value, fallback) {
const parsed = Number(value)
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback
}
function normalizePriority(value) {
const parsed = Number(value)
if (!Number.isFinite(parsed)) {
return 100
}
return Math.max(1, Math.round(parsed))
}
function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
@@ -66,6 +66,10 @@ export {}
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminKuaishouCloudFulfillmentConfigInput} AdminKuaishouCloudFulfillmentConfigRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSendSmsCodeInput} AdminCloudtentaclesSendSmsCodeRouteBody
*/
@@ -126,6 +130,10 @@ export {}
* @typedef {import('./admin-write-inputs.js').AdminTaskManualDispatchInput} AdminTaskManualDispatchRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminTaskKuaishouCloudDispatchInput} AdminTaskKuaishouCloudDispatchRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminManualRedeemCreateInput} AdminManualRedeemCreateRouteBody
*/
@@ -67,6 +67,40 @@ export {}
* }} AdminAgisoShopConfigSaveInput
*/
/**
* @typedef {{
* id?: string
* enabled?: boolean
* priority?: number | string
* provider?: string
* platform?: string
* shopId?: string
* internalSkuCode?: string
* internalSkuName?: string
* externalSkuCode?: string
* externalItemId?: string
* externalSkuName?: string
* resolvedSkuName?: string
* cloudSourceKey?: string
* cloudSkuId?: number | string
* cloudSkuName?: string
* vnKey?: string
* autoBuyEnabled?: boolean
* minAssetReserve?: number | string
* autoReturnNumberAfterDispatch?: boolean
* autoConsumeAfterDispatch?: boolean
* kuaishouConsumeShopId?: string
* notes?: string
* }} AdminKuaishouCloudFulfillmentItemInput
*/
/**
* @typedef {{
* enabled?: boolean
* items?: AdminKuaishouCloudFulfillmentItemInput[]
* }} AdminKuaishouCloudFulfillmentConfigInput
*/
/**
* @typedef {{
* enabled?: boolean
@@ -302,6 +336,12 @@ export {}
* }} AdminTaskManualDispatchInput
*/
/**
* @typedef {{
* ticketCode?: string
* }} AdminTaskKuaishouCloudDispatchInput
*/
/**
* @typedef {{
* proofValue?: string