后端迁移履约任务服务
This commit is contained in:
+119
-25
@@ -8,12 +8,102 @@ import { createTaskClaimToken } from '../claim/claim-service.js'
|
|||||||
import { notifyTaskAutoManualReview } from '../notification/domain-notifications.js'
|
import { notifyTaskAutoManualReview } from '../notification/domain-notifications.js'
|
||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
import { randomId } from '../../utils/random.js'
|
import { randomId } from '../../utils/random.js'
|
||||||
|
import type { InventoryItemRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository-rows.js'
|
||||||
|
|
||||||
export async function syncDeliveryTasksForOrder(order, orderItems) {
|
type JsonObject = Record<string, unknown>
|
||||||
|
|
||||||
|
type ClaimTokenLike = {
|
||||||
|
token: string
|
||||||
|
expired_at: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type FulfillmentBindingLike = {
|
||||||
|
id: number
|
||||||
|
profile_id?: number
|
||||||
|
profile_key?: string
|
||||||
|
profile_name?: string
|
||||||
|
name?: string
|
||||||
|
executor_key?: string
|
||||||
|
requires_claim?: boolean
|
||||||
|
auto_dispatch?: boolean
|
||||||
|
config_json?: string | JsonObject
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type FulfillmentRequirementLike = {
|
||||||
|
role_key?: string
|
||||||
|
roleKey?: string
|
||||||
|
credential_type?: string
|
||||||
|
credentialType?: string
|
||||||
|
is_required?: boolean
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliveryTaskRow = TaskRow & {
|
||||||
|
skuCode?: string
|
||||||
|
skuName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliveryTaskDeps = {
|
||||||
|
createTask?: typeof createTask
|
||||||
|
listTasksByOrderId?: typeof listTasksByOrderId
|
||||||
|
updateTask?: typeof updateTask
|
||||||
|
listFulfillmentProfileRequirements?: (
|
||||||
|
profileId: number | string,
|
||||||
|
) => Promise<FulfillmentRequirementLike[]>
|
||||||
|
resolveFulfillmentBinding?: (input: {
|
||||||
|
skuCode: string
|
||||||
|
provider?: string
|
||||||
|
platform?: string
|
||||||
|
shopId?: string
|
||||||
|
}) => Promise<FulfillmentBindingLike | null>
|
||||||
|
reserveInventoryForTask?: (input: {
|
||||||
|
skuCode: string
|
||||||
|
taskId: number | string
|
||||||
|
credentialType?: string
|
||||||
|
roleKey?: string
|
||||||
|
}) => Promise<InventoryItemRow | null>
|
||||||
|
createTaskClaimToken?: (taskId: number | string) => Promise<ClaimTokenLike>
|
||||||
|
notifyTaskAutoManualReview?: (payload: {
|
||||||
|
task: unknown
|
||||||
|
reason: string
|
||||||
|
source: string
|
||||||
|
}) => Promise<unknown> | unknown
|
||||||
|
nowIso?: () => string
|
||||||
|
randomId?: (prefix?: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuntimeDeliveryTaskDeps = Required<Pick<
|
||||||
|
DeliveryTaskDeps,
|
||||||
|
'updateTask'
|
||||||
|
| 'reserveInventoryForTask'
|
||||||
|
| 'createTaskClaimToken'
|
||||||
|
| 'notifyTaskAutoManualReview'
|
||||||
|
| 'nowIso'
|
||||||
|
>>
|
||||||
|
|
||||||
|
type TaskContext = {
|
||||||
|
inventorySkuCode?: string
|
||||||
|
primaryRequirement?: {
|
||||||
|
roleKey?: string
|
||||||
|
credentialType?: string
|
||||||
|
} | null
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncDeliveryTasksForOrder(
|
||||||
|
order: OrderRow,
|
||||||
|
orderItems: OrderItemRow[],
|
||||||
|
): Promise<Array<TaskRow | null>> {
|
||||||
return syncDeliveryTasksForOrderWithDeps(order, orderItems)
|
return syncDeliveryTasksForOrderWithDeps(order, orderItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncDeliveryTasksForOrderWithDeps(order, orderItems, deps = {}) {
|
export async function syncDeliveryTasksForOrderWithDeps(
|
||||||
|
order: OrderRow,
|
||||||
|
orderItems: OrderItemRow[],
|
||||||
|
deps: DeliveryTaskDeps = {},
|
||||||
|
): Promise<Array<TaskRow | null>> {
|
||||||
const {
|
const {
|
||||||
createTask: createDeliveryTask = createTask,
|
createTask: createDeliveryTask = createTask,
|
||||||
listTasksByOrderId: listTasks = listTasksByOrderId,
|
listTasksByOrderId: listTasks = listTasksByOrderId,
|
||||||
@@ -27,7 +117,7 @@ export async function syncDeliveryTasksForOrderWithDeps(order, orderItems, deps
|
|||||||
randomId: createRandomId = randomId,
|
randomId: createRandomId = randomId,
|
||||||
} = deps
|
} = deps
|
||||||
|
|
||||||
const runtimeDeps = {
|
const runtimeDeps: RuntimeDeliveryTaskDeps = {
|
||||||
updateTask: updateDeliveryTask,
|
updateTask: updateDeliveryTask,
|
||||||
reserveInventoryForTask: reserveInventory,
|
reserveInventoryForTask: reserveInventory,
|
||||||
createTaskClaimToken: createClaimToken,
|
createTaskClaimToken: createClaimToken,
|
||||||
@@ -50,7 +140,7 @@ export async function syncDeliveryTasksForOrderWithDeps(order, orderItems, deps
|
|||||||
}, runtimeDeps)))
|
}, runtimeDeps)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = []
|
const tasks: DeliveryTaskRow[] = []
|
||||||
|
|
||||||
for (const item of orderItems) {
|
for (const item of orderItems) {
|
||||||
const binding = await resolveBinding({
|
const binding = await resolveBinding({
|
||||||
@@ -68,9 +158,9 @@ export async function syncDeliveryTasksForOrderWithDeps(order, orderItems, deps
|
|||||||
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)
|
const fulfillmentConfig = parseJsonObject(profile.config_json)
|
||||||
const kuaishouShopConfig = fulfillmentConfig.kuaishouShop && typeof fulfillmentConfig.kuaishouShop === 'object'
|
const cloudtentaclesConfig = parseJsonObject(fulfillmentConfig.cloudtentacles)
|
||||||
? fulfillmentConfig.kuaishouShop
|
const kuaishouConsumeConfig = parseJsonObject(fulfillmentConfig.kuaishouConsume)
|
||||||
: {}
|
const kuaishouShopConfig = parseJsonObject(fulfillmentConfig.kuaishouShop)
|
||||||
|
|
||||||
for (let index = 0; index < quantity; index += 1) {
|
for (let index = 0; index < quantity; index += 1) {
|
||||||
const createdAt = getNowIso()
|
const createdAt = getNowIso()
|
||||||
@@ -133,9 +223,9 @@ export async function syncDeliveryTasksForOrderWithDeps(order, orderItems, deps
|
|||||||
},
|
},
|
||||||
binding: {
|
binding: {
|
||||||
prepareStatus: 'pending',
|
prepareStatus: 'pending',
|
||||||
cloudSourceKey: String(fulfillmentConfig.cloudtentacles?.cloudSourceKey || 'default').trim() || 'default',
|
cloudSourceKey: String(cloudtentaclesConfig.cloudSourceKey || 'default').trim() || 'default',
|
||||||
skuId: Number(fulfillmentConfig.cloudtentacles?.skuId || 0) || 0,
|
skuId: Number(cloudtentaclesConfig.skuId || 0) || 0,
|
||||||
skuName: String(fulfillmentConfig.cloudtentacles?.skuName || '').trim(),
|
skuName: String(cloudtentaclesConfig.skuName || '').trim(),
|
||||||
vnKey: '1',
|
vnKey: '1',
|
||||||
vnId: 0,
|
vnId: 0,
|
||||||
vnPhone: '',
|
vnPhone: '',
|
||||||
@@ -155,8 +245,8 @@ export async function syncDeliveryTasksForOrderWithDeps(order, orderItems, deps
|
|||||||
rawInfo: null,
|
rawInfo: null,
|
||||||
},
|
},
|
||||||
purchase: {
|
purchase: {
|
||||||
autoBuyEnabled: fulfillmentConfig.cloudtentacles?.autoBuyEnabled !== false,
|
autoBuyEnabled: cloudtentaclesConfig.autoBuyEnabled !== false,
|
||||||
minAssetReserve: Number(fulfillmentConfig.cloudtentacles?.minAssetReserve || 0) || 0,
|
minAssetReserve: Number(cloudtentaclesConfig.minAssetReserve || 0) || 0,
|
||||||
usedKnapsack: false,
|
usedKnapsack: false,
|
||||||
purchaseTriggered: false,
|
purchaseTriggered: false,
|
||||||
assetBefore: 0,
|
assetBefore: 0,
|
||||||
@@ -174,13 +264,13 @@ export async function syncDeliveryTasksForOrderWithDeps(order, orderItems, deps
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
returnedAt: null,
|
returnedAt: null,
|
||||||
returnedBy: null,
|
returnedBy: null,
|
||||||
autoReturnEnabled: fulfillmentConfig.cloudtentacles?.autoReturnNumberAfterDispatch === true,
|
autoReturnEnabled: cloudtentaclesConfig.autoReturnNumberAfterDispatch === true,
|
||||||
},
|
},
|
||||||
consume: {
|
consume: {
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
shopId: String(fulfillmentConfig.kuaishouConsume?.shopId || kuaishouShopConfig.shopId || '').trim(),
|
shopId: String(kuaishouConsumeConfig.shopId || kuaishouShopConfig.shopId || '').trim(),
|
||||||
shopName: String(fulfillmentConfig.kuaishouConsume?.shopName || kuaishouShopConfig.kshopName || order.shop_name || '').trim(),
|
shopName: String(kuaishouConsumeConfig.shopName || kuaishouShopConfig.kshopName || order.shop_name || '').trim(),
|
||||||
autoConsumeEnabled: fulfillmentConfig.kuaishouConsume?.autoConsumeAfterDispatch === true,
|
autoConsumeEnabled: kuaishouConsumeConfig.autoConsumeAfterDispatch === true,
|
||||||
consumedAt: null,
|
consumedAt: null,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
},
|
},
|
||||||
@@ -207,7 +297,10 @@ export async function syncDeliveryTasksForOrderWithDeps(order, orderItems, deps
|
|||||||
return Promise.all(tasks.map((task) => preparePaidTask(task, runtimeDeps)))
|
return Promise.all(tasks.map((task) => preparePaidTask(task, runtimeDeps)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preparePaidTask(task, deps = {}) {
|
async function preparePaidTask(
|
||||||
|
task: DeliveryTaskRow,
|
||||||
|
deps: Partial<RuntimeDeliveryTaskDeps> = {},
|
||||||
|
): Promise<TaskRow | DeliveryTaskRow | null> {
|
||||||
const {
|
const {
|
||||||
updateTask: updateDeliveryTask = updateTask,
|
updateTask: updateDeliveryTask = updateTask,
|
||||||
reserveInventoryForTask: reserveInventory = reserveInventoryForTask,
|
reserveInventoryForTask: reserveInventory = reserveInventoryForTask,
|
||||||
@@ -344,7 +437,7 @@ async function preparePaidTask(task, deps = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePaidTaskStatus(profile) {
|
function resolvePaidTaskStatus(profile: FulfillmentBindingLike | null | undefined): string {
|
||||||
if (Boolean(profile?.requires_claim)) {
|
if (Boolean(profile?.requires_claim)) {
|
||||||
return 'paid'
|
return 'paid'
|
||||||
}
|
}
|
||||||
@@ -360,7 +453,7 @@ function resolvePaidTaskStatus(profile) {
|
|||||||
return 'paid'
|
return 'paid'
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTaskContext(task) {
|
function parseTaskContext(task: { context_json?: unknown } | null | undefined): TaskContext {
|
||||||
const value = task?.context_json
|
const value = task?.context_json
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -368,33 +461,34 @@ function parseTaskContext(task) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
return value
|
return value as TaskContext
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(String(value || '{}'))
|
const parsed = JSON.parse(String(value || '{}'))
|
||||||
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as TaskContext : {}
|
||||||
} catch {
|
} catch {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJsonObject(value) {
|
function parseJsonObject(value: unknown): JsonObject {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
return value
|
return value as JsonObject
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(String(value || '{}'))
|
const parsed = JSON.parse(String(value || '{}'))
|
||||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as JsonObject : {}
|
||||||
} catch {
|
} catch {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKuaishouCloudExecutor(value) {
|
function isKuaishouCloudExecutor(value: unknown): boolean {
|
||||||
return String(value || '').trim() === 'kuaishou_ct_assisted'
|
return String(value || '').trim() === 'kuaishou_ct_assisted'
|
||||||
}
|
}
|
||||||
@@ -297,14 +297,21 @@
|
|||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `npm test` 共 128 个用例通过
|
- `npm test` 共 128 个用例通过
|
||||||
|
39. delivery task 服务迁移到 `.ts`:
|
||||||
|
- `src/services/order/delivery-task-service.ts`
|
||||||
|
40. 订单行、订单商品、履约 profile / requirement、依赖注入、任务上下文、JSON 配置读取已显式类型化
|
||||||
|
41. Docker 内验证通过:
|
||||||
|
- delivery task / 订单 / webhook / open91 相关 4 个测试文件共 18 个用例通过
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run build`
|
||||||
|
- `npm test` 共 128 个用例通过
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
第一批继续推进时,建议按这个顺序:
|
第一批继续推进时,建议按这个顺序:
|
||||||
|
|
||||||
1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块
|
1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块
|
||||||
2. 迁移 delivery task 服务到 `.ts`
|
2. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货
|
||||||
3. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货
|
|
||||||
|
|
||||||
## 执行原则
|
## 执行原则
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user