统一前后端代码格式化配置
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js'
|
||||
import {
|
||||
normalizeDateQuery,
|
||||
normalizePage,
|
||||
normalizePageSize,
|
||||
safeParseJson,
|
||||
} from './admin-query-utils.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
|
||||
export async function writeAdminAuditLog(session: JsonObject | null | undefined, payload: JsonObject = {}) {
|
||||
export async function writeAdminAuditLog(
|
||||
session: JsonObject | null | undefined,
|
||||
payload: JsonObject = {},
|
||||
) {
|
||||
if (!session?.userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -32,10 +32,14 @@ type AdminUserStatus = 'active' | 'disabled'
|
||||
export async function ensureAdminUsersBootstrapped(): Promise<void> {
|
||||
ensureAdminAuthConfigured()
|
||||
|
||||
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : []
|
||||
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers)
|
||||
? runtimeConfig.admin.defaultUsers
|
||||
: []
|
||||
|
||||
for (const configuredUser of configuredUsers) {
|
||||
const username = String(configuredUser?.username || '').trim().toLowerCase()
|
||||
const username = String(configuredUser?.username || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
const password = String(configuredUser?.password || '').trim()
|
||||
const role = normalizeAdminRole(configuredUser?.role)
|
||||
|
||||
@@ -70,7 +74,9 @@ export async function loginAdmin(
|
||||
): Promise<JsonObject> {
|
||||
ensureAdminAuthConfigured()
|
||||
|
||||
const normalizedUsername = String(username || '').trim().toLowerCase()
|
||||
const normalizedUsername = String(username || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
const normalizedPassword = String(password || '').trim()
|
||||
|
||||
if (!normalizedUsername || !normalizedPassword) {
|
||||
@@ -87,7 +93,11 @@ export async function loginAdmin(
|
||||
}
|
||||
|
||||
const user = await getAdminUserByUsername(normalizedUsername)
|
||||
if (!user || user.status !== 'active' || !verifyAdminPassword(normalizedPassword, user.password_hash)) {
|
||||
if (
|
||||
!user ||
|
||||
user.status !== 'active' ||
|
||||
!verifyAdminPassword(normalizedPassword, user.password_hash)
|
||||
) {
|
||||
await recordAdminLoginLog({
|
||||
userId: user ? Number(user.id) : null,
|
||||
username: normalizedUsername,
|
||||
@@ -201,7 +211,10 @@ export async function getAdminSessionSummary(token: unknown): Promise<JsonObject
|
||||
}
|
||||
}
|
||||
|
||||
export function requireAdminRole(session: { role?: string } | null | undefined, allowedRoles: string[]): void {
|
||||
export function requireAdminRole(
|
||||
session: { role?: string } | null | undefined,
|
||||
allowedRoles: string[],
|
||||
): void {
|
||||
if (session && allowedRoles.includes(session.role || '')) {
|
||||
return
|
||||
}
|
||||
@@ -342,7 +355,10 @@ export async function updateManagedAdminUserStatus(
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetManagedAdminUserPassword(userId: number | string, payload: JsonObject = {}): Promise<JsonObject> {
|
||||
export async function resetManagedAdminUserPassword(
|
||||
userId: number | string,
|
||||
payload: JsonObject = {},
|
||||
): Promise<JsonObject> {
|
||||
const user = await getRequiredAdminUser(userId)
|
||||
const password = normalizePassword(payload.password)
|
||||
|
||||
@@ -373,7 +389,9 @@ export async function resetManagedAdminUserPassword(userId: number | string, pay
|
||||
|
||||
export function ensureAdminAuthConfigured(): void {
|
||||
const sessionSecret = String(runtimeConfig.admin?.sessionSecret || '').trim()
|
||||
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : []
|
||||
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers)
|
||||
? runtimeConfig.admin.defaultUsers
|
||||
: []
|
||||
|
||||
if (sessionSecret && configuredUsers.length > 0) {
|
||||
return
|
||||
@@ -435,7 +453,9 @@ function signPayload(encodedPayload: string): string {
|
||||
}
|
||||
|
||||
export function normalizeAdminRole(role: unknown): AdminRole {
|
||||
const normalized = String(role || '').trim().toLowerCase()
|
||||
const normalized = String(role || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
if (normalized === 'admin') {
|
||||
return 'admin'
|
||||
@@ -449,7 +469,11 @@ export function normalizeAdminRole(role: unknown): AdminRole {
|
||||
}
|
||||
|
||||
export function normalizeAdminUserStatus(status: unknown): AdminUserStatus {
|
||||
return String(status || '').trim().toLowerCase() === 'disabled' ? 'disabled' : 'active'
|
||||
return String(status || '')
|
||||
.trim()
|
||||
.toLowerCase() === 'disabled'
|
||||
? 'disabled'
|
||||
: 'active'
|
||||
}
|
||||
|
||||
function safeCompare(input: unknown, expected: unknown): boolean {
|
||||
@@ -464,17 +488,23 @@ function safeCompare(input: unknown, expected: unknown): boolean {
|
||||
}
|
||||
|
||||
function normalizeRoleQuery(role: unknown): string {
|
||||
const normalized = String(role || '').trim().toLowerCase()
|
||||
const normalized = String(role || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return ['admin', 'operator', 'support'].includes(normalized) ? normalized : ''
|
||||
}
|
||||
|
||||
function normalizeStatusQuery(status: unknown): string {
|
||||
const normalized = String(status || '').trim().toLowerCase()
|
||||
const normalized = String(status || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return ['active', 'disabled'].includes(normalized) ? normalized : ''
|
||||
}
|
||||
|
||||
function normalizeUsername(username: unknown): string {
|
||||
return String(username || '').trim().toLowerCase()
|
||||
return String(username || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function normalizePassword(password: unknown): string {
|
||||
@@ -523,7 +553,7 @@ async function getRequiredAdminUser(userId: number | string): Promise<AdminUserR
|
||||
|
||||
async function ensureAdminUserChangeAllowed(
|
||||
user: AdminUserRow,
|
||||
options: { nextRole?: AdminRole, nextStatus?: AdminUserStatus } = {},
|
||||
options: { nextRole?: AdminRole; nextStatus?: AdminUserStatus } = {},
|
||||
session: AdminSession,
|
||||
): Promise<void> {
|
||||
const nextRole = options.nextRole || user.role
|
||||
@@ -536,7 +566,11 @@ async function ensureAdminUserChangeAllowed(
|
||||
})
|
||||
}
|
||||
|
||||
if (user.role === 'admin' && (nextRole !== 'admin' || nextStatus !== 'active') && await countActiveAdminUsers() <= 1) {
|
||||
if (
|
||||
user.role === 'admin' &&
|
||||
(nextRole !== 'admin' || nextStatus !== 'active') &&
|
||||
(await countActiveAdminUsers()) <= 1
|
||||
) {
|
||||
throw createHttpError('至少保留一个启用中的管理员账号', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_user_last_admin_not_allowed',
|
||||
@@ -555,11 +589,13 @@ function mapAdminUser(user: AdminUserRow): JsonObject {
|
||||
}
|
||||
}
|
||||
|
||||
function pickLoginMeta(meta: {
|
||||
ip?: string
|
||||
userAgent?: string
|
||||
location?: string
|
||||
} = {}) {
|
||||
function pickLoginMeta(
|
||||
meta: {
|
||||
ip?: string
|
||||
userAgent?: string
|
||||
location?: string
|
||||
} = {},
|
||||
) {
|
||||
const result: {
|
||||
ip?: string
|
||||
userAgent?: string
|
||||
|
||||
@@ -11,15 +11,8 @@ export async function getAdminDashboardSummary() {
|
||||
TASK_STATUS.PENDING_BINDING_PREPARE,
|
||||
TASK_STATUS.WAITING_BINDING,
|
||||
]
|
||||
const claimingStatuses = [
|
||||
TASK_STATUS.CLAIMED,
|
||||
TASK_STATUS.ROLE_CONFIRMED,
|
||||
TASK_STATUS.REDEEMING,
|
||||
]
|
||||
const abnormalStatuses = [
|
||||
TASK_STATUS.RETRY_PENDING,
|
||||
TASK_STATUS.MANUAL_REVIEW,
|
||||
]
|
||||
const claimingStatuses = [TASK_STATUS.CLAIMED, TASK_STATUS.ROLE_CONFIRMED, TASK_STATUS.REDEEMING]
|
||||
const abnormalStatuses = [TASK_STATUS.RETRY_PENDING, TASK_STATUS.MANUAL_REVIEW]
|
||||
const result = await query(
|
||||
`
|
||||
SELECT
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import type { Request } from 'express'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
|
||||
import {
|
||||
createAdminLoginLog,
|
||||
listAdminLoginLogs,
|
||||
} from '../../repositories/admin-login-log-repo.js'
|
||||
import { createAdminLoginLog, listAdminLoginLogs } from '../../repositories/admin-login-log-repo.js'
|
||||
import { logWarn } from '../../utils/logger.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import {
|
||||
normalizeDateQuery,
|
||||
normalizePage,
|
||||
normalizePageSize,
|
||||
} from './admin-query-utils.js'
|
||||
import { normalizeDateQuery, normalizePage, normalizePageSize } from './admin-query-utils.js'
|
||||
|
||||
type RecordAdminLoginInput = {
|
||||
userId?: number | null
|
||||
@@ -108,16 +101,8 @@ export function resolveClientLocation(req: Request) {
|
||||
'cloudfront-viewer-country',
|
||||
'x-country-code',
|
||||
])
|
||||
const city = firstHeader(req, [
|
||||
'cf-ipcity',
|
||||
'x-vercel-ip-city',
|
||||
'x-city',
|
||||
])
|
||||
const region = firstHeader(req, [
|
||||
'cf-region',
|
||||
'x-vercel-ip-country-region',
|
||||
'x-region',
|
||||
])
|
||||
const city = firstHeader(req, ['cf-ipcity', 'x-vercel-ip-city', 'x-city'])
|
||||
const region = firstHeader(req, ['cf-region', 'x-vercel-ip-country-region', 'x-region'])
|
||||
|
||||
return [country, region, city].filter(Boolean).join(' · ')
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ export async function mapAdminOrderListItem(item: OrderListRow): Promise<AdminOr
|
||||
createdAt: item.created_at,
|
||||
updatedAt: item.updated_at,
|
||||
itemCount: orderItems.length,
|
||||
totalQuantity: orderItems.reduce((sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)), 0),
|
||||
totalQuantity: orderItems.reduce(
|
||||
(sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)),
|
||||
0,
|
||||
),
|
||||
itemSummary,
|
||||
taskCount: Number(item.task_count || tasks.length || 0),
|
||||
resourceStatus: fulfillmentProgress.resourceStatus,
|
||||
@@ -52,8 +55,9 @@ export function summarizeOrderItems(items: OrderItemRow[] | null | undefined): s
|
||||
}
|
||||
|
||||
const [firstItem] = normalizedItems
|
||||
const firstLabel = resolveOrderItemTitle(firstItem)
|
||||
|| String(firstItem?.sku_name || firstItem?.sku_code || '').trim()
|
||||
const firstLabel =
|
||||
resolveOrderItemTitle(firstItem) ||
|
||||
String(firstItem?.sku_name || firstItem?.sku_code || '').trim()
|
||||
|
||||
if (normalizedItems.length === 1) {
|
||||
return firstLabel
|
||||
|
||||
@@ -66,10 +66,7 @@ import type {
|
||||
AdminTaskListQueryInput,
|
||||
AdminViewerSessionInput,
|
||||
} from '../../types/admin/read-inputs.js'
|
||||
import type {
|
||||
KuaishouIndustryVoucherRow,
|
||||
TaskRow,
|
||||
} from '../../types/repository/rows.js'
|
||||
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
export async function getAdminOrders(
|
||||
query: AdminOrderListQueryInput = {},
|
||||
@@ -229,15 +226,16 @@ export async function getAdminTaskDetail(
|
||||
}
|
||||
|
||||
const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task)
|
||||
const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers] = await Promise.all([
|
||||
getOrderById(task.order_id),
|
||||
primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null),
|
||||
listTaskEventsByTaskId(task.id),
|
||||
listKuaishouIndustryVouchersByTaskId(task.id),
|
||||
task.platform_order_id
|
||||
? listKuaishouIndustryVouchersByOid(task.platform_order_id)
|
||||
: Promise.resolve([]),
|
||||
])
|
||||
const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers] =
|
||||
await Promise.all([
|
||||
getOrderById(task.order_id),
|
||||
primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null),
|
||||
listTaskEventsByTaskId(task.id),
|
||||
listKuaishouIndustryVouchersByTaskId(task.id),
|
||||
task.platform_order_id
|
||||
? listKuaishouIndustryVouchersByOid(task.platform_order_id)
|
||||
: Promise.resolve([]),
|
||||
])
|
||||
const orderItems = order ? await listOrderItemsByOrderId(order.id) : []
|
||||
const orderItem = orderItems.find((item) => item.id === task.order_item_id) || null
|
||||
const taskContext = parseTaskContext(task)
|
||||
@@ -252,10 +250,7 @@ export async function getAdminTaskDetail(
|
||||
cloudSourceLabelMap,
|
||||
}) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap })
|
||||
const claimIdentity = buildClaimIdentityAdminSummary(taskContext, {
|
||||
flowLike:
|
||||
taskContext.kuaishouCloudFulfillment ||
|
||||
taskContext.kuaishouFeifei ||
|
||||
null,
|
||||
flowLike: taskContext.kuaishouCloudFulfillment || taskContext.kuaishouFeifei || null,
|
||||
taskRoleId: task.role_id,
|
||||
taskRoleName: task.role_name,
|
||||
})
|
||||
@@ -265,17 +260,20 @@ export async function getAdminTaskDetail(
|
||||
taskKuaishouIndustryVouchers,
|
||||
oidKuaishouIndustryVouchers,
|
||||
)
|
||||
const kuaishouIndustryVoucher =
|
||||
firstKuaishouIndustryVoucher
|
||||
? mapAdminKuaishouIndustryVoucher(firstKuaishouIndustryVoucher)
|
||||
: mapKuaishouIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
|
||||
const kuaishouIndustryVoucher = firstKuaishouIndustryVoucher
|
||||
? mapAdminKuaishouIndustryVoucher(firstKuaishouIndustryVoucher)
|
||||
: mapKuaishouIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
|
||||
const hasKuaishouIndustryVoucher = Boolean(
|
||||
kuaishouIndustryVoucher && String(kuaishouIndustryVoucher.voucherCode || '').trim(),
|
||||
)
|
||||
const kuaishouIndustryVoucherStatus =
|
||||
String(kuaishouIndustryVoucher?.status || '').trim().toUpperCase()
|
||||
const kuaishouIndustryVoucherSendCallbackStatus =
|
||||
String(kuaishouIndustryVoucher?.sendCallbackStatus || '').trim().toLowerCase()
|
||||
const kuaishouIndustryVoucherStatus = String(kuaishouIndustryVoucher?.status || '')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
const kuaishouIndustryVoucherSendCallbackStatus = String(
|
||||
kuaishouIndustryVoucher?.sendCallbackStatus || '',
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
return {
|
||||
task: mapAdminTaskListItem(
|
||||
@@ -435,7 +433,7 @@ function resolveKuaishouIndustryVoucherForTask(
|
||||
taskContext.kuaishouIndustryVoucher &&
|
||||
typeof taskContext.kuaishouIndustryVoucher === 'object' &&
|
||||
!Array.isArray(taskContext.kuaishouIndustryVoucher)
|
||||
? taskContext.kuaishouIndustryVoucher as JsonRecord
|
||||
? (taskContext.kuaishouIndustryVoucher as JsonRecord)
|
||||
: {}
|
||||
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
|
||||
if (voucherCode) {
|
||||
@@ -457,7 +455,7 @@ function resolveKuaishouIndustryVoucherForTask(
|
||||
}
|
||||
}
|
||||
|
||||
return oidVouchers.length === 1 ? (oidVouchers[0] || null) : null
|
||||
return oidVouchers.length === 1 ? oidVouchers[0] || null : null
|
||||
}
|
||||
|
||||
function buildCloudSourceLabelMap() {
|
||||
|
||||
@@ -15,7 +15,10 @@ test('resolveAdminTaskScreenshotUrl lets support view final redeemed screenshot'
|
||||
runtime_session_id: '',
|
||||
}
|
||||
|
||||
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, createAdminViewerContext({ role: 'support' }))
|
||||
const screenshotUrl = await resolveAdminTaskScreenshotUrl(
|
||||
task,
|
||||
createAdminViewerContext({ role: 'support' }),
|
||||
)
|
||||
|
||||
assert.equal(screenshotUrl, '/api/v1/admin/tasks/12/screenshot')
|
||||
})
|
||||
@@ -27,7 +30,10 @@ test('resolveAdminTaskScreenshotUrl falls back to review screenshot when runtime
|
||||
runtime_session_id: 'runtime-session-13',
|
||||
}
|
||||
|
||||
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, createAdminViewerContext({ role: 'support' }))
|
||||
const screenshotUrl = await resolveAdminTaskScreenshotUrl(
|
||||
task,
|
||||
createAdminViewerContext({ role: 'support' }),
|
||||
)
|
||||
|
||||
assert.equal(screenshotUrl, '/api/v1/admin/tasks/13/screenshot')
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { safeParseJson } from './admin-query-utils.js'
|
||||
import { normalizeAdminRole } from './admin-auth-service.js'
|
||||
import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js'
|
||||
import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js'
|
||||
import {
|
||||
parseTaskContext as parseTaskContextValue,
|
||||
parseTaskState as parseTaskStateValue,
|
||||
@@ -79,8 +79,7 @@ export function mapKuaishouCloudFulfillmentContext(
|
||||
const role = asJsonObject(record.role)
|
||||
const purchase = asJsonObject(record.purchase)
|
||||
const dispatch = asJsonObject(record.dispatch)
|
||||
const returnNumber =
|
||||
asJsonObject(record.returnNumber)
|
||||
const returnNumber = asJsonObject(record.returnNumber)
|
||||
const consume = asJsonObject(record.consume)
|
||||
const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys)
|
||||
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
|
||||
@@ -373,7 +372,8 @@ export function createAdminViewerContext(
|
||||
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
|
||||
canManageTaskLifecycle: role === 'admin' || role === 'operator',
|
||||
canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support',
|
||||
canOperateKuaishouIndustryVoucher: role === 'admin' || role === 'operator' || role === 'support',
|
||||
canOperateKuaishouIndustryVoucher:
|
||||
role === 'admin' || role === 'operator' || role === 'support',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getTaskById } from '../../repositories/task-repo.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { safeParseJson } from './admin-query-utils.js'
|
||||
import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js'
|
||||
import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js'
|
||||
import {
|
||||
TASK_STATUS,
|
||||
isTaskFulfillmentCompletedStatus,
|
||||
@@ -17,10 +17,7 @@ import type {
|
||||
AdminTaskListItem,
|
||||
} from '../../types/admin/read-models.js'
|
||||
import type { AdminTaskActionPayload } from '../../types/admin/write-models.js'
|
||||
import type {
|
||||
TaskEventRow,
|
||||
TaskRow,
|
||||
} from '../../types/repository/rows.js'
|
||||
import type { TaskEventRow, TaskRow } from '../../types/repository/rows.js'
|
||||
import type { AdminViewerContext } from './admin-read-shared-helpers.js'
|
||||
|
||||
type TaskFulfillmentState = {
|
||||
@@ -28,9 +25,7 @@ type TaskFulfillmentState = {
|
||||
customerStatus: string
|
||||
}
|
||||
|
||||
export function mapAdminTaskSummary(
|
||||
task: TaskRow,
|
||||
): JsonRecord {
|
||||
export function mapAdminTaskSummary(task: TaskRow): JsonRecord {
|
||||
const fulfillment = buildTaskFulfillmentState(task)
|
||||
|
||||
return {
|
||||
@@ -111,8 +106,10 @@ export function mapAdminTaskListItem(
|
||||
lastError: task.last_error,
|
||||
createdAt: task.created_at,
|
||||
updatedAt: task.updated_at,
|
||||
claimToken: viewerContext.canViewSensitiveTaskData ? (task.primary_claim_token || task.claim_token || '') : '',
|
||||
screenshotPath: viewerContext.role === 'support' ? '' : (task.screenshot_path || ''),
|
||||
claimToken: viewerContext.canViewSensitiveTaskData
|
||||
? task.primary_claim_token || task.claim_token || ''
|
||||
: '',
|
||||
screenshotPath: viewerContext.role === 'support' ? '' : task.screenshot_path || '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +131,9 @@ export function buildOrderFulfillmentProgress(
|
||||
const totalTaskCount = normalizedTasks.length
|
||||
const taskFulfillments = normalizedTasks.map((task) => buildTaskFulfillmentState(task))
|
||||
const preparedTaskCount = normalizedTasks.filter((task) => isTaskResourcePrepared(task)).length
|
||||
const completedTaskCount = normalizedTasks.filter((task) => isTaskFulfillmentCompleted(task)).length
|
||||
const completedTaskCount = normalizedTasks.filter((task) =>
|
||||
isTaskFulfillmentCompleted(task),
|
||||
).length
|
||||
|
||||
let resourceStatus = 'pending_prepare'
|
||||
let customerStatus = 'not_started'
|
||||
@@ -152,7 +151,11 @@ export function buildOrderFulfillmentProgress(
|
||||
if (normalizedTasks.every((task) => isTaskFulfillmentCompleted(task))) {
|
||||
resourceStatus = 'resource_ready'
|
||||
customerStatus = 'customer_completed'
|
||||
} else if (taskFulfillments.some((item) => ['customer_processing', 'customer_confirmed', 'link_opened'].includes(item.customerStatus))) {
|
||||
} else if (
|
||||
taskFulfillments.some((item) =>
|
||||
['customer_processing', 'customer_confirmed', 'link_opened'].includes(item.customerStatus),
|
||||
)
|
||||
) {
|
||||
resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare'
|
||||
customerStatus = 'customer_processing'
|
||||
} else if (taskFulfillments.some((item) => item.customerStatus === 'waiting_customer')) {
|
||||
@@ -163,7 +166,11 @@ export function buildOrderFulfillmentProgress(
|
||||
customerStatus = 'customer_exception'
|
||||
} else if (preparedTaskCount > 0) {
|
||||
resourceStatus = 'resource_ready'
|
||||
} else if (taskFulfillments.some((item) => ['manual_review', 'retry_pending'].includes(item.resourceStatus))) {
|
||||
} else if (
|
||||
taskFulfillments.some((item) =>
|
||||
['manual_review', 'retry_pending'].includes(item.resourceStatus),
|
||||
)
|
||||
) {
|
||||
resourceStatus = 'resource_exception'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { getAffiliateDashConfig } from '../../platforms/affiliate-dash/config.js'
|
||||
import type { JsonObject } from '../../../types/json.js'
|
||||
import {
|
||||
getAffiliateDashWallet,
|
||||
} from '../../platforms/affiliate-dash/order-service.js'
|
||||
import { getAffiliateDashWallet } from '../../platforms/affiliate-dash/order-service.js'
|
||||
import { listAffiliateDashProducts } from '../../platforms/affiliate-dash/product-service.js'
|
||||
import {
|
||||
getAffiliateDashSourceConfig,
|
||||
@@ -35,9 +33,7 @@ export async function updateAdminAffiliateDashConfig(payload: JsonObject = {}) {
|
||||
export function matchAdminAffiliateDashSku(payload: JsonObject = {}) {
|
||||
const productNo = String(payload.productNo || payload.product_no || '').trim()
|
||||
const source = getAdminEditableAffiliateDashConfig()
|
||||
const sku = productNo
|
||||
? String(source.skuMapping[productNo] || '').trim()
|
||||
: ''
|
||||
const sku = productNo ? String(source.skuMapping[productNo] || '').trim() : ''
|
||||
|
||||
return {
|
||||
productNo,
|
||||
|
||||
@@ -52,7 +52,13 @@ test('resolveCloudtentaclesAdminContext merges payload source and persisted sess
|
||||
test('hasCloudtentaclesCredentialContextChanged detects normalized credential changes', () => {
|
||||
assert.equal(
|
||||
hasCloudtentaclesCredentialContextChanged(
|
||||
{ baseUrl: ' https://a ', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
|
||||
{
|
||||
baseUrl: ' https://a ',
|
||||
username: 'u1',
|
||||
phone: '13812345678',
|
||||
deviceId: 'd1',
|
||||
deviceType: 1,
|
||||
},
|
||||
{ baseUrl: 'https://a', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
|
||||
),
|
||||
false,
|
||||
|
||||
@@ -1,70 +1,57 @@
|
||||
export function pickFirstNonEmpty(values: unknown[]) {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || "").trim();
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
|
||||
export function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
|
||||
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
|
||||
import { getCloudtentaclesSourceByKey } from '../../../platforms/cloudtentacles/source-config-service.js'
|
||||
import { getCloudtentaclesSessionStateByKey } from '../../../platforms/cloudtentacles/session-state-service.js'
|
||||
import type { JsonObject } from '../../../../types/json.js'
|
||||
import {
|
||||
normalizeCloudtentaclesDeviceId,
|
||||
normalizeCloudtentaclesDeviceType,
|
||||
} from "../../../platforms/cloudtentacles/defaults.js";
|
||||
} from '../../../platforms/cloudtentacles/defaults.js'
|
||||
|
||||
export function resolveCloudtentaclesAdminContext(payload: JsonObject = {}, options: JsonObject = {}) {
|
||||
const sourceKey = String(
|
||||
payload.sourceKey || options.sourceKey || "default"
|
||||
).trim();
|
||||
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {};
|
||||
const persistedSession = options.persistedSession ||
|
||||
getCloudtentaclesSessionStateByKey(sourceKey) ||
|
||||
{};
|
||||
const defaultBaseUrl = String(
|
||||
options.defaultBaseUrl || "https://123.207.217.176"
|
||||
).trim();
|
||||
export function resolveCloudtentaclesAdminContext(
|
||||
payload: JsonObject = {},
|
||||
options: JsonObject = {},
|
||||
) {
|
||||
const sourceKey = String(payload.sourceKey || options.sourceKey || 'default').trim()
|
||||
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
|
||||
const persistedSession =
|
||||
options.persistedSession || getCloudtentaclesSessionStateByKey(sourceKey) || {}
|
||||
const defaultBaseUrl = String(options.defaultBaseUrl || 'https://123.207.217.176').trim()
|
||||
return {
|
||||
sourceKey,
|
||||
baseUrl: pickFirstNonEmpty([
|
||||
payload.baseUrl,
|
||||
savedSource.baseUrl,
|
||||
defaultBaseUrl,
|
||||
]),
|
||||
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
|
||||
token: pickFirstNonEmpty([payload.token, persistedSession.token]),
|
||||
deviceId: normalizeCloudtentaclesDeviceId(
|
||||
pickFirstNonEmpty([
|
||||
payload.deviceId,
|
||||
savedSource.deviceId,
|
||||
persistedSession.deviceId,
|
||||
])
|
||||
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, persistedSession.deviceId]),
|
||||
),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(
|
||||
payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType
|
||||
payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function hasCloudtentaclesCredentialContextChanged(
|
||||
current: JsonObject = {},
|
||||
next: JsonObject = {}
|
||||
next: JsonObject = {},
|
||||
) {
|
||||
return (
|
||||
String(current.baseUrl || "").trim() !==
|
||||
String(next.baseUrl || "").trim() ||
|
||||
String(current.username || "").trim() !==
|
||||
String(next.username || "").trim() ||
|
||||
String(current.phone || "").trim() !== String(next.phone || "").trim() ||
|
||||
String(current.deviceId || "").trim() !==
|
||||
String(next.deviceId || "").trim() ||
|
||||
String(current.baseUrl || '').trim() !== String(next.baseUrl || '').trim() ||
|
||||
String(current.username || '').trim() !== String(next.username || '').trim() ||
|
||||
String(current.phone || '').trim() !== String(next.phone || '').trim() ||
|
||||
String(current.deviceId || '').trim() !== String(next.deviceId || '').trim() ||
|
||||
Number(current.deviceType || 0) !== Number(next.deviceType || 0)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,7 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
mapAdminCloudtentaclesSession,
|
||||
maskPhone,
|
||||
maskSecret,
|
||||
} from './mappers.js'
|
||||
import { mapAdminCloudtentaclesSession, maskPhone, maskSecret } from './mappers.js'
|
||||
|
||||
test('maskSecret preserves edges while hiding middle characters', () => {
|
||||
assert.equal(maskSecret('abcdef1234567890'), 'abcdef****567890')
|
||||
|
||||
@@ -2,44 +2,44 @@ import type { JsonObject } from '../../../../types/json.js'
|
||||
import {
|
||||
maskPhone as maskPhoneValue,
|
||||
maskSecret as maskSecretValue,
|
||||
} from "../../../../utils/masking.js";
|
||||
} from '../../../../utils/masking.js'
|
||||
import {
|
||||
normalizeCloudtentaclesDeviceId,
|
||||
normalizeCloudtentaclesDeviceType,
|
||||
} from "../../../platforms/cloudtentacles/defaults.js";
|
||||
} from '../../../platforms/cloudtentacles/defaults.js'
|
||||
|
||||
export function maskSecret(value: unknown) {
|
||||
return maskSecretValue(value);
|
||||
return maskSecretValue(value)
|
||||
}
|
||||
|
||||
export function maskPhone(value: unknown) {
|
||||
return maskPhoneValue(value, { maskShort: false });
|
||||
return maskPhoneValue(value, { maskShort: false })
|
||||
}
|
||||
|
||||
export function mapAdminCloudtentaclesSourceConfig(config: JsonObject = {}) {
|
||||
return {
|
||||
key: String(config.key || "").trim(),
|
||||
label: String(config.label || "").trim(),
|
||||
key: String(config.key || '').trim(),
|
||||
label: String(config.label || '').trim(),
|
||||
enabled: config.enabled !== false,
|
||||
baseUrl: String(config.baseUrl || "").trim(),
|
||||
username: String(config.username || "").trim(),
|
||||
password: String(config.password || "").trim(),
|
||||
phone: String(config.phone || "").trim(),
|
||||
baseUrl: String(config.baseUrl || '').trim(),
|
||||
username: String(config.username || '').trim(),
|
||||
password: String(config.password || '').trim(),
|
||||
phone: String(config.phone || '').trim(),
|
||||
deviceId: normalizeCloudtentaclesDeviceId(config.deviceId),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(config.deviceType),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function mapAdminCloudtentaclesSession(session: JsonObject = {}) {
|
||||
return {
|
||||
token: String(session.token || "").trim(),
|
||||
token: String(session.token || '').trim(),
|
||||
tokenMasked: maskSecret(session.token),
|
||||
baseUrl: String(session.baseUrl || "").trim(),
|
||||
username: String(session.username || "").trim(),
|
||||
baseUrl: String(session.baseUrl || '').trim(),
|
||||
username: String(session.username || '').trim(),
|
||||
phoneMasked: maskPhone(session.phone),
|
||||
loggedInAt: String(session.loggedInAt || "").trim(),
|
||||
loggedInAt: String(session.loggedInAt || '').trim(),
|
||||
deviceId: normalizeCloudtentaclesDeviceId(session.deviceId),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(session.deviceType),
|
||||
hasToken: Boolean(String(session.token || "").trim()),
|
||||
};
|
||||
hasToken: Boolean(String(session.token || '').trim()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,80 @@
|
||||
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
|
||||
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
|
||||
import { getCloudtentaclesSourceByKey } from '../../../platforms/cloudtentacles/source-config-service.js'
|
||||
import { getCloudtentaclesSessionStateByKey } from '../../../platforms/cloudtentacles/session-state-service.js'
|
||||
import type { JsonObject } from '../../../../types/json.js'
|
||||
import {
|
||||
normalizeCloudtentaclesDeviceId,
|
||||
normalizeCloudtentaclesDeviceType,
|
||||
} from "../../../platforms/cloudtentacles/defaults.js";
|
||||
import {
|
||||
pickFirstNonEmpty,
|
||||
resolveCloudtentaclesAdminContext,
|
||||
} from "./context.js";
|
||||
import { maskPhone, maskSecret } from "./mappers.js";
|
||||
} from '../../../platforms/cloudtentacles/defaults.js'
|
||||
import { pickFirstNonEmpty, resolveCloudtentaclesAdminContext } from './context.js'
|
||||
import { maskPhone, maskSecret } from './mappers.js'
|
||||
|
||||
const DEFAULT_CLOUDTENTACLES_BASE_URL = "https://123.207.217.176";
|
||||
const DEFAULT_CLOUDTENTACLES_BASE_URL = 'https://123.207.217.176'
|
||||
|
||||
function _resolveSourceKey(payload: JsonObject = {}) {
|
||||
return String(payload.sourceKey || "").trim() || "default";
|
||||
return String(payload.sourceKey || '').trim() || 'default'
|
||||
}
|
||||
|
||||
export function resolveAdminCloudtentaclesCredentialPayload(
|
||||
payload: JsonObject = {},
|
||||
options: JsonObject = {}
|
||||
options: JsonObject = {},
|
||||
) {
|
||||
const sourceKey = _resolveSourceKey(payload);
|
||||
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {};
|
||||
const defaultBaseUrl = String(
|
||||
options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL
|
||||
).trim();
|
||||
const sourceKey = _resolveSourceKey(payload)
|
||||
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
|
||||
const defaultBaseUrl = String(options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL).trim()
|
||||
|
||||
return {
|
||||
sourceKey,
|
||||
baseUrl: pickFirstNonEmpty([
|
||||
payload.baseUrl,
|
||||
savedSource.baseUrl,
|
||||
defaultBaseUrl,
|
||||
]),
|
||||
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
|
||||
username: pickFirstNonEmpty([payload.username, savedSource.username]),
|
||||
password: pickFirstNonEmpty([payload.password, savedSource.password]),
|
||||
phone: pickFirstNonEmpty([payload.phone, savedSource.phone]),
|
||||
deviceId: normalizeCloudtentaclesDeviceId(
|
||||
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId])
|
||||
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId]),
|
||||
),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(
|
||||
payload.deviceType ?? savedSource.deviceType
|
||||
),
|
||||
};
|
||||
deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType ?? savedSource.deviceType),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveAdminCloudtentaclesSessionPayload(
|
||||
payload: JsonObject = {},
|
||||
options: JsonObject = {}
|
||||
options: JsonObject = {},
|
||||
) {
|
||||
const sourceKey = _resolveSourceKey(payload);
|
||||
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {};
|
||||
const persistedSession = options.persistedSession ||
|
||||
getCloudtentaclesSessionStateByKey(sourceKey) ||
|
||||
{};
|
||||
const sourceKey = _resolveSourceKey(payload)
|
||||
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
|
||||
const persistedSession =
|
||||
options.persistedSession || getCloudtentaclesSessionStateByKey(sourceKey) || {}
|
||||
|
||||
return resolveCloudtentaclesAdminContext(payload, {
|
||||
savedSource,
|
||||
persistedSession,
|
||||
sourceKey,
|
||||
defaultBaseUrl: options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
export function buildAdminCloudtentaclesPersistedSessionPayload(
|
||||
session: JsonObject = {},
|
||||
options: JsonObject = {}
|
||||
options: JsonObject = {},
|
||||
) {
|
||||
return {
|
||||
token: String(session.token || "").trim(),
|
||||
baseUrl: String(session.baseUrl || "").trim(),
|
||||
token: String(session.token || '').trim(),
|
||||
baseUrl: String(session.baseUrl || '').trim(),
|
||||
username: pickFirstNonEmpty([options.username, session.username]),
|
||||
phone: pickFirstNonEmpty([options.phone, session.phone]),
|
||||
loggedInAt: String(session.loggedInAt || "").trim(),
|
||||
loggedInAt: String(session.loggedInAt || '').trim(),
|
||||
deviceId: normalizeCloudtentaclesDeviceId(
|
||||
pickFirstNonEmpty([options.deviceId, session.deviceId])
|
||||
pickFirstNonEmpty([options.deviceId, session.deviceId]),
|
||||
),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(
|
||||
options.deviceType ?? session.deviceType
|
||||
),
|
||||
};
|
||||
deviceType: normalizeCloudtentaclesDeviceType(options.deviceType ?? session.deviceType),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAdminCloudtentaclesSessionSummary(
|
||||
session: JsonObject = {},
|
||||
savedSession: JsonObject = {},
|
||||
sourceKey = "default"
|
||||
sourceKey = 'default',
|
||||
) {
|
||||
const permissions = Array.isArray(session.permissions)
|
||||
? session.permissions
|
||||
: [];
|
||||
const permissions = Array.isArray(session.permissions) ? session.permissions : []
|
||||
|
||||
return {
|
||||
sourceKey,
|
||||
@@ -98,47 +82,39 @@ export function buildAdminCloudtentaclesSessionSummary(
|
||||
permissionCount: permissions.length,
|
||||
permissions,
|
||||
persisted: Boolean(savedSession.token),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAdminCloudtentaclesLoginResult(
|
||||
session: JsonObject = {},
|
||||
savedSession: JsonObject = {},
|
||||
sourceKey = "default"
|
||||
sourceKey = 'default',
|
||||
) {
|
||||
return {
|
||||
sourceKey,
|
||||
baseUrl: String(session.baseUrl || "").trim(),
|
||||
username: String(session.username || "").trim(),
|
||||
baseUrl: String(session.baseUrl || '').trim(),
|
||||
username: String(session.username || '').trim(),
|
||||
phoneMasked: maskPhone(session.phone),
|
||||
loggedInAt: String(session.loggedInAt || "").trim(),
|
||||
responseMessage: String(session.responseMessage || "").trim(),
|
||||
token: String(session.token || "").trim(),
|
||||
session: buildAdminCloudtentaclesSessionSummary(
|
||||
session,
|
||||
savedSession,
|
||||
sourceKey
|
||||
),
|
||||
loggedInAt: String(session.loggedInAt || '').trim(),
|
||||
responseMessage: String(session.responseMessage || '').trim(),
|
||||
token: String(session.token || '').trim(),
|
||||
session: buildAdminCloudtentaclesSessionSummary(session, savedSession, sourceKey),
|
||||
userInfo: session.userInfo,
|
||||
asset: session.asset,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAdminCloudtentaclesValidateResult(
|
||||
session: JsonObject = {},
|
||||
savedSession: JsonObject = {},
|
||||
sourceKey = "default"
|
||||
sourceKey = 'default',
|
||||
) {
|
||||
return {
|
||||
sourceKey,
|
||||
baseUrl: String(session.baseUrl || "").trim(),
|
||||
loggedInAt: String(session.loggedInAt || "").trim(),
|
||||
session: buildAdminCloudtentaclesSessionSummary(
|
||||
session,
|
||||
savedSession,
|
||||
sourceKey
|
||||
),
|
||||
baseUrl: String(session.baseUrl || '').trim(),
|
||||
loggedInAt: String(session.loggedInAt || '').trim(),
|
||||
session: buildAdminCloudtentaclesSessionSummary(session, savedSession, sourceKey),
|
||||
userInfo: session.userInfo,
|
||||
asset: session.asset,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,19 @@ import type { JsonObject } from '../../../../types/json.js'
|
||||
import {
|
||||
normalizeCloudtentaclesDeviceId,
|
||||
normalizeCloudtentaclesDeviceType,
|
||||
} from "../../../platforms/cloudtentacles/defaults.js";
|
||||
} from '../../../platforms/cloudtentacles/defaults.js'
|
||||
|
||||
export function normalizeAdminCloudtentaclesSourceConfigPayload(
|
||||
payload: JsonObject = {}
|
||||
) {
|
||||
const sourceKey =
|
||||
String(payload.sourceKey || payload.key || "").trim() || "default";
|
||||
export function normalizeAdminCloudtentaclesSourceConfigPayload(payload: JsonObject = {}) {
|
||||
const sourceKey = String(payload.sourceKey || payload.key || '').trim() || 'default'
|
||||
return {
|
||||
key: sourceKey,
|
||||
label: String(payload.label || "").trim(),
|
||||
label: String(payload.label || '').trim(),
|
||||
enabled: payload.enabled !== false,
|
||||
baseUrl: String(payload.baseUrl || "").trim() || "https://123.207.217.176",
|
||||
username: String(payload.username || "").trim(),
|
||||
password: String(payload.password || "").trim(),
|
||||
phone: String(payload.phone || "").trim(),
|
||||
baseUrl: String(payload.baseUrl || '').trim() || 'https://123.207.217.176',
|
||||
username: String(payload.username || '').trim(),
|
||||
password: String(payload.password || '').trim(),
|
||||
phone: String(payload.phone || '').trim(),
|
||||
deviceId: normalizeCloudtentaclesDeviceId(payload.deviceId),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +60,18 @@ export async function listAdminKuaishouFeifeiProducts(payload: JsonObject = {})
|
||||
page: Number(payload.page || 1) || 1,
|
||||
perPage: Number(payload.perPage || payload.per_page || 20) || 20,
|
||||
status: String(payload.status || 'on_sale').trim(),
|
||||
supplyProductName: String(payload.supplyProductName || payload.supply_product_name || '').trim(),
|
||||
supplyProductName: String(
|
||||
payload.supplyProductName || payload.supply_product_name || '',
|
||||
).trim(),
|
||||
})
|
||||
}
|
||||
|
||||
export async function syncAdminKuaishouFeifeiProductRules(payload: JsonObject = {}) {
|
||||
const source = getAdminEditableKuaishouFeifeiConfig()
|
||||
const status = String(payload.status || 'on_sale').trim() || 'on_sale'
|
||||
const supplyProductName = String(payload.supplyProductName || payload.supply_product_name || '').trim()
|
||||
const supplyProductName = String(
|
||||
payload.supplyProductName || payload.supply_product_name || '',
|
||||
).trim()
|
||||
const products = await listAllKuaishouFeifeiProducts({
|
||||
status,
|
||||
supplyProductName,
|
||||
@@ -185,10 +189,7 @@ function mapEffectiveKuaishouFeifeiConfig(config: ReturnType<typeof getKuaishouF
|
||||
}
|
||||
}
|
||||
|
||||
async function listAllKuaishouFeifeiProducts(input: {
|
||||
status: string
|
||||
supplyProductName: string
|
||||
}) {
|
||||
async function listAllKuaishouFeifeiProducts(input: { status: string; supplyProductName: string }) {
|
||||
const perPage = 100
|
||||
const firstPage = await listKuaishouFeifeiProducts({
|
||||
page: 1,
|
||||
|
||||
@@ -14,16 +14,9 @@ import {
|
||||
refreshKuaishouIndustryAccessToken,
|
||||
} from '../../platforms/kuaishou-industry/token-service.js'
|
||||
|
||||
const SECRET_FIELDS = [
|
||||
'appSecret',
|
||||
'signSecret',
|
||||
'messageSecret',
|
||||
] as const
|
||||
const SECRET_FIELDS = ['appSecret', 'signSecret', 'messageSecret'] as const
|
||||
|
||||
const SHOP_SECRET_FIELDS = [
|
||||
'accessToken',
|
||||
'refreshToken',
|
||||
] as const
|
||||
const SHOP_SECRET_FIELDS = ['accessToken', 'refreshToken'] as const
|
||||
|
||||
export function getAdminKuaishouIndustrySourceConfig() {
|
||||
const config = getKuaishouIndustrySourceConfig()
|
||||
@@ -38,15 +31,23 @@ export async function updateAdminKuaishouIndustrySourceConfig(payload: JsonObjec
|
||||
const current = getKuaishouIndustrySourceConfig()
|
||||
const saved = await saveKuaishouIndustrySourceConfig({
|
||||
...current,
|
||||
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current.enabled !== false,
|
||||
enabled: hasPayloadField(payload, 'enabled')
|
||||
? payload.enabled !== false
|
||||
: current.enabled !== false,
|
||||
baseUrl: readConfigString(payload, 'baseUrl', current.baseUrl),
|
||||
authBaseUrl: readConfigString(payload, 'authBaseUrl', current.authBaseUrl),
|
||||
redirectUri: readConfigString(payload, 'redirectUri', current.redirectUri, { allowBlank: true }),
|
||||
scopes: normalizeScopeText(readConfigString(payload, 'scopes', current.scopes, { allowBlank: true })),
|
||||
redirectUri: readConfigString(payload, 'redirectUri', current.redirectUri, {
|
||||
allowBlank: true,
|
||||
}),
|
||||
scopes: normalizeScopeText(
|
||||
readConfigString(payload, 'scopes', current.scopes, { allowBlank: true }),
|
||||
),
|
||||
authState: readConfigString(payload, 'authState', current.authState, { allowBlank: true }),
|
||||
appKey: readConfigString(payload, 'appKey', current.appKey, { allowBlank: true }),
|
||||
openId: readConfigString(payload, 'openId', current.openId, { allowBlank: true }),
|
||||
grantedScopes: normalizeScopeText(readConfigString(payload, 'grantedScopes', current.grantedScopes, { allowBlank: true })),
|
||||
grantedScopes: normalizeScopeText(
|
||||
readConfigString(payload, 'grantedScopes', current.grantedScopes, { allowBlank: true }),
|
||||
),
|
||||
sellerId: readConfigString(payload, 'sellerId', current.sellerId, { allowBlank: true }),
|
||||
provider: readConfigString(payload, 'provider', current.provider),
|
||||
platform: readConfigString(payload, 'platform', current.platform),
|
||||
@@ -245,33 +246,77 @@ function normalizeShopConfigPayload(
|
||||
|
||||
const payload = rawShop as JsonObject
|
||||
const current = resolveCurrentShopConfig(payload, index, currentShops)
|
||||
const sellerId = readConfigString(payload, 'sellerId', current?.sellerId || '', { allowBlank: true })
|
||||
const shopId = readConfigString(payload, 'shopId', current?.shopId || sellerId, { allowBlank: true }) || sellerId
|
||||
const shopName = readConfigString(payload, 'shopName', current?.shopName || '', { allowBlank: true })
|
||||
const customShopName = readConfigString(payload, 'customShopName', current?.customShopName || '', { allowBlank: true })
|
||||
const sellerId = readConfigString(payload, 'sellerId', current?.sellerId || '', {
|
||||
allowBlank: true,
|
||||
})
|
||||
const shopId =
|
||||
readConfigString(payload, 'shopId', current?.shopId || sellerId, { allowBlank: true }) ||
|
||||
sellerId
|
||||
const shopName = readConfigString(payload, 'shopName', current?.shopName || '', {
|
||||
allowBlank: true,
|
||||
})
|
||||
const customShopName = readConfigString(
|
||||
payload,
|
||||
'customShopName',
|
||||
current?.customShopName || '',
|
||||
{ allowBlank: true },
|
||||
)
|
||||
const accessToken = readSecretString(payload, 'accessToken', current?.accessToken || '')
|
||||
const refreshToken = readSecretString(payload, 'refreshToken', current?.refreshToken || '')
|
||||
const openId = readConfigString(payload, 'openId', current?.openId || '', { allowBlank: true })
|
||||
|
||||
if (!sellerId && !shopId && !shopName && !customShopName && !accessToken && !refreshToken && !openId) {
|
||||
if (
|
||||
!sellerId &&
|
||||
!shopId &&
|
||||
!shopName &&
|
||||
!customShopName &&
|
||||
!accessToken &&
|
||||
!refreshToken &&
|
||||
!openId
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current?.enabled !== false,
|
||||
enabled: hasPayloadField(payload, 'enabled')
|
||||
? payload.enabled !== false
|
||||
: current?.enabled !== false,
|
||||
sellerId,
|
||||
shopId,
|
||||
shopName,
|
||||
customShopName,
|
||||
authState: readConfigString(payload, 'authState', current?.authState || '', { allowBlank: true }),
|
||||
authState: readConfigString(payload, 'authState', current?.authState || '', {
|
||||
allowBlank: true,
|
||||
}),
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accessTokenExpiresAt: readConfigString(payload, 'accessTokenExpiresAt', current?.accessTokenExpiresAt || '', { allowBlank: true }),
|
||||
refreshTokenExpiresAt: readConfigString(payload, 'refreshTokenExpiresAt', current?.refreshTokenExpiresAt || '', { allowBlank: true }),
|
||||
accessTokenExpiresAt: readConfigString(
|
||||
payload,
|
||||
'accessTokenExpiresAt',
|
||||
current?.accessTokenExpiresAt || '',
|
||||
{ allowBlank: true },
|
||||
),
|
||||
refreshTokenExpiresAt: readConfigString(
|
||||
payload,
|
||||
'refreshTokenExpiresAt',
|
||||
current?.refreshTokenExpiresAt || '',
|
||||
{ allowBlank: true },
|
||||
),
|
||||
openId,
|
||||
grantedScopes: normalizeScopeText(readConfigString(payload, 'grantedScopes', current?.grantedScopes || '', { allowBlank: true })),
|
||||
lastRefreshedAt: readConfigString(payload, 'lastRefreshedAt', current?.lastRefreshedAt || '', { allowBlank: true }),
|
||||
lastRefreshError: readConfigString(payload, 'lastRefreshError', current?.lastRefreshError || '', { allowBlank: true }),
|
||||
grantedScopes: normalizeScopeText(
|
||||
readConfigString(payload, 'grantedScopes', current?.grantedScopes || '', {
|
||||
allowBlank: true,
|
||||
}),
|
||||
),
|
||||
lastRefreshedAt: readConfigString(payload, 'lastRefreshedAt', current?.lastRefreshedAt || '', {
|
||||
allowBlank: true,
|
||||
}),
|
||||
lastRefreshError: readConfigString(
|
||||
payload,
|
||||
'lastRefreshError',
|
||||
current?.lastRefreshError || '',
|
||||
{ allowBlank: true },
|
||||
),
|
||||
...resolveShopSecretPatch(payload, current),
|
||||
}
|
||||
}
|
||||
@@ -305,16 +350,14 @@ function resolveShopSecretPatch(
|
||||
return patch
|
||||
}
|
||||
|
||||
function readSecretString(
|
||||
payload: JsonObject,
|
||||
field: string,
|
||||
fallback: string,
|
||||
): string {
|
||||
function readSecretString(payload: JsonObject, field: string, fallback: string): string {
|
||||
const text = String(payload[field] || '').trim()
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
function resolveAccessTokenStatus(config: Pick<KuaishouIndustrySourceConfig, 'accessToken' | 'accessTokenExpiresAt'>) {
|
||||
function resolveAccessTokenStatus(
|
||||
config: Pick<KuaishouIndustrySourceConfig, 'accessToken' | 'accessTokenExpiresAt'>,
|
||||
) {
|
||||
if (!config.accessToken) {
|
||||
return {
|
||||
status: 'missing',
|
||||
|
||||
@@ -94,23 +94,27 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
|
||||
bark: {
|
||||
enabled: bark.enabled !== false,
|
||||
serverUrl: String(bark.serverUrl || 'https://api.day.app').trim(),
|
||||
recipients: (Array.isArray(bark.recipients) ? bark.recipients : []).map((item: JsonObject) => ({
|
||||
id: String(item.id || '').trim(),
|
||||
name: String(item.name || '').trim(),
|
||||
deviceKey: String(item.deviceKey || '').trim(),
|
||||
deviceKeyMasked: maskSecret(item.deviceKey),
|
||||
enabled: item.enabled !== false,
|
||||
})),
|
||||
recipients: (Array.isArray(bark.recipients) ? bark.recipients : []).map(
|
||||
(item: JsonObject) => ({
|
||||
id: String(item.id || '').trim(),
|
||||
name: String(item.name || '').trim(),
|
||||
deviceKey: String(item.deviceKey || '').trim(),
|
||||
deviceKeyMasked: maskSecret(item.deviceKey),
|
||||
enabled: item.enabled !== false,
|
||||
}),
|
||||
),
|
||||
},
|
||||
wpush: {
|
||||
enabled: wpush.enabled !== false,
|
||||
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map((item: JsonObject) => ({
|
||||
id: String(item.id || '').trim(),
|
||||
name: String(item.name || '').trim(),
|
||||
apiKey: String(item.apiKey || item.apikey || '').trim(),
|
||||
apiKeyMasked: maskSecret(item.apiKey || item.apikey),
|
||||
enabled: item.enabled !== false,
|
||||
})),
|
||||
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map(
|
||||
(item: JsonObject) => ({
|
||||
id: String(item.id || '').trim(),
|
||||
name: String(item.name || '').trim(),
|
||||
apiKey: String(item.apiKey || item.apikey || '').trim(),
|
||||
apiKeyMasked: maskSecret(item.apiKey || item.apikey),
|
||||
enabled: item.enabled !== false,
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -118,8 +122,7 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
|
||||
|
||||
function mapAdminScheduledJobsConfig(config: JsonObject = {}) {
|
||||
const cloudtentaclesAccountMap = new Map(
|
||||
listAdminCloudtentaclesMonitorAccounts()
|
||||
.map((item) => [item.sourceKey, item]),
|
||||
listAdminCloudtentaclesMonitorAccounts().map((item) => [item.sourceKey, item]),
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -147,21 +150,23 @@ function listAdminCloudtentaclesMonitorAccounts() {
|
||||
const sessionsConfig = getAllCloudtentaclesSessionStates()
|
||||
const sessions: Record<string, JsonObject> = sessionsConfig.sessions || {}
|
||||
|
||||
return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : []).map((source) => {
|
||||
const sourceKey = String(source.key || '').trim()
|
||||
const session = sessions[sourceKey] || {}
|
||||
const label = String(source.label || source.username || sourceKey).trim() || sourceKey
|
||||
return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : [])
|
||||
.map((source) => {
|
||||
const sourceKey = String(source.key || '').trim()
|
||||
const session = sessions[sourceKey] || {}
|
||||
const label = String(source.label || source.username || sourceKey).trim() || sourceKey
|
||||
|
||||
return {
|
||||
sourceKey,
|
||||
label,
|
||||
enabled: source.enabled !== false,
|
||||
username: String(source.username || '').trim(),
|
||||
phoneMasked: maskPhone(source.phone || session.phone),
|
||||
hasToken: Boolean(String(session.token || '').trim()),
|
||||
loggedInAt: String(session.loggedInAt || '').trim(),
|
||||
}
|
||||
}).filter((item) => item.sourceKey)
|
||||
return {
|
||||
sourceKey,
|
||||
label,
|
||||
enabled: source.enabled !== false,
|
||||
username: String(source.username || '').trim(),
|
||||
phoneMasked: maskPhone(source.phone || session.phone),
|
||||
hasToken: Boolean(String(session.token || '').trim()),
|
||||
loggedInAt: String(session.loggedInAt || '').trim(),
|
||||
}
|
||||
})
|
||||
.filter((item) => item.sourceKey)
|
||||
}
|
||||
|
||||
function mapScheduledJobCloudtentaclesAccounts(
|
||||
@@ -193,30 +198,35 @@ function mapScheduledJobCloudtentaclesAccounts(
|
||||
},
|
||||
] as const
|
||||
})
|
||||
.filter(Boolean) as Array<readonly [string, {
|
||||
sourceKey: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
assetThreshold: number
|
||||
}]>,
|
||||
.filter(Boolean) as Array<
|
||||
readonly [
|
||||
string,
|
||||
{
|
||||
sourceKey: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
assetThreshold: number
|
||||
},
|
||||
]
|
||||
>,
|
||||
)
|
||||
|
||||
// 读配置时合并全部 kuaishou-lewan 账号,避免前端/任务只看到历史 default。
|
||||
const merged = Array.from(cloudtentaclesAccountMap.values()).map((option) => {
|
||||
const sourceKey = String(option.sourceKey || '').trim()
|
||||
const configured = configuredMap.get(sourceKey)
|
||||
return {
|
||||
sourceKey,
|
||||
label: String(configured?.label || option.label || sourceKey).trim(),
|
||||
enabled: configured
|
||||
? configured.enabled !== false
|
||||
: option.enabled !== false,
|
||||
assetThreshold: normalizeNonNegativeNumber(
|
||||
configured?.assetThreshold,
|
||||
defaultAssetThreshold,
|
||||
),
|
||||
}
|
||||
}).filter((item) => item.sourceKey)
|
||||
const merged = Array.from(cloudtentaclesAccountMap.values())
|
||||
.map((option) => {
|
||||
const sourceKey = String(option.sourceKey || '').trim()
|
||||
const configured = configuredMap.get(sourceKey)
|
||||
return {
|
||||
sourceKey,
|
||||
label: String(configured?.label || option.label || sourceKey).trim(),
|
||||
enabled: configured ? configured.enabled !== false : option.enabled !== false,
|
||||
assetThreshold: normalizeNonNegativeNumber(
|
||||
configured?.assetThreshold,
|
||||
defaultAssetThreshold,
|
||||
),
|
||||
}
|
||||
})
|
||||
.filter((item) => item.sourceKey)
|
||||
|
||||
const mergedKeys = new Set(merged.map((item) => item.sourceKey))
|
||||
for (const [sourceKey, configured] of configuredMap.entries()) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { buildClaimUrl, createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import {
|
||||
maskCode as maskCodeValue,
|
||||
maskPhone as maskPhoneValue,
|
||||
} from '../../utils/masking.js'
|
||||
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../utils/masking.js'
|
||||
|
||||
import type { TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
@@ -31,7 +28,9 @@ export function isRecoverableTaskSessionCloseError(error: ErrorLike | null | und
|
||||
}
|
||||
|
||||
export function normalizeManualDispatchOutcome(value: unknown): 'delivered' | 'failed' {
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
const normalized = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
if (normalized === 'failed') {
|
||||
return 'failed'
|
||||
|
||||
@@ -25,18 +25,13 @@ import {
|
||||
refreshFulfillmentRole,
|
||||
returnFulfillmentNumber,
|
||||
} from '../../fulfillment/executors/registry.js'
|
||||
import {
|
||||
isKuaishouCloudTask,
|
||||
normalizeKuaishouCloudFlow,
|
||||
} from './kuaishou-cloud-helpers.js'
|
||||
import { isKuaishouCloudTask, normalizeKuaishouCloudFlow } from './kuaishou-cloud-helpers.js'
|
||||
|
||||
import type {
|
||||
AdminEntityIdInput,
|
||||
AdminViewerSessionInput,
|
||||
} from '../../../types/admin/read-inputs.js'
|
||||
import type {
|
||||
AdminTaskKuaishouIndustryConsumeInput,
|
||||
} from '../../../types/admin/write-inputs.js'
|
||||
import type { AdminTaskKuaishouIndustryConsumeInput } from '../../../types/admin/write-inputs.js'
|
||||
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
|
||||
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js'
|
||||
|
||||
@@ -443,8 +438,8 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
|
||||
const taskContext = parseTaskContext(consumeTargetTask)
|
||||
const hasCloudFulfillmentContext = Boolean(
|
||||
taskContext.kuaishouCloudFulfillment &&
|
||||
typeof taskContext.kuaishouCloudFulfillment === 'object' &&
|
||||
!Array.isArray(taskContext.kuaishouCloudFulfillment),
|
||||
typeof taskContext.kuaishouCloudFulfillment === 'object' &&
|
||||
!Array.isArray(taskContext.kuaishouCloudFulfillment),
|
||||
)
|
||||
const flow = hasCloudFulfillmentContext
|
||||
? normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
@@ -491,7 +486,9 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
|
||||
return { task: mapTaskActionPayload(updatedTask || consumeTargetTask) }
|
||||
}
|
||||
|
||||
async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<KuaishouIndustryVoucherRow> {
|
||||
async function getRequiredIndustryVoucherForTask(
|
||||
task: TaskRow,
|
||||
): Promise<KuaishouIndustryVoucherRow> {
|
||||
const vouchers = await listKuaishouIndustryVouchersByTaskId(task.id)
|
||||
const firstVoucher = vouchers[0]
|
||||
if (firstVoucher) {
|
||||
@@ -499,7 +496,9 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
|
||||
const voucherContext = normalizeAdminTaskIndustryVoucherContext(
|
||||
taskContext.kuaishouIndustryVoucher,
|
||||
)
|
||||
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
|
||||
if (voucherCode) {
|
||||
const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id)
|
||||
@@ -539,12 +538,12 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
|
||||
}
|
||||
|
||||
function normalizeAdminTaskIndustryVoucherContext(value: unknown): JsonObject {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
}
|
||||
|
||||
async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise<KuaishouIndustryVoucherRow[]> {
|
||||
async function resolveIndustryVouchersForOrder(
|
||||
platformOrderId: string,
|
||||
): Promise<KuaishouIndustryVoucherRow[]> {
|
||||
const normalizedOid = String(platformOrderId || '').trim()
|
||||
if (!normalizedOid) {
|
||||
return []
|
||||
|
||||
@@ -52,7 +52,9 @@ export type PreparedKuaishouCloudBindResource = {
|
||||
bindUrl: string
|
||||
}
|
||||
|
||||
export function resolvePersistedCloudtentaclesContext(sourceKeys: unknown[]): CloudtentaclesContext {
|
||||
export function resolvePersistedCloudtentaclesContext(
|
||||
sourceKeys: unknown[],
|
||||
): CloudtentaclesContext {
|
||||
try {
|
||||
return resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys)
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,14 +4,8 @@ import { createTaskEvent } from '../../../repositories/task-event-repo.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import {
|
||||
canViewerCloseTask,
|
||||
createAdminViewerContext,
|
||||
} from '../admin-read-shared-helpers.js'
|
||||
import {
|
||||
getRequiredTask,
|
||||
mapTaskActionPayload,
|
||||
} from '../admin-task-read-helpers.js'
|
||||
import { canViewerCloseTask, createAdminViewerContext } from '../admin-read-shared-helpers.js'
|
||||
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
|
||||
|
||||
import type {
|
||||
AdminEntityIdInput,
|
||||
@@ -62,16 +56,21 @@ export async function closeAdminTask(
|
||||
})
|
||||
}
|
||||
|
||||
await createTaskEvent(task.id, 'task_closed', {
|
||||
closedBy: session
|
||||
? {
|
||||
userId: session.userId,
|
||||
username: session.username,
|
||||
role: session.role,
|
||||
}
|
||||
: null,
|
||||
claimTokenClosed: claimTokenId > 0,
|
||||
}, now)
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'task_closed',
|
||||
{
|
||||
closedBy: session
|
||||
? {
|
||||
userId: session.userId,
|
||||
username: session.username,
|
||||
role: session.role,
|
||||
}
|
||||
: null,
|
||||
claimTokenClosed: claimTokenId > 0,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
|
||||
@@ -69,7 +69,11 @@ export function getClaimIdentityFromTask(task: Partial<TaskRow> | null | undefin
|
||||
}
|
||||
|
||||
export function hasClaimExpectedUid(taskOrContext: unknown): boolean {
|
||||
if (taskOrContext && typeof taskOrContext === 'object' && 'context_json' in (taskOrContext as object)) {
|
||||
if (
|
||||
taskOrContext &&
|
||||
typeof taskOrContext === 'object' &&
|
||||
'context_json' in (taskOrContext as object)
|
||||
) {
|
||||
return Boolean(getClaimIdentityFromTask(taskOrContext as TaskRow).expectedUid)
|
||||
}
|
||||
return Boolean(getClaimIdentityFromContext(taskOrContext).expectedUid)
|
||||
@@ -158,16 +162,11 @@ export function buildClaimIdentityAdminSummary(
|
||||
role.name || binding.roleName || options.taskRoleName || '',
|
||||
).trim()
|
||||
const boundUid = useShipped ? shippedUid : liveBoundUid
|
||||
const boundRoleName = useShipped
|
||||
? shippedRoleName || liveBoundRoleName
|
||||
: liveBoundRoleName
|
||||
const boundRoleName = useShipped ? shippedRoleName || liveBoundRoleName : liveBoundRoleName
|
||||
const ready = Boolean(identity.expectedUid)
|
||||
const uidMatched = ready && boundUid ? isClaimUidMatched(identity.expectedUid, boundUid) : null
|
||||
const liveMismatched =
|
||||
useShipped &&
|
||||
liveBoundUid &&
|
||||
shippedUid &&
|
||||
!isClaimUidMatched(shippedUid, liveBoundUid)
|
||||
useShipped && liveBoundUid && shippedUid && !isClaimUidMatched(shippedUid, liveBoundUid)
|
||||
|
||||
let note = '旧单或未提交 UID:用户须先打开领取页填写 UID,否则禁止自动发货'
|
||||
if (ready) {
|
||||
|
||||
@@ -90,8 +90,5 @@ test('assertBoundUidMatchesExpected 在不匹配时抛错', () => {
|
||||
})
|
||||
|
||||
test('assertClaimExpectedUidReady 要求 claimIdentity', () => {
|
||||
assert.throws(
|
||||
() => assertClaimExpectedUidReady({ context_json: '{}' }),
|
||||
/填写游戏 UID/,
|
||||
)
|
||||
assert.throws(() => assertClaimExpectedUidReady({ context_json: '{}' }), /填写游戏 UID/)
|
||||
})
|
||||
|
||||
@@ -42,9 +42,7 @@ export function resolveClaimTokenExpiration(
|
||||
tokenTtlHours: unknown,
|
||||
): string | null {
|
||||
const ttlHours = Number(tokenTtlHours)
|
||||
return Number.isFinite(ttlHours) && ttlHours > 0
|
||||
? addHours(createdAt, ttlHours)
|
||||
: null
|
||||
return Number.isFinite(ttlHours) && ttlHours > 0 ? addHours(createdAt, ttlHours) : null
|
||||
}
|
||||
|
||||
export function buildClaimUrl(token: string): string {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { buildClaimIdentityPayload, getClaimIdentityFromContext } from './claim-
|
||||
import { resolveKuaishouFeifeiH5UrlWithUid } from '../fulfillment/kuaishou-feifei/index.js'
|
||||
import { normalizeAffiliateDashFlow } from '../fulfillment/affiliate-dash/index.js'
|
||||
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
import { asJsonObject, type JsonObject } from '../../types/json.js'
|
||||
import { asJsonObject, type JsonObject } from '../../types/json.js'
|
||||
|
||||
export const CLAIM_TERMINAL_STATUSES = new Set([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED])
|
||||
type ClaimContext = {
|
||||
@@ -148,16 +148,21 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
|
||||
kuaishouFeifei: null as null,
|
||||
result: task.redeemed_at
|
||||
? {
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
||||
function buildAffiliateDashClaimDetailPayload({
|
||||
claimToken,
|
||||
task,
|
||||
order,
|
||||
orderItem,
|
||||
}: ClaimContext) {
|
||||
const context = parseTaskContext(task)
|
||||
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
||||
const flow = normalizeAffiliateDashFlow(context.affiliateDash)
|
||||
@@ -169,11 +174,13 @@ function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderIt
|
||||
skuCode: String(orderItem.sku_code || '').trim(),
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
isBundle: false,
|
||||
items: [{
|
||||
cloudSkuId: 0,
|
||||
name: productTitle,
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
}],
|
||||
items: [
|
||||
{
|
||||
cloudSkuId: 0,
|
||||
name: productTitle,
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -218,16 +225,21 @@ function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderIt
|
||||
affiliateDash: flow,
|
||||
result: task.redeemed_at
|
||||
? {
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) {
|
||||
function buildKuaishouFeifeiClaimDetailPayload({
|
||||
claimToken,
|
||||
task,
|
||||
order,
|
||||
orderItem,
|
||||
}: ClaimContext) {
|
||||
const context = parseTaskContext(task)
|
||||
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
||||
const expectedUid = getClaimIdentityFromContext(context).expectedUid
|
||||
@@ -237,11 +249,13 @@ function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderI
|
||||
skuCode: String(orderItem.sku_code || '').trim(),
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
isBundle: false,
|
||||
items: [{
|
||||
cloudSkuId: 0,
|
||||
name: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
}],
|
||||
items: [
|
||||
{
|
||||
cloudSkuId: 0,
|
||||
name: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -285,11 +299,11 @@ function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderI
|
||||
kuaishouFeifei: flow,
|
||||
result: task.redeemed_at
|
||||
? {
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
resultCode: String(task.result_code || ''),
|
||||
resultMessage: String(task.result_message || ''),
|
||||
screenshotReady: false,
|
||||
screenshotUrl: '',
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
@@ -362,13 +376,16 @@ function buildClaimProductPayload(
|
||||
quantity: item.quantity,
|
||||
}))
|
||||
.filter((item) => item.name)
|
||||
const normalizedDeliveryItems = deliveryItems.length > 0
|
||||
? mergeClaimProductItems(deliveryItems)
|
||||
: [{
|
||||
cloudSkuId: 0,
|
||||
name: displaySkuName,
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
}]
|
||||
const normalizedDeliveryItems =
|
||||
deliveryItems.length > 0
|
||||
? mergeClaimProductItems(deliveryItems)
|
||||
: [
|
||||
{
|
||||
cloudSkuId: 0,
|
||||
name: displaySkuName,
|
||||
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
title: displaySkuName,
|
||||
@@ -452,7 +469,9 @@ export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow)
|
||||
binding: {
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
cloudSourceKeys: Array.isArray(binding.cloudSourceKeys)
|
||||
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
|
||||
? binding.cloudSourceKeys
|
||||
.map((value: unknown) => String(value || '').trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(),
|
||||
skuId: Number(binding.skuId || 0) || 0,
|
||||
@@ -566,7 +585,9 @@ function normalizeClaimDeliveryItems(value: unknown, binding: JsonObject) {
|
||||
const rawItems = Array.isArray(value) ? value : []
|
||||
const items = rawItems
|
||||
.map((item) => normalizeClaimDeliveryItem(item))
|
||||
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } => Boolean(item))
|
||||
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } =>
|
||||
Boolean(item),
|
||||
)
|
||||
|
||||
if (items.length > 0) {
|
||||
return mergeClaimDeliveryItems(items)
|
||||
@@ -577,11 +598,13 @@ function normalizeClaimDeliveryItems(value: unknown, binding: JsonObject) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(binding.skuName || '').trim(),
|
||||
quantity: 1,
|
||||
}]
|
||||
return [
|
||||
{
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(binding.skuName || '').trim(),
|
||||
quantity: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeClaimDeliveryItem(value: unknown) {
|
||||
@@ -633,12 +656,13 @@ async function expireClaimContext(claimToken: ClaimTokenRow, task: TaskRow) {
|
||||
let nextTask = task
|
||||
|
||||
if (!isTaskFinalStatus(task.task_status)) {
|
||||
nextTask = await updateTask(task.id, {
|
||||
task_status: TASK_STATUS.EXPIRED,
|
||||
user_action_status: TASK_STATUS.EXPIRED,
|
||||
last_error: '领取链接已过期',
|
||||
updated_at: now,
|
||||
}) || task
|
||||
nextTask =
|
||||
(await updateTask(task.id, {
|
||||
task_status: TASK_STATUS.EXPIRED,
|
||||
user_action_status: TASK_STATUS.EXPIRED,
|
||||
last_error: '领取链接已过期',
|
||||
updated_at: now,
|
||||
})) || task
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -61,7 +61,9 @@ async function requireExecutorAction<T>(
|
||||
}
|
||||
|
||||
function isKuaishouCloudBindingReady(flow: ReturnType<typeof normalizeKuaishouCloudFlow>) {
|
||||
return flow.binding.prepareStatus === 'ready' && Boolean(String(flow.binding.bindUrl || '').trim())
|
||||
return (
|
||||
flow.binding.prepareStatus === 'ready' && Boolean(String(flow.binding.bindUrl || '').trim())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,9 +162,10 @@ async function verifyIndustryVoucherTicket(
|
||||
shopId: context.order.shop_id,
|
||||
shopName: context.order.shop_name,
|
||||
autoConsumeEnabled: true,
|
||||
consumedAt: voucherContext.status === 'CONSUMED'
|
||||
? voucherContext.consumedAt || flow.consume.consumedAt || now
|
||||
: flow.consume.consumedAt,
|
||||
consumedAt:
|
||||
voucherContext.status === 'CONSUMED'
|
||||
? voucherContext.consumedAt || flow.consume.consumedAt || now
|
||||
: flow.consume.consumedAt,
|
||||
},
|
||||
certInfo: {
|
||||
certExpireType,
|
||||
@@ -242,8 +245,10 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
|
||||
// 已核销也要走 verify:内部会补 prepare 绑定资源
|
||||
if (flow.consume.status !== 'success' || !isKuaishouCloudBindingReady(flow)) {
|
||||
const now = nowIso()
|
||||
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now)
|
||||
.catch(() => null)
|
||||
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(
|
||||
context,
|
||||
now,
|
||||
).catch(() => null)
|
||||
if (prepared) {
|
||||
return prepared
|
||||
}
|
||||
@@ -255,15 +260,14 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
const needsIndustryVoucherPrepare =
|
||||
hasUsableIndustryVoucher(taskContext) &&
|
||||
(
|
||||
flow.ticket.status !== 'verified' ||
|
||||
!isKuaishouCloudBindingReady(flow)
|
||||
)
|
||||
(flow.ticket.status !== 'verified' || !isKuaishouCloudBindingReady(flow))
|
||||
|
||||
if (needsIndustryVoucherPrepare) {
|
||||
const now = nowIso()
|
||||
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now)
|
||||
.catch(() => null)
|
||||
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(
|
||||
context,
|
||||
now,
|
||||
).catch(() => null)
|
||||
if (prepared) {
|
||||
return prepared
|
||||
}
|
||||
@@ -273,7 +277,8 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
|
||||
if (!isKuaishouCloudMockTask(task)) {
|
||||
const latestFlow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
|
||||
if (
|
||||
(latestFlow.ticket.status === 'verified' || hasUsableIndustryVoucher(parseTaskContext(task))) &&
|
||||
(latestFlow.ticket.status === 'verified' ||
|
||||
hasUsableIndustryVoucher(parseTaskContext(task))) &&
|
||||
!isKuaishouCloudBindingReady(latestFlow)
|
||||
) {
|
||||
task = await ensureKuaishouCloudBindingPrepared(task, {
|
||||
@@ -816,8 +821,8 @@ async function refreshAffiliateDashBindState(task: TaskRow): Promise<TaskRow | n
|
||||
// (不依赖平台 mismatch 字段,防止平台未标记但实际账号不一致的情况)。
|
||||
const localMismatch = Boolean(
|
||||
boundAccount &&
|
||||
expectedGameAccount &&
|
||||
normalizeUid(boundAccount) !== normalizeUid(expectedGameAccount),
|
||||
expectedGameAccount &&
|
||||
normalizeUid(boundAccount) !== normalizeUid(expectedGameAccount),
|
||||
)
|
||||
const nextFlow = {
|
||||
...flow,
|
||||
@@ -923,10 +928,11 @@ function hasUsableIndustryVoucher(context: JsonObject = {}) {
|
||||
}
|
||||
|
||||
function normalizeIndustryVoucherContext(value: unknown): JsonObject {
|
||||
const source = value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
const status = String(source.status || 'UNUSED').trim().toUpperCase()
|
||||
const source =
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
const status = String(source.status || 'UNUSED')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
|
||||
return {
|
||||
...source,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import {
|
||||
listAppConfigEntries,
|
||||
upsertAppConfigEntry,
|
||||
} from '../../repositories/app-config-repo.js'
|
||||
import { listAppConfigEntries, upsertAppConfigEntry } from '../../repositories/app-config-repo.js'
|
||||
import { readJsonFile } from '../../utils/json-file-store.js'
|
||||
|
||||
type NormalizeJsonValue<T> = (value: unknown) => T
|
||||
|
||||
@@ -67,8 +67,8 @@ const JSON_CONFIG_MIGRATION_ITEMS: JsonConfigMigrationItem[] = [
|
||||
]
|
||||
|
||||
export async function migrateJsonConfigFilesToDatabase() {
|
||||
const migrated: Array<{ configKey: string, filePath: string }> = []
|
||||
const skipped: Array<{ configKey: string, filePath: string, reason: string }> = []
|
||||
const migrated: Array<{ configKey: string; filePath: string }> = []
|
||||
const skipped: Array<{ configKey: string; filePath: string; reason: string }> = []
|
||||
|
||||
for (const item of JSON_CONFIG_MIGRATION_ITEMS) {
|
||||
const filePath = path.join(DATA_DIR, item.fileName)
|
||||
|
||||
@@ -65,7 +65,11 @@ export function isDevMockEnabled(env: NodeJS.ProcessEnv = process.env): boolean
|
||||
if (String(env.ENABLE_DEV_MOCK || '').trim() === '1') {
|
||||
return true
|
||||
}
|
||||
if (String(env.ENABLE_DEV_MOCK || '').trim().toLowerCase() === 'true') {
|
||||
if (
|
||||
String(env.ENABLE_DEV_MOCK || '')
|
||||
.trim()
|
||||
.toLowerCase() === 'true'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return !isProductionLike(env)
|
||||
@@ -111,14 +115,16 @@ export function getDevMockStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createLewanMockClaim(input: {
|
||||
step?: unknown
|
||||
orderNo?: unknown
|
||||
productNo?: unknown
|
||||
uid?: unknown
|
||||
items?: unknown
|
||||
frontendBaseUrl?: unknown
|
||||
} = {}): Promise<DevMockCreateResult> {
|
||||
export async function createLewanMockClaim(
|
||||
input: {
|
||||
step?: unknown
|
||||
orderNo?: unknown
|
||||
productNo?: unknown
|
||||
uid?: unknown
|
||||
items?: unknown
|
||||
frontendBaseUrl?: unknown
|
||||
} = {},
|
||||
): Promise<DevMockCreateResult> {
|
||||
assertDevMockEnabled()
|
||||
|
||||
const step = normalizeLewanStep(input.step)
|
||||
@@ -192,7 +198,10 @@ export async function createLewanMockClaim(input: {
|
||||
})
|
||||
|
||||
if (!task) {
|
||||
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' })
|
||||
throw createHttpError('履约任务创建失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'dev_mock_task_failed',
|
||||
})
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
@@ -223,15 +232,17 @@ export async function createLewanMockClaim(input: {
|
||||
})
|
||||
}
|
||||
|
||||
export async function createFeifeiMockClaim(input: {
|
||||
step?: unknown
|
||||
orderNo?: unknown
|
||||
productName?: unknown
|
||||
productCode?: unknown
|
||||
uid?: unknown
|
||||
h5Url?: unknown
|
||||
frontendBaseUrl?: unknown
|
||||
} = {}): Promise<DevMockCreateResult> {
|
||||
export async function createFeifeiMockClaim(
|
||||
input: {
|
||||
step?: unknown
|
||||
orderNo?: unknown
|
||||
productName?: unknown
|
||||
productCode?: unknown
|
||||
uid?: unknown
|
||||
h5Url?: unknown
|
||||
frontendBaseUrl?: unknown
|
||||
} = {},
|
||||
): Promise<DevMockCreateResult> {
|
||||
assertDevMockEnabled()
|
||||
|
||||
const step = normalizeFeifeiStep(input.step)
|
||||
@@ -296,7 +307,12 @@ export async function createFeifeiMockClaim(input: {
|
||||
? TASK_STATUS.MANUAL_REVIEW
|
||||
: TASK_STATUS.LINK_GENERATED,
|
||||
deliveryStatus: step === 'completed' ? 'delivered' : 'pending',
|
||||
resultCode: step === 'completed' ? 'kuaishou_feifei_completed' : step === 'failed' ? 'kuaishou_feifei_status_40' : '',
|
||||
resultCode:
|
||||
step === 'completed'
|
||||
? 'kuaishou_feifei_completed'
|
||||
: step === 'failed'
|
||||
? 'kuaishou_feifei_status_40'
|
||||
: '',
|
||||
resultMessage: rechargeStatusLabel,
|
||||
claimToken: '',
|
||||
claimExpiresAt: null,
|
||||
@@ -349,7 +365,10 @@ export async function createFeifeiMockClaim(input: {
|
||||
})
|
||||
|
||||
if (!task) {
|
||||
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' })
|
||||
throw createHttpError('履约任务创建失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'dev_mock_task_failed',
|
||||
})
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
@@ -383,14 +402,16 @@ type AffiliateDashMockStep = 'uid' | 'bind' | 'submitted' | 'completed' | 'faile
|
||||
* 生成 affiliate-dash 领取 mock:不调用真实 affiliate-dash 平台。
|
||||
* 上下文带 mock 标记,sync/refresh/submit 全部短路,领取页可走完四步。
|
||||
*/
|
||||
export async function createAffiliateDashMockClaim(input: {
|
||||
step?: unknown
|
||||
orderNo?: unknown
|
||||
productName?: unknown
|
||||
productSku?: unknown
|
||||
uid?: unknown
|
||||
frontendBaseUrl?: unknown
|
||||
} = {}): Promise<DevMockCreateResult> {
|
||||
export async function createAffiliateDashMockClaim(
|
||||
input: {
|
||||
step?: unknown
|
||||
orderNo?: unknown
|
||||
productName?: unknown
|
||||
productSku?: unknown
|
||||
uid?: unknown
|
||||
frontendBaseUrl?: unknown
|
||||
} = {},
|
||||
): Promise<DevMockCreateResult> {
|
||||
assertDevMockEnabled()
|
||||
|
||||
const step = normalizeAffiliateDashStep(input.step)
|
||||
@@ -457,11 +478,7 @@ export async function createAffiliateDashMockClaim(input: {
|
||||
? TASK_STATUS.REDEEMING
|
||||
: TASK_STATUS.LINK_GENERATED,
|
||||
deliveryStatus:
|
||||
step === 'completed'
|
||||
? 'delivered'
|
||||
: step === 'submitted'
|
||||
? 'delivering'
|
||||
: 'pending',
|
||||
step === 'completed' ? 'delivered' : step === 'submitted' ? 'delivering' : 'pending',
|
||||
resultCode:
|
||||
step === 'completed'
|
||||
? 'affiliate_dash_delivered'
|
||||
@@ -541,7 +558,10 @@ export async function createAffiliateDashMockClaim(input: {
|
||||
})
|
||||
|
||||
if (!task) {
|
||||
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' })
|
||||
throw createHttpError('履约任务创建失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'dev_mock_task_failed',
|
||||
})
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
@@ -573,9 +593,11 @@ export async function createAffiliateDashMockClaim(input: {
|
||||
* 生成电子凭证列表测试数据,不调用快手接口。
|
||||
* 覆盖未使用、已核销、已销毁和发码失败等运营页面常见状态。
|
||||
*/
|
||||
export async function createKuaishouIndustryVoucherMockData(input: {
|
||||
sellerId?: unknown
|
||||
} = {}): Promise<DevMockKuaishouIndustryVoucherResult> {
|
||||
export async function createKuaishouIndustryVoucherMockData(
|
||||
input: {
|
||||
sellerId?: unknown
|
||||
} = {},
|
||||
): Promise<DevMockKuaishouIndustryVoucherResult> {
|
||||
assertDevMockEnabled()
|
||||
|
||||
const now = nowIso()
|
||||
@@ -668,11 +690,13 @@ export async function createKuaishouIndustryVoucherMockData(input: {
|
||||
/**
|
||||
* 生成 91 查询请求体(带签名),可选对本机发起查询。
|
||||
*/
|
||||
export async function buildOpen91QueryMock(input: {
|
||||
orderNo?: unknown
|
||||
execute?: unknown
|
||||
baseUrl?: unknown
|
||||
} = {}) {
|
||||
export async function buildOpen91QueryMock(
|
||||
input: {
|
||||
orderNo?: unknown
|
||||
execute?: unknown
|
||||
baseUrl?: unknown
|
||||
} = {},
|
||||
) {
|
||||
assertDevMockEnabled()
|
||||
|
||||
const orderNo = String(input.orderNo || '').trim()
|
||||
@@ -767,7 +791,10 @@ async function ensureProfile(profileKey: string, name: string, requiresClaim: bo
|
||||
})
|
||||
|
||||
if (!profile) {
|
||||
throw createHttpError('履约配置创建失败', { statusCode: 500, errorCode: 'dev_mock_profile_failed' })
|
||||
throw createHttpError('履约配置创建失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'dev_mock_profile_failed',
|
||||
})
|
||||
}
|
||||
return profile
|
||||
}
|
||||
@@ -853,19 +880,19 @@ async function createBaseOrderItem(input: {
|
||||
},
|
||||
}
|
||||
: {
|
||||
source: OPEN_91_PROVIDER,
|
||||
orderNo: input.orderNo,
|
||||
productNo: input.productNo,
|
||||
productName: input.productName,
|
||||
shopId: input.consumeShopId,
|
||||
cloudtentacles: {
|
||||
matchMode: 'mock',
|
||||
normalizedProductName: input.productName,
|
||||
cloudSourceKeys: ['mock-cloudtentacles'],
|
||||
resolvedSourceKey: 'mock-cloudtentacles',
|
||||
deliveryItems: input.deliveryItems,
|
||||
},
|
||||
}
|
||||
source: OPEN_91_PROVIDER,
|
||||
orderNo: input.orderNo,
|
||||
productNo: input.productNo,
|
||||
productName: input.productName,
|
||||
shopId: input.consumeShopId,
|
||||
cloudtentacles: {
|
||||
matchMode: 'mock',
|
||||
normalizedProductName: input.productName,
|
||||
cloudSourceKeys: ['mock-cloudtentacles'],
|
||||
resolvedSourceKey: 'mock-cloudtentacles',
|
||||
deliveryItems: input.deliveryItems,
|
||||
},
|
||||
}
|
||||
|
||||
const [orderItem] = await replaceOrderItems(input.orderId, [
|
||||
{
|
||||
@@ -885,7 +912,10 @@ async function createBaseOrderItem(input: {
|
||||
])
|
||||
|
||||
if (!orderItem) {
|
||||
throw createHttpError('订单商品创建失败', { statusCode: 500, errorCode: 'dev_mock_item_failed' })
|
||||
throw createHttpError('订单商品创建失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'dev_mock_item_failed',
|
||||
})
|
||||
}
|
||||
return orderItem
|
||||
}
|
||||
@@ -931,9 +961,7 @@ function buildLewanMockContext(input: {
|
||||
configId: `mock:${input.productName}`,
|
||||
internalSkuCode: input.productName,
|
||||
internalSkuName: input.productName,
|
||||
deliveryItems: input.deliveryItems.length
|
||||
? input.deliveryItems
|
||||
: [primaryItem],
|
||||
deliveryItems: input.deliveryItems.length ? input.deliveryItems : [primaryItem],
|
||||
mock: {
|
||||
enabled: true,
|
||||
orderNo: input.orderNo,
|
||||
@@ -960,9 +988,7 @@ function buildLewanMockContext(input: {
|
||||
vnKey: '1',
|
||||
vnId: roleReady ? 900001 : 0,
|
||||
vnPhone: roleReady ? '13800000000' : '',
|
||||
bindUrl: roleReady
|
||||
? `https://example.com/mock-kuaishou-cloud-bind/${input.orderNo}`
|
||||
: '',
|
||||
bindUrl: roleReady ? `https://example.com/mock-kuaishou-cloud-bind/${input.orderNo}` : '',
|
||||
bindPreparedAt: roleReady ? input.timestamp : null,
|
||||
bindExpiresAt: roleReady ? addHours(input.timestamp, 24) : null,
|
||||
bindProbeAt: roleReady ? input.timestamp : null,
|
||||
@@ -1190,7 +1216,10 @@ function buildAffiliateDashTips(step: AffiliateDashMockStep, uid: string) {
|
||||
return [`打开领取链接,Step1 填 UID:${uid}(或自定义)`, '提交后进入绑定步,mock 会给出二维码']
|
||||
}
|
||||
if (step === 'bind') {
|
||||
return [`已预填 UID=${uid},绑定二维码为 mock 生成(扫描无效)`, '正常流程:扫码完成真实绑定后自动进入下一步']
|
||||
return [
|
||||
`已预填 UID=${uid},绑定二维码为 mock 生成(扫描无效)`,
|
||||
'正常流程:扫码完成真实绑定后自动进入下一步',
|
||||
]
|
||||
}
|
||||
if (step === 'submitted') {
|
||||
return ['已模拟绑定成功,可点「提交发货」(mock 直接模拟发货成功)']
|
||||
|
||||
@@ -50,10 +50,7 @@ function buildTask(overrides: Partial<TaskRow> = {}): TaskRow {
|
||||
test('buildAffiliateDashClientOrderNo uses the platform order number for a single task', () => {
|
||||
const task = buildTask()
|
||||
|
||||
assert.equal(
|
||||
buildAffiliateDashClientOrderNo(task, [task]),
|
||||
'2622300001260431',
|
||||
)
|
||||
assert.equal(buildAffiliateDashClientOrderNo(task, [task]), '2622300001260431')
|
||||
})
|
||||
|
||||
test('buildAffiliateDashClientOrderNo appends a stable position for split affiliate-dash tasks', () => {
|
||||
|
||||
@@ -52,10 +52,13 @@ export async function prepareAffiliateDashTask(task: TaskRow) {
|
||||
}
|
||||
|
||||
if (!flow.sku) {
|
||||
throw createHttpError('affiliate-dash 商品 sku 缺失,请配置 91 商品 → affiliate_dash sku 映射', {
|
||||
statusCode: 409,
|
||||
errorCode: 'affiliate_dash_sku_missing',
|
||||
})
|
||||
throw createHttpError(
|
||||
'affiliate-dash 商品 sku 缺失,请配置 91 商品 → affiliate_dash sku 映射',
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'affiliate_dash_sku_missing',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const siblingTasks = await listTasksByOrderId(task.order_id)
|
||||
@@ -97,13 +100,18 @@ export async function prepareAffiliateDashTask(task: TaskRow) {
|
||||
)
|
||||
}
|
||||
if (!consumeResult.ok) {
|
||||
logIntegration('[affiliate-dash]', '建单成功但电子凭证核销失败,delivered 回调将兜底重试', {
|
||||
taskId: task.id,
|
||||
orderNo: order.orderNo,
|
||||
voucherCount: consumeResult.vouchers.length,
|
||||
failedCount: consumeResult.failed.length,
|
||||
errorMessage: consumeResult.failed[0]?.errorMessage || '',
|
||||
}, { level: 'warn' })
|
||||
logIntegration(
|
||||
'[affiliate-dash]',
|
||||
'建单成功但电子凭证核销失败,delivered 回调将兜底重试',
|
||||
{
|
||||
taskId: task.id,
|
||||
orderNo: order.orderNo,
|
||||
voucherCount: consumeResult.vouchers.length,
|
||||
failedCount: consumeResult.failed.length,
|
||||
errorMessage: consumeResult.failed[0]?.errorMessage || '',
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
nextFlow.consumeStatus = 'not_required'
|
||||
@@ -231,11 +239,16 @@ export async function syncAffiliateDashTaskStatus(
|
||||
resultMessage = consumeResult.failed[0]?.errorMessage || '电子凭证核销失败,请人工处理'
|
||||
lastError = resultMessage
|
||||
nextFlow.consumeStatus = 'failed'
|
||||
logIntegration('[affiliate-dash]', 'affiliate-dash 履约完成但核销失败', {
|
||||
taskId: task.id,
|
||||
orderNo: flow.orderNo,
|
||||
errorMessage: resultMessage,
|
||||
}, { level: 'warn' })
|
||||
logIntegration(
|
||||
'[affiliate-dash]',
|
||||
'affiliate-dash 履约完成但核销失败',
|
||||
{
|
||||
taskId: task.id,
|
||||
orderNo: flow.orderNo,
|
||||
errorMessage: resultMessage,
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -256,7 +269,12 @@ export async function syncAffiliateDashTaskStatus(
|
||||
}
|
||||
|
||||
const isAlreadyTerminal = (
|
||||
[TASK_STATUS.REDEEMED, TASK_STATUS.MANUAL_REVIEW, TASK_STATUS.CLOSED, TASK_STATUS.FAILED] as string[]
|
||||
[
|
||||
TASK_STATUS.REDEEMED,
|
||||
TASK_STATUS.MANUAL_REVIEW,
|
||||
TASK_STATUS.CLOSED,
|
||||
TASK_STATUS.FAILED,
|
||||
] as string[]
|
||||
).includes(task.task_status)
|
||||
// 终态保护:已收敛成功的任务,非 delivered 状态不允许降级(如轮询时平台详情短暂返回
|
||||
// delivering/paid 会把 REDEEMED 打回 REDEEMING,导致结果页闪烁/倒退)。
|
||||
@@ -316,9 +334,8 @@ export type AffiliateDashFlow = {
|
||||
}
|
||||
|
||||
export function normalizeAffiliateDashFlow(value: unknown): AffiliateDashFlow {
|
||||
const source = value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
const source =
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
|
||||
return {
|
||||
flowType: 'affiliate_dash',
|
||||
@@ -397,10 +414,15 @@ async function preflightAffiliateDashWallet(sku: string) {
|
||||
if ((error as { errorCode?: string })?.errorCode === 'affiliate_dash_wallet_not_enough') {
|
||||
throw error
|
||||
}
|
||||
logIntegration('[affiliate-dash]', '余额预检失败(忽略,继续下单)', {
|
||||
sku,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}, { level: 'warn' })
|
||||
logIntegration(
|
||||
'[affiliate-dash]',
|
||||
'余额预检失败(忽略,继续下单)',
|
||||
{
|
||||
sku,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ import { resolveTaskDeliveryLink } from './delivery-link-service.js'
|
||||
import type { TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
test('resolveTaskDeliveryLink 为 cloud 任务返回内部领取链接', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
primary_claim_token: 'cloud-token',
|
||||
primary_claim_expires_at: '2026-07-09T00:00:00.000Z',
|
||||
}))
|
||||
const result = await resolveTaskDeliveryLink(
|
||||
createTask({
|
||||
executor_key: 'kuaishou_ct_assisted',
|
||||
primary_claim_token: 'cloud-token',
|
||||
primary_claim_expires_at: '2026-07-09T00:00:00.000Z',
|
||||
}),
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
claimUrl: buildClaimUrl('cloud-token'),
|
||||
@@ -19,11 +21,13 @@ test('resolveTaskDeliveryLink 为 cloud 任务返回内部领取链接', async (
|
||||
})
|
||||
|
||||
test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'kuaishou-industry',
|
||||
claim_token: 'industry-token',
|
||||
claim_expires_at: '2026-07-09T08:00:00.000Z',
|
||||
}))
|
||||
const result = await resolveTaskDeliveryLink(
|
||||
createTask({
|
||||
executor_key: 'kuaishou-industry',
|
||||
claim_token: 'industry-token',
|
||||
claim_expires_at: '2026-07-09T08:00:00.000Z',
|
||||
}),
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
claimUrl: buildClaimUrl('industry-token'),
|
||||
@@ -32,18 +36,20 @@ test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', asyn
|
||||
})
|
||||
|
||||
test('resolveTaskDeliveryLink 为 feifei 任务返回本站领取链接', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'kuaishou_feifei',
|
||||
primary_claim_token: 'feifei-token',
|
||||
primary_claim_expires_at: '2026-07-09T12:00:00.000Z',
|
||||
context_json: {
|
||||
kuaishouFeifei: {
|
||||
h5: {
|
||||
rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long',
|
||||
const result = await resolveTaskDeliveryLink(
|
||||
createTask({
|
||||
executor_key: 'kuaishou_feifei',
|
||||
primary_claim_token: 'feifei-token',
|
||||
primary_claim_expires_at: '2026-07-09T12:00:00.000Z',
|
||||
context_json: {
|
||||
kuaishouFeifei: {
|
||||
h5: {
|
||||
rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
}),
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
claimUrl: buildClaimUrl('feifei-token'),
|
||||
@@ -52,9 +58,11 @@ test('resolveTaskDeliveryLink 为 feifei 任务返回本站领取链接', async
|
||||
})
|
||||
|
||||
test('resolveTaskDeliveryLink 对人工履约任务返回 null', async () => {
|
||||
const result = await resolveTaskDeliveryLink(createTask({
|
||||
executor_key: 'manual_dispatch',
|
||||
}))
|
||||
const result = await resolveTaskDeliveryLink(
|
||||
createTask({
|
||||
executor_key: 'manual_dispatch',
|
||||
}),
|
||||
)
|
||||
|
||||
assert.equal(result, null)
|
||||
})
|
||||
|
||||
@@ -4,8 +4,6 @@ import type { TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
export type TaskDeliveryLink = FulfillmentDeliveryLink
|
||||
|
||||
export async function resolveTaskDeliveryLink(
|
||||
task: TaskRow,
|
||||
): Promise<TaskDeliveryLink | null> {
|
||||
export async function resolveTaskDeliveryLink(task: TaskRow): Promise<TaskDeliveryLink | null> {
|
||||
return resolveFulfillmentDeliveryLink(task)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { buildClaimUrl } from '../../claim/claim-service.js'
|
||||
import {
|
||||
shouldEnsureKuaishouCloudClaimLink,
|
||||
TASK_STATUS,
|
||||
} from '../../../domain/task-status.js'
|
||||
import { shouldEnsureKuaishouCloudClaimLink, TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import {
|
||||
confirmKuaishouCloudTaskRole,
|
||||
dispatchKuaishouCloudFulfillmentTask,
|
||||
@@ -95,9 +92,7 @@ async function prepareBinding(
|
||||
options: FulfillmentActionOptions = {},
|
||||
): Promise<FulfillmentActionResult> {
|
||||
// 后台/显式 force:旧号可被 CT 回收,忽略退号失败并取新号+新绑链
|
||||
const force =
|
||||
options.force === true ||
|
||||
String(options.source || '').includes('admin_')
|
||||
const force = options.force === true || String(options.source || '').includes('admin_')
|
||||
const result = await prepareKuaishouCloudFulfillmentTask(task, {
|
||||
source: options.source || 'executor_prepare_binding',
|
||||
actor: options.actor,
|
||||
|
||||
@@ -78,51 +78,30 @@ async function runExecutorAction(
|
||||
return handler(task, options)
|
||||
}
|
||||
|
||||
export function prepareFulfillmentBinding(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function prepareFulfillmentBinding(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'prepareBinding', options)
|
||||
}
|
||||
|
||||
export function rebindFulfillmentRole(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function rebindFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'rebindRole', options)
|
||||
}
|
||||
|
||||
export function refreshFulfillmentRole(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function refreshFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'refreshRole', options)
|
||||
}
|
||||
|
||||
export function confirmFulfillmentRole(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function confirmFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'confirmRole', options)
|
||||
}
|
||||
|
||||
export function redeemFulfillmentTask(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function redeemFulfillmentTask(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'redeemTask', options)
|
||||
}
|
||||
|
||||
export function dispatchFulfillmentTask(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function dispatchFulfillmentTask(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'dispatchTask', options)
|
||||
}
|
||||
|
||||
export function returnFulfillmentNumber(
|
||||
task: TaskRow,
|
||||
options: FulfillmentActionOptions = {},
|
||||
) {
|
||||
export function returnFulfillmentNumber(task: TaskRow, options: FulfillmentActionOptions = {}) {
|
||||
return runExecutorAction(task, 'returnNumber', options)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ export const FULFILLMENT_EXECUTOR_KEYS = {
|
||||
} as const
|
||||
|
||||
export type FulfillmentExecutorKey =
|
||||
(typeof FULFILLMENT_EXECUTOR_KEYS)[keyof typeof FULFILLMENT_EXECUTOR_KEYS] | (string & {})
|
||||
| (typeof FULFILLMENT_EXECUTOR_KEYS)[keyof typeof FULFILLMENT_EXECUTOR_KEYS]
|
||||
| (string & {})
|
||||
|
||||
export type FulfillmentDeliveryLink = {
|
||||
claimUrl: string
|
||||
@@ -53,10 +54,7 @@ export type FulfillmentActionResult = {
|
||||
|
||||
export type FulfillmentExecutor = {
|
||||
key: FulfillmentExecutorKey
|
||||
preparePaidTask?: (
|
||||
task: TaskRow,
|
||||
deps: FulfillmentPrepareDeps,
|
||||
) => Promise<TaskRow | null>
|
||||
preparePaidTask?: (task: TaskRow, deps: FulfillmentPrepareDeps) => Promise<TaskRow | null>
|
||||
resolveDeliveryLink?: (task: TaskRow) => Promise<FulfillmentDeliveryLink | null>
|
||||
/** lewan:准备绑定资源(虚拟号 / bindUrl) */
|
||||
prepareBinding?: (
|
||||
@@ -104,8 +102,10 @@ export function normalizeExecutorKey(value: unknown): FulfillmentExecutorKey {
|
||||
|
||||
export function isKuaishouCloudExecutor(value: unknown): boolean {
|
||||
const executorKey = normalizeExecutorKey(value)
|
||||
return executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD ||
|
||||
return (
|
||||
executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD ||
|
||||
executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_INDUSTRY
|
||||
)
|
||||
}
|
||||
|
||||
export function isKuaishouFeifeiExecutor(value: unknown): boolean {
|
||||
|
||||
@@ -65,7 +65,8 @@ test('selectCloudtentaclesSourceForFulfillment 无号码时忽略残留固定账
|
||||
},
|
||||
},
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
resolveContextBySourceKeys: (sourceKeys) =>
|
||||
contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
listSourceLoadStats: async () => [
|
||||
{ sourceKey: 'account-a', activeCount: 20, redeemingCount: 0 },
|
||||
{ sourceKey: 'account-b', activeCount: 1, redeemingCount: 0 },
|
||||
@@ -86,7 +87,8 @@ test('selectCloudtentaclesSourceForFulfillment 跳过真实占用已达20的账
|
||||
const selection = await selectCloudtentaclesSourceForFulfillment(
|
||||
{ binding: { cloudSourceKeys: ['account-a', 'account-b'] } },
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
resolveContextBySourceKeys: (sourceKeys) =>
|
||||
contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
listSourceLoadStats: async () => [],
|
||||
listVirtualNumbers: async (payload) => {
|
||||
const count = payload.sourceKey === 'account-a' ? 20 : 3
|
||||
@@ -115,12 +117,14 @@ test('selectCloudtentaclesSourceForFulfillment 不把 status=0 的空闲号码
|
||||
const selection = await selectCloudtentaclesSourceForFulfillment(
|
||||
{ binding: { cloudSourceKeys: ['account-a', 'account-b'] } },
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
resolveContextBySourceKeys: (sourceKeys) =>
|
||||
contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
listSourceLoadStats: async () => [],
|
||||
listVirtualNumbers: async (payload) => {
|
||||
const items = payload.sourceKey === 'account-a'
|
||||
? Array.from({ length: 20 }, (_, id) => ({ id: id + 1, status: 0 }))
|
||||
: Array.from({ length: 3 }, (_, id) => ({ id: id + 1, status: 1 }))
|
||||
const items =
|
||||
payload.sourceKey === 'account-a'
|
||||
? Array.from({ length: 20 }, (_, id) => ({ id: id + 1, status: 0 }))
|
||||
: Array.from({ length: 3 }, (_, id) => ({ id: id + 1, status: 1 }))
|
||||
return {
|
||||
baseUrl: 'https://cloud.example.com',
|
||||
key: '1',
|
||||
@@ -145,7 +149,8 @@ test('selectCloudtentaclesSourceForFulfillment 真实占用优先于数据库历
|
||||
const selection = await selectCloudtentaclesSourceForFulfillment(
|
||||
{ binding: { cloudSourceKeys: ['account-a', 'account-b'] } },
|
||||
{
|
||||
resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
resolveContextBySourceKeys: (sourceKeys) =>
|
||||
contexts[String(sourceKeys[0]) as keyof typeof contexts],
|
||||
listSourceLoadStats: async () => [
|
||||
{ sourceKey: 'account-a', activeCount: 99, redeemingCount: 10 },
|
||||
{ sourceKey: 'account-b', activeCount: 0, redeemingCount: 0 },
|
||||
|
||||
@@ -10,14 +10,8 @@ import {
|
||||
} from './domain.js'
|
||||
|
||||
test('isKuaishouCloudDispatchSucceeded 识别 dispatch.success', () => {
|
||||
assert.equal(
|
||||
isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'success' } }),
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'pending' } }),
|
||||
false,
|
||||
)
|
||||
assert.equal(isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'success' } }), true)
|
||||
assert.equal(isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'pending' } }), false)
|
||||
})
|
||||
|
||||
test('isKuaishouCloudBindingMutationFrozen:dispatch.success 即使 status=waiting_binding 也冻结', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHttpError } from "../../../utils/http.js";
|
||||
import { normalizeProductName } from "../product-resolution-service.js";
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { normalizeProductName } from '../product-resolution-service.js'
|
||||
import {
|
||||
appointCloudtentaclesVirtualNumber,
|
||||
backCloudtentaclesVirtualNumber,
|
||||
@@ -7,107 +7,95 @@ import {
|
||||
generateCloudtentaclesLoginCode,
|
||||
getCloudtentaclesBindUrl,
|
||||
verifyCloudtentaclesLoginCode,
|
||||
} from "../../platforms/cloudtentacles/virtual-number-service.js";
|
||||
import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from "./domain.js";
|
||||
import { logInfo } from "../../../utils/logger.js";
|
||||
} from '../../platforms/cloudtentacles/virtual-number-service.js'
|
||||
import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from './domain.js'
|
||||
import { logInfo } from '../../../utils/logger.js'
|
||||
|
||||
/** 账号号码配额占满后冷却:避免领取页轮询 / open-91 交付每轮都狂打上游 */
|
||||
const APPOINT_QUOTA_COOLDOWN_MS = 60_000;
|
||||
const appointQuotaCooldowns = new Map<string, number>();
|
||||
const APPOINT_QUOTA_COOLDOWN_MS = 60_000
|
||||
const appointQuotaCooldowns = new Map<string, number>()
|
||||
|
||||
export function resolveKuaishouCloudBindingResources(
|
||||
flow: JsonObject,
|
||||
{ skuItems = [], knapsackItems = [] }: { skuItems?: unknown[], knapsackItems?: unknown[] } = {}
|
||||
{ skuItems = [], knapsackItems = [] }: { skuItems?: unknown[]; knapsackItems?: unknown[] } = {},
|
||||
) {
|
||||
const normalizedSkuItems = Array.isArray(skuItems)
|
||||
? skuItems.filter(isCloudSkuLikeItem)
|
||||
: [];
|
||||
const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : []
|
||||
const normalizedKnapsackItems = Array.isArray(knapsackItems)
|
||||
? knapsackItems.filter(isCloudSkuLikeItem)
|
||||
: [];
|
||||
const currentSkuId = Number(flow?.binding?.skuId || 0) || 0;
|
||||
const currentSkuName = String(flow?.binding?.skuName || "").trim();
|
||||
const nameCandidates = collectKuaishouCloudNameCandidates(flow);
|
||||
: []
|
||||
const currentSkuId = Number(flow?.binding?.skuId || 0) || 0
|
||||
const currentSkuName = String(flow?.binding?.skuName || '').trim()
|
||||
const nameCandidates = collectKuaishouCloudNameCandidates(flow)
|
||||
|
||||
const skuItemById =
|
||||
currentSkuId > 0
|
||||
? normalizedSkuItems.find(
|
||||
(item) => Number(item.id || 0) === currentSkuId
|
||||
) || null
|
||||
: null;
|
||||
? normalizedSkuItems.find((item) => Number(item.id || 0) === currentSkuId) || null
|
||||
: null
|
||||
const knapsackItemById =
|
||||
currentSkuId > 0
|
||||
? normalizedKnapsackItems.find(
|
||||
(item) => Number(item.id || 0) === currentSkuId
|
||||
) || null
|
||||
: null;
|
||||
? normalizedKnapsackItems.find((item) => Number(item.id || 0) === currentSkuId) || null
|
||||
: null
|
||||
|
||||
if (skuItemById || knapsackItemById) {
|
||||
const matchedItem = skuItemById || knapsackItemById;
|
||||
const matchedItem = skuItemById || knapsackItemById
|
||||
return {
|
||||
skuId: Number(matchedItem?.id || 0) || 0,
|
||||
skuName: String(currentSkuName || matchedItem?.name || "").trim(),
|
||||
skuName: String(currentSkuName || matchedItem?.name || '').trim(),
|
||||
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
skuItem: skuItemById,
|
||||
knapsackItem: knapsackItemById,
|
||||
resolvedByName: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const matchedSkuItem = findCloudItemByNames(
|
||||
normalizedSkuItems,
|
||||
nameCandidates
|
||||
);
|
||||
const matchedSkuItem = findCloudItemByNames(normalizedSkuItems, nameCandidates)
|
||||
const matchedKnapsackItem = findCloudItemByNames(
|
||||
normalizedKnapsackItems,
|
||||
nameCandidates,
|
||||
matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0
|
||||
);
|
||||
const matchedItem = matchedSkuItem || matchedKnapsackItem;
|
||||
matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0,
|
||||
)
|
||||
const matchedItem = matchedSkuItem || matchedKnapsackItem
|
||||
|
||||
return {
|
||||
skuId: Number(matchedItem?.id || 0) || 0,
|
||||
skuName: String(currentSkuName || matchedItem?.name || "").trim(),
|
||||
skuName: String(currentSkuName || matchedItem?.name || '').trim(),
|
||||
vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
skuItem: matchedSkuItem,
|
||||
knapsackItem: matchedKnapsackItem,
|
||||
resolvedByName: Boolean(matchedItem),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveKuaishouCloudVnKeyCandidates(_input: JsonObject = {}) {
|
||||
return [KUAISHOU_CLOUD_FIXED_VN_KEY];
|
||||
return [KUAISHOU_CLOUD_FIXED_VN_KEY]
|
||||
}
|
||||
|
||||
export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonObject = {}) {
|
||||
const { cloudContext = {}, vnKeyCandidates = [] } = input;
|
||||
const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : [];
|
||||
const sourceKey = String(cloudContext.resolvedSourceKey || "").trim();
|
||||
let lastError = null;
|
||||
const { cloudContext = {}, vnKeyCandidates = [] } = input
|
||||
const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : []
|
||||
const sourceKey = String(cloudContext.resolvedSourceKey || '').trim()
|
||||
let lastError = null
|
||||
|
||||
for (const vnKey of candidates) {
|
||||
let vnId = 0;
|
||||
let vnPhone = "";
|
||||
const cooldownKey = `${sourceKey}|${vnKey}`;
|
||||
let vnId = 0
|
||||
let vnPhone = ''
|
||||
const cooldownKey = `${sourceKey}|${vnKey}`
|
||||
|
||||
const cooldownUntil = appointQuotaCooldowns.get(cooldownKey) || 0;
|
||||
const cooldownUntil = appointQuotaCooldowns.get(cooldownKey) || 0
|
||||
if (cooldownUntil > Date.now()) {
|
||||
throw createHttpError(
|
||||
"账号虚拟号配额已满,请先退回已占用号码或稍后重试",
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: "cloudtentacles_vn_quota_cooldown",
|
||||
}
|
||||
);
|
||||
throw createHttpError('账号虚拟号配额已满,请先退回已占用号码或稍后重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'cloudtentacles_vn_quota_cooldown',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const appointed = await appointCloudtentaclesVirtualNumber({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
});
|
||||
vnId = Number(appointed.item?.id || 0);
|
||||
vnPhone = String(appointed.item?.phone || "").trim();
|
||||
})
|
||||
vnId = Number(appointed.item?.id || 0)
|
||||
vnPhone = String(appointed.item?.phone || '').trim()
|
||||
|
||||
logInfo('[kuaishou-cloud/binding]', '虚拟号申请成功', {
|
||||
sourceKey,
|
||||
@@ -115,51 +103,51 @@ export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonOb
|
||||
purpose: String(input.purpose || 'prepare_binding'),
|
||||
vnKey,
|
||||
vnId,
|
||||
});
|
||||
})
|
||||
|
||||
if (!vnId || !vnPhone) {
|
||||
throw createHttpError("申请虚拟号成功但返回数据不完整", {
|
||||
throw createHttpError('申请虚拟号成功但返回数据不完整', {
|
||||
statusCode: 502,
|
||||
errorCode: "kuaishou_cloud_invalid_vn",
|
||||
});
|
||||
errorCode: 'kuaishou_cloud_invalid_vn',
|
||||
})
|
||||
}
|
||||
|
||||
await generateCloudtentaclesLoginCode({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
});
|
||||
})
|
||||
|
||||
const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
phone: vnPhone,
|
||||
});
|
||||
})
|
||||
|
||||
await verifyCloudtentaclesLoginCode({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
code: fetchedCode.code,
|
||||
});
|
||||
})
|
||||
|
||||
const bindUrlResult = await getCloudtentaclesBindUrl({
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
});
|
||||
})
|
||||
|
||||
return {
|
||||
vnKey,
|
||||
vnId,
|
||||
vnPhone,
|
||||
bindUrl: String(bindUrlResult.bindUrl || "").trim(),
|
||||
};
|
||||
bindUrl: String(bindUrlResult.bindUrl || '').trim(),
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
lastError = error
|
||||
|
||||
if (isAppointQuotaExhaustedError(error)) {
|
||||
appointQuotaCooldowns.set(cooldownKey, Date.now() + APPOINT_QUOTA_COOLDOWN_MS);
|
||||
appointQuotaCooldowns.set(cooldownKey, Date.now() + APPOINT_QUOTA_COOLDOWN_MS)
|
||||
}
|
||||
|
||||
if (vnId > 0) {
|
||||
@@ -168,112 +156,108 @@ export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonOb
|
||||
...cloudContext,
|
||||
key: vnKey,
|
||||
id: vnId,
|
||||
});
|
||||
})
|
||||
} catch {
|
||||
// 退号失败保留主错误
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRecoverableKuaishouCloudVnKeyError(error)) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw (
|
||||
lastError ||
|
||||
createHttpError("没有找到可用的 VN Key", {
|
||||
createHttpError('没有找到可用的 VN Key', {
|
||||
statusCode: 409,
|
||||
errorCode: "kuaishou_cloud_missing_binding_config",
|
||||
errorCode: 'kuaishou_cloud_missing_binding_config',
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function collectKuaishouCloudNameCandidates(flow: JsonObject) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
[
|
||||
String(flow?.binding?.skuName || "").trim(),
|
||||
String(flow?.internalSkuName || "").trim(),
|
||||
String(flow?.internalSkuCode || "").trim(),
|
||||
].filter(Boolean)
|
||||
)
|
||||
);
|
||||
String(flow?.binding?.skuName || '').trim(),
|
||||
String(flow?.internalSkuName || '').trim(),
|
||||
String(flow?.internalSkuCode || '').trim(),
|
||||
].filter(Boolean),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function findCloudItemByNames(items: JsonObject[], nameCandidates: string[], preferredId = 0) {
|
||||
const normalizedItems = Array.isArray(items) ? items : [];
|
||||
const normalizedItems = Array.isArray(items) ? items : []
|
||||
const normalizedNames = nameCandidates
|
||||
.map((item: string) => ({
|
||||
raw: String(item || "").trim(),
|
||||
raw: String(item || '').trim(),
|
||||
normalized: normalizeProductName(item),
|
||||
}))
|
||||
.filter((item) => item.raw && item.normalized);
|
||||
.filter((item) => item.raw && item.normalized)
|
||||
|
||||
if (normalizedNames.length === 0 || normalizedItems.length === 0) {
|
||||
return preferredId > 0
|
||||
? normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) ||
|
||||
null
|
||||
: null;
|
||||
? normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) || null
|
||||
: null
|
||||
}
|
||||
|
||||
if (preferredId > 0) {
|
||||
const preferred =
|
||||
normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) ||
|
||||
null;
|
||||
normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) || null
|
||||
if (preferred) {
|
||||
return preferred;
|
||||
return preferred
|
||||
}
|
||||
}
|
||||
|
||||
const exactMatches = normalizedItems.filter((item: JsonObject) => {
|
||||
const itemName = normalizeProductName(item.name);
|
||||
const itemName = normalizeProductName(item.name)
|
||||
return normalizedNames.some(
|
||||
(candidate: { normalized: string }) => candidate.normalized === itemName
|
||||
);
|
||||
});
|
||||
(candidate: { normalized: string }) => candidate.normalized === itemName,
|
||||
)
|
||||
})
|
||||
if (exactMatches.length > 0) {
|
||||
return exactMatches[0];
|
||||
return exactMatches[0]
|
||||
}
|
||||
|
||||
const partialMatches = normalizedItems.filter((item: JsonObject) => {
|
||||
const itemName = normalizeProductName(item.name);
|
||||
const itemName = normalizeProductName(item.name)
|
||||
return normalizedNames.some(
|
||||
(candidate: { normalized: string }) =>
|
||||
itemName.includes(candidate.normalized) ||
|
||||
candidate.normalized.includes(itemName)
|
||||
);
|
||||
});
|
||||
itemName.includes(candidate.normalized) || candidate.normalized.includes(itemName),
|
||||
)
|
||||
})
|
||||
if (partialMatches.length > 0) {
|
||||
return partialMatches.sort(
|
||||
(left, right) =>
|
||||
String(left.name || "").length - String(right.name || "").length
|
||||
)[0];
|
||||
(left, right) => String(left.name || '').length - String(right.name || '').length,
|
||||
)[0]
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
function isCloudSkuLikeItem(item: unknown): item is JsonObject {
|
||||
const current = item && typeof item === "object" ? item as JsonObject : {};
|
||||
return Number(current.id || 0) > 0;
|
||||
const current = item && typeof item === 'object' ? (item as JsonObject) : {}
|
||||
return Number(current.id || 0) > 0
|
||||
}
|
||||
|
||||
function isAppointQuotaExhaustedError(error: unknown) {
|
||||
const current = error && typeof error === "object" ? error as JsonObject : {};
|
||||
const current = error && typeof error === 'object' ? (error as JsonObject) : {}
|
||||
return (
|
||||
String(current.errorCode || current.code || "").trim() ===
|
||||
"cloudtentacles_vn_appoint_failed" &&
|
||||
String(current.message || "").trim().includes("最多同时占用")
|
||||
);
|
||||
String(current.errorCode || current.code || '').trim() === 'cloudtentacles_vn_appoint_failed' &&
|
||||
String(current.message || '')
|
||||
.trim()
|
||||
.includes('最多同时占用')
|
||||
)
|
||||
}
|
||||
|
||||
function isRecoverableKuaishouCloudVnKeyError(error: unknown) {
|
||||
const current = error && typeof error === "object" ? error as JsonObject : {};
|
||||
const errorCode = String(current.errorCode || current.code || "").trim();
|
||||
const errorMessage = String(current.message || "").trim();
|
||||
const current = error && typeof error === 'object' ? (error as JsonObject) : {}
|
||||
const errorCode = String(current.errorCode || current.code || '').trim()
|
||||
const errorMessage = String(current.message || '').trim()
|
||||
return (
|
||||
errorCode === "cloudtentacles_vn_bind_url_failed" &&
|
||||
errorMessage.includes("不支持的游戏类型")
|
||||
);
|
||||
errorCode === 'cloudtentacles_vn_bind_url_failed' && errorMessage.includes('不支持的游戏类型')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createHttpError } from "../../../utils/http.js";
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import {
|
||||
normalizeCloudtentaclesDeviceId,
|
||||
normalizeCloudtentaclesDeviceType,
|
||||
} from "../../platforms/cloudtentacles/defaults.js";
|
||||
import { getCloudtentaclesSourceByKey } from "../../platforms/cloudtentacles/source-config-service.js";
|
||||
import { getCloudtentaclesSessionStateByKey } from "../../platforms/cloudtentacles/session-state-service.js";
|
||||
import { normalizeStringArray } from "./domain.js";
|
||||
} from '../../platforms/cloudtentacles/defaults.js'
|
||||
import { getCloudtentaclesSourceByKey } from '../../platforms/cloudtentacles/source-config-service.js'
|
||||
import { getCloudtentaclesSessionStateByKey } from '../../platforms/cloudtentacles/session-state-service.js'
|
||||
import { normalizeStringArray } from './domain.js'
|
||||
|
||||
/**
|
||||
* 严格模式:只解析列表中第一个账号(调用方均把任务实际取号账号 resolvedSourceKey 放首位),
|
||||
@@ -14,21 +14,19 @@ import { normalizeStringArray } from "./domain.js";
|
||||
* 退号、取链、发货等「操作任务已有号码」的场景必须使用此函数。
|
||||
* 将实际使用的 resolvedSourceKey 也返回,确保后续操作使用同一个 sourceKey。
|
||||
*/
|
||||
export function resolvePersistedCloudtentaclesContextBySourceKeys(
|
||||
sourceKeys: unknown[] = []
|
||||
) {
|
||||
const candidates = [...new Set(normalizeStringArray(sourceKeys))];
|
||||
export function resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys: unknown[] = []) {
|
||||
const candidates = [...new Set(normalizeStringArray(sourceKeys))]
|
||||
|
||||
if (candidates.length === 0) {
|
||||
throw createHttpError(
|
||||
"cloudtentacles 没有可用账号,请先到平台配置完成账号配置",
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_no_source_keys" }
|
||||
);
|
||||
throw createHttpError('cloudtentacles 没有可用账号,请先到平台配置完成账号配置', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_no_source_keys',
|
||||
})
|
||||
}
|
||||
|
||||
const sourceKey = String(candidates[0] || "").trim();
|
||||
const sourceKey = String(candidates[0] || '').trim()
|
||||
|
||||
return resolveSingleCloudtentaclesAccountContext(sourceKey);
|
||||
return resolveSingleCloudtentaclesAccountContext(sourceKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,77 +34,69 @@ export function resolvePersistedCloudtentaclesContextBySourceKeys(
|
||||
* 仅用于「取新号」场景(新号码归属被选中的账号,不会产生跨账号孤儿),
|
||||
* 以及 account-selector 单候选解析。严禁用于退号/取链/发货等操作已有号码的场景。
|
||||
*/
|
||||
export function resolvePersistedCloudtentaclesContextWithFallback(
|
||||
sourceKeys: unknown[] = []
|
||||
) {
|
||||
const candidates = [...new Set(normalizeStringArray(sourceKeys))];
|
||||
export function resolvePersistedCloudtentaclesContextWithFallback(sourceKeys: unknown[] = []) {
|
||||
const candidates = [...new Set(normalizeStringArray(sourceKeys))]
|
||||
|
||||
let lastError: unknown = null;
|
||||
let lastError: unknown = null
|
||||
|
||||
for (const sourceKey of candidates) {
|
||||
try {
|
||||
return resolveSingleCloudtentaclesAccountContext(sourceKey);
|
||||
return resolveSingleCloudtentaclesAccountContext(sourceKey)
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
|
||||
throw (
|
||||
lastError ||
|
||||
createHttpError(
|
||||
"所有 cloudtentacles 账号均不可用,请先到平台配置完成登录校验",
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_all_source_keys_exhausted" }
|
||||
)
|
||||
);
|
||||
createHttpError('所有 cloudtentacles 账号均不可用,请先到平台配置完成登录校验', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_all_source_keys_exhausted',
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function resolveSingleCloudtentaclesAccountContext(sourceKey: string) {
|
||||
const source = getCloudtentaclesSourceByKey(sourceKey);
|
||||
const session = getCloudtentaclesSessionStateByKey(sourceKey);
|
||||
const source = getCloudtentaclesSourceByKey(sourceKey)
|
||||
const session = getCloudtentaclesSessionStateByKey(sourceKey)
|
||||
|
||||
if (!source) {
|
||||
throw createHttpError(
|
||||
`cloudtentacles 账号 ${sourceKey} 不存在(取号账号已变更或被删除,无法继续操作其虚拟号)`,
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_source_missing" }
|
||||
);
|
||||
{ statusCode: 409, errorCode: 'kuaishou_cloud_source_missing' },
|
||||
)
|
||||
}
|
||||
|
||||
if (source.enabled === false) {
|
||||
throw createHttpError(
|
||||
`cloudtentacles 账号 ${sourceKey} 已停用`,
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_source_disabled" }
|
||||
);
|
||||
throw createHttpError(`cloudtentacles 账号 ${sourceKey} 已停用`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_source_disabled',
|
||||
})
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
throw createHttpError(
|
||||
`cloudtentacles 账号 ${sourceKey} 没有可用 token`,
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" }
|
||||
);
|
||||
throw createHttpError(`cloudtentacles 账号 ${sourceKey} 没有可用 token`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_missing_cloud_token',
|
||||
})
|
||||
}
|
||||
|
||||
const token = String(session.token || "").trim();
|
||||
const token = String(session.token || '').trim()
|
||||
if (!token) {
|
||||
throw createHttpError(
|
||||
`cloudtentacles 账号 ${sourceKey} 没有可用 token`,
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" }
|
||||
);
|
||||
throw createHttpError(`cloudtentacles 账号 ${sourceKey} 没有可用 token`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_missing_cloud_token',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl:
|
||||
String(session.baseUrl || source.baseUrl || "").trim() ||
|
||||
"https://123.207.217.176",
|
||||
baseUrl: String(session.baseUrl || source.baseUrl || '').trim() || 'https://123.207.217.176',
|
||||
token,
|
||||
deviceId: normalizeCloudtentaclesDeviceId(
|
||||
session.deviceId || source.deviceId
|
||||
),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(
|
||||
session.deviceType ?? source.deviceType
|
||||
),
|
||||
deviceId: normalizeCloudtentaclesDeviceId(session.deviceId || source.deviceId),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(session.deviceType ?? source.deviceType),
|
||||
// 透传给 http-client / 错误包装,便于定位跨账号问题
|
||||
sourceKey,
|
||||
resolvedSourceKey: sourceKey,
|
||||
accountLabel: String(source.label || sourceKey).trim() || sourceKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ export async function confirmKuaishouCloudTaskRole(
|
||||
}
|
||||
|
||||
const expectedUid = assertClaimExpectedUidReady(task)
|
||||
const mockMode = isKuaishouCloudMockTask(task) || isKuaishouCloudMockContext(parseTaskContext(task))
|
||||
const mockMode =
|
||||
isKuaishouCloudMockTask(task) || isKuaishouCloudMockContext(parseTaskContext(task))
|
||||
|
||||
const refreshed = mockMode
|
||||
? { task }
|
||||
@@ -58,9 +59,7 @@ export async function confirmKuaishouCloudTaskRole(
|
||||
forceProbe: options.forceProbe !== false,
|
||||
})
|
||||
|
||||
const flow = normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(refreshed.task).kuaishouCloudFulfillment,
|
||||
)
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
|
||||
|
||||
if (!flow.binding.vnPhone || !flow.binding.roleName || !flow.binding.roleId) {
|
||||
throw createHttpError('角色信息还未刷新到系统,请完成绑定后稍等片刻再试', {
|
||||
|
||||
@@ -68,8 +68,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
||||
const industryVoucherCode = String(
|
||||
voucherContext.voucherCode || voucherContext.eticketId || '',
|
||||
).trim()
|
||||
const hasIndustryVoucherForDispatch =
|
||||
Boolean(industryVoucherCode) || isIndustryEVoucherTask(task)
|
||||
const hasIndustryVoucherForDispatch = Boolean(industryVoucherCode) || isIndustryEVoucherTask(task)
|
||||
const resolvedTicketCode = persistedTicketCode || industryVoucherCode
|
||||
|
||||
if (!resolvedTicketCode && !hasIndustryVoucherForDispatch) {
|
||||
@@ -190,9 +189,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
||||
purchaseTriggered: stockResult.purchaseTriggered,
|
||||
assetBefore: stockResult.assetBefore,
|
||||
assetAfter: stockResult.assetAfter,
|
||||
purchaseAt: stockResult.purchaseTriggered
|
||||
? now
|
||||
: syncedFlow.purchase.purchaseAt,
|
||||
purchaseAt: stockResult.purchaseTriggered ? now : syncedFlow.purchase.purchaseAt,
|
||||
items: stockResult.items,
|
||||
},
|
||||
},
|
||||
@@ -255,9 +252,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
||||
|
||||
return {
|
||||
task: updatedTask,
|
||||
flow: normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(updatedTask).kuaishouCloudFulfillment,
|
||||
),
|
||||
flow: normalizeKuaishouCloudFlow(parseTaskContext(updatedTask).kuaishouCloudFulfillment),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +1,65 @@
|
||||
import { createTaskEvent } from "../../../repositories/task-event-repo.js";
|
||||
import { updateTask } from "../../../repositories/task-repo.js";
|
||||
import type { TaskRow } from "../../../types/repository/rows.js";
|
||||
import { createHttpError } from "../../../utils/http.js";
|
||||
import { getCloudtentaclesBindInfo } from "../../platforms/cloudtentacles/virtual-number-service.js";
|
||||
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
|
||||
import { updateTask } from '../../../repositories/task-repo.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { getCloudtentaclesBindInfo } from '../../platforms/cloudtentacles/virtual-number-service.js'
|
||||
import {
|
||||
assertLewanAutoFulfillmentUidReady,
|
||||
isClaimUidMatched,
|
||||
normalizeClaimUid,
|
||||
} from "../../claim/claim-identity.js";
|
||||
import { asJsonObject } from "../../../types/json.js";
|
||||
} from '../../claim/claim-identity.js'
|
||||
import { asJsonObject } from '../../../types/json.js'
|
||||
import {
|
||||
normalizeKuaishouCloudFlow,
|
||||
normalizeKuaishouCloudRoleInfo,
|
||||
type JsonObject,
|
||||
} from "./domain.js";
|
||||
} from './domain.js'
|
||||
|
||||
export async function syncKuaishouCloudRoleInfoBeforeDispatch(
|
||||
task: TaskRow,
|
||||
input: JsonObject = {}
|
||||
input: JsonObject = {},
|
||||
) {
|
||||
const now = String(input.now || "").trim() || new Date().toISOString();
|
||||
const source =
|
||||
String(input.source || "system_before_dispatch").trim() ||
|
||||
"system_before_dispatch";
|
||||
const now = String(input.now || '').trim() || new Date().toISOString()
|
||||
const source = String(input.source || 'system_before_dispatch').trim() || 'system_before_dispatch'
|
||||
const errorCodePrefix =
|
||||
String(input.errorCodePrefix || "kuaishou_cloud").trim() ||
|
||||
"kuaishou_cloud";
|
||||
const cloudContext =
|
||||
asJsonObject(input.cloudContext);
|
||||
const taskContext =
|
||||
asJsonObject(input.taskContext);
|
||||
const flow = normalizeKuaishouCloudFlow(
|
||||
input.flow || taskContext.kuaishouCloudFulfillment
|
||||
);
|
||||
String(input.errorCodePrefix || 'kuaishou_cloud').trim() || 'kuaishou_cloud'
|
||||
const cloudContext = asJsonObject(input.cloudContext)
|
||||
const taskContext = asJsonObject(input.taskContext)
|
||||
const flow = normalizeKuaishouCloudFlow(input.flow || taskContext.kuaishouCloudFulfillment)
|
||||
|
||||
// 新策略:lewan 自动发货必须有 expectedUid,旧单无 UID 不可静默回落
|
||||
const expectedUid = assertLewanAutoFulfillmentUidReady(task, {
|
||||
allowMockSkip: true,
|
||||
errorCodePrefix,
|
||||
});
|
||||
})
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||
throw createHttpError("当前任务缺少可同步角色的虚拟号信息,暂时不能发货", {
|
||||
throw createHttpError('当前任务缺少可同步角色的虚拟号信息,暂时不能发货', {
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_missing_bind_info_context`,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const bindInfoResult = await getCloudtentaclesBindInfo({
|
||||
...cloudContext,
|
||||
key: flow.binding.vnKey,
|
||||
id: flow.binding.vnId,
|
||||
});
|
||||
const roleInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo);
|
||||
})
|
||||
const roleInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo)
|
||||
|
||||
if (!roleInfo.name || !roleInfo.rid) {
|
||||
throw createHttpError(
|
||||
"cloudtentacles 还没有同步到客户绑定角色,请稍后刷新角色信息后再发货",
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_role_not_ready`,
|
||||
}
|
||||
);
|
||||
throw createHttpError('cloudtentacles 还没有同步到客户绑定角色,请稍后刷新角色信息后再发货', {
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_role_not_ready`,
|
||||
})
|
||||
}
|
||||
|
||||
const liveBoundUid = normalizeClaimUid(roleInfo.rid);
|
||||
const liveBoundUid = normalizeClaimUid(roleInfo.rid)
|
||||
|
||||
if (expectedUid && !isClaimUidMatched(expectedUid, liveBoundUid)) {
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
"kuaishou_cloud_dispatch_uid_mismatch",
|
||||
'kuaishou_cloud_dispatch_uid_mismatch',
|
||||
{
|
||||
source,
|
||||
vnId: flow.binding.vnId,
|
||||
@@ -78,16 +68,16 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
|
||||
cloudtentaclesRoleId: roleInfo.rid,
|
||||
actor: input.actor || null,
|
||||
},
|
||||
now
|
||||
);
|
||||
now,
|
||||
)
|
||||
|
||||
throw createHttpError(
|
||||
`cloudtentacles 当前绑定角色 ID(${roleInfo.rid})与用户填写 UID(${expectedUid})不一致,不能发货`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: `${errorCodePrefix}_dispatch_uid_mismatch`,
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const nextFlow = normalizeKuaishouCloudFlow({
|
||||
@@ -99,36 +89,36 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
|
||||
},
|
||||
role: {
|
||||
...flow.role,
|
||||
status: "ready",
|
||||
status: 'ready',
|
||||
name: roleInfo.name,
|
||||
rid: roleInfo.rid,
|
||||
refreshedAt: now,
|
||||
errorMessage: "",
|
||||
errorMessage: '',
|
||||
rawInfo: roleInfo.rawInfo,
|
||||
},
|
||||
});
|
||||
})
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: nextFlow,
|
||||
};
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
role_id: roleInfo.rid,
|
||||
role_name: roleInfo.name,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
});
|
||||
})
|
||||
|
||||
if (!updatedTask) {
|
||||
throw createHttpError("发货前角色信息同步失败", {
|
||||
throw createHttpError('发货前角色信息同步失败', {
|
||||
statusCode: 500,
|
||||
errorCode: `${errorCodePrefix}_dispatch_role_update_failed`,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
"kuaishou_cloud_role_info_synced_before_dispatch",
|
||||
'kuaishou_cloud_role_info_synced_before_dispatch',
|
||||
{
|
||||
source,
|
||||
roleName: roleInfo.name,
|
||||
@@ -136,15 +126,13 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
|
||||
vnId: flow.binding.vnId,
|
||||
actor: input.actor || null,
|
||||
},
|
||||
now
|
||||
);
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: updatedTask,
|
||||
taskContext: nextContext,
|
||||
flow: nextFlow,
|
||||
roleInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -51,9 +51,8 @@ export function resolveDispatchDeliveryItems(flow: JsonObject): DispatchDelivery
|
||||
return items
|
||||
}
|
||||
|
||||
const binding = flow.binding && typeof flow.binding === 'object'
|
||||
? (flow.binding as JsonObject)
|
||||
: {}
|
||||
const binding =
|
||||
flow.binding && typeof flow.binding === 'object' ? (flow.binding as JsonObject) : {}
|
||||
const fallbackSkuId = Number(binding.skuId || 0) || 0
|
||||
if (!fallbackSkuId) {
|
||||
return []
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js'
|
||||
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../../utils/masking.js'
|
||||
import {
|
||||
isTaskFinalStatus,
|
||||
normalizeTaskStatus,
|
||||
TASK_STATUS,
|
||||
} from '../../../domain/task-status.js'
|
||||
import { isTaskFinalStatus, normalizeTaskStatus, TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import { asJsonObject, type JsonObject } from '../../../types/json.js'
|
||||
|
||||
export type { JsonObject }
|
||||
@@ -138,8 +134,7 @@ export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow {
|
||||
const role = asJsonObject(source.role)
|
||||
const purchase = asJsonObject(source.purchase)
|
||||
const dispatch = asJsonObject(source.dispatch)
|
||||
const returnNumber =
|
||||
asJsonObject(source.returnNumber)
|
||||
const returnNumber = asJsonObject(source.returnNumber)
|
||||
const consume = asJsonObject(source.consume)
|
||||
const ticket = asJsonObject(source.ticket)
|
||||
const rebind = asJsonObject(source.rebind)
|
||||
@@ -280,7 +275,8 @@ export function normalizeKuaishouCloudShippedSnapshot(
|
||||
roleName: String(source.roleName || source.name || '').trim(),
|
||||
vnId,
|
||||
vnPhone: String(source.vnPhone || '').trim(),
|
||||
vnKey: String(source.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
vnKey:
|
||||
String(source.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
dispatchedAt,
|
||||
}
|
||||
}
|
||||
@@ -331,8 +327,7 @@ export function buildKuaishouCloudShippedSnapshot(input: {
|
||||
roleName: String(input.roleName || '').trim(),
|
||||
vnId: Number(input.vnId || 0) || 0,
|
||||
vnPhone: String(input.vnPhone || '').trim(),
|
||||
vnKey:
|
||||
String(input.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
vnKey: String(input.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
dispatchedAt: input.dispatchedAt ? String(input.dispatchedAt) : null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,7 @@ import {
|
||||
} from './account-selector.js'
|
||||
import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
|
||||
import { wrapCloudtentaclesOperationError } from './cloudtentacles-errors.js'
|
||||
import {
|
||||
getTaskClaimExpiresAt,
|
||||
normalizeActor,
|
||||
parseTaskContext,
|
||||
} from './task-context.js'
|
||||
import { getTaskClaimExpiresAt, normalizeActor, parseTaskContext } from './task-context.js'
|
||||
import { resolveKuaishouCloudDeliveryPlan } from './delivery-plan.js'
|
||||
import { ensureTaskClaimLink } from './ensure-claim-link.js'
|
||||
import {
|
||||
@@ -332,11 +328,14 @@ export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options
|
||||
}
|
||||
|
||||
function isRetryableCloudtentaclesSourceError(error: unknown) {
|
||||
const current = error && typeof error === 'object' ? error as JsonObject : {}
|
||||
const current = error && typeof error === 'object' ? (error as JsonObject) : {}
|
||||
const code = String(current.errorCode || current.code || '').trim()
|
||||
return (
|
||||
code === 'cloudtentacles_vn_quota_cooldown' ||
|
||||
(code === 'cloudtentacles_vn_appoint_failed' && String(current.message || '').trim().includes('最多同时占用')) ||
|
||||
(code === 'cloudtentacles_vn_appoint_failed' &&
|
||||
String(current.message || '')
|
||||
.trim()
|
||||
.includes('最多同时占用')) ||
|
||||
code === 'cloudtentacles_vn_list_failed' ||
|
||||
code === 'cloudtentacles_sku_list_failed' ||
|
||||
code === 'cloudtentacles_knapsack_failed' ||
|
||||
@@ -484,8 +483,9 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
|
||||
binding: {
|
||||
...flow.binding,
|
||||
prepareStatus: 'ready',
|
||||
resolvedSourceKey:
|
||||
String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(),
|
||||
resolvedSourceKey: String(
|
||||
cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '',
|
||||
).trim(),
|
||||
bindUrl,
|
||||
bindPreparedAt: now,
|
||||
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
|
||||
@@ -667,8 +667,9 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
|
||||
binding: {
|
||||
...flow.binding,
|
||||
prepareStatus: 'ready',
|
||||
resolvedSourceKey:
|
||||
String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(),
|
||||
resolvedSourceKey: String(
|
||||
cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '',
|
||||
).trim(),
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId: preparedBinding.vnId,
|
||||
vnPhone: preparedBinding.vnPhone,
|
||||
@@ -739,7 +740,6 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function markKuaishouCloudBindUrlRefreshFailed(
|
||||
task: TaskRow,
|
||||
{
|
||||
@@ -855,7 +855,7 @@ async function clearKuaishouCloudStaleBinding(
|
||||
error: unknown
|
||||
claimUrl: string
|
||||
token: string
|
||||
}
|
||||
},
|
||||
) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error || '绑定已失效')
|
||||
const oldVnId = flow.binding.vnId
|
||||
@@ -912,7 +912,7 @@ async function clearKuaishouCloudStaleBinding(
|
||||
errorMessage,
|
||||
actor,
|
||||
},
|
||||
now
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -51,9 +51,7 @@ export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: Json
|
||||
|
||||
const probeIntervalMs =
|
||||
Number(resolveCloudtentaclesConfig().bindUrlProbeIntervalSeconds || 30) * 1000
|
||||
const lastProbeAt = flow.binding.bindProbeAt
|
||||
? Date.parse(String(flow.binding.bindProbeAt))
|
||||
: NaN
|
||||
const lastProbeAt = flow.binding.bindProbeAt ? Date.parse(String(flow.binding.bindProbeAt)) : NaN
|
||||
if (
|
||||
!options.force &&
|
||||
Number.isFinite(lastProbeAt) &&
|
||||
@@ -187,4 +185,3 @@ async function markKuaishouCloudBindUrlRefreshFailed(
|
||||
|
||||
return updatedTask
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,7 @@ import {
|
||||
isCloudtentaclesPermissionLikeError,
|
||||
wrapCloudtentaclesOperationError,
|
||||
} from './cloudtentacles-errors.js'
|
||||
import {
|
||||
getTaskClaimExpiresAt,
|
||||
normalizeActor,
|
||||
parseTaskContext,
|
||||
} from './task-context.js'
|
||||
import { getTaskClaimExpiresAt, normalizeActor, parseTaskContext } from './task-context.js'
|
||||
import { ensureTaskClaimLink } from './ensure-claim-link.js'
|
||||
import {
|
||||
buildPendingRoleWithDefaultSnapshot,
|
||||
@@ -115,7 +111,8 @@ export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonOb
|
||||
bindExpiresAt: flow.binding.bindExpiresAt,
|
||||
roleName: flow.binding.roleName || flow.role.name || task.role_name || '',
|
||||
roleId: flow.binding.roleId || flow.role.rid || task.role_id || '',
|
||||
resolvedSourceKey: flow.binding.resolvedSourceKey || String(cloudContext.resolvedSourceKey || ''),
|
||||
resolvedSourceKey:
|
||||
flow.binding.resolvedSourceKey || String(cloudContext.resolvedSourceKey || ''),
|
||||
}
|
||||
const previousRebind: JsonObject =
|
||||
flow.rebind && typeof flow.rebind === 'object' ? (flow.rebind as JsonObject) : {}
|
||||
@@ -339,8 +336,9 @@ export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonOb
|
||||
binding: {
|
||||
...flow.binding,
|
||||
prepareStatus: 'ready',
|
||||
resolvedSourceKey:
|
||||
String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(),
|
||||
resolvedSourceKey: String(
|
||||
cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '',
|
||||
).trim(),
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId: preparedBinding.vnId,
|
||||
vnPhone: preparedBinding.vnPhone,
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
canRedeemKuaishouCloudClaimStatus,
|
||||
isKuaishouCloudRedeemSettledStatus,
|
||||
} from '../../../domain/task-status.js'
|
||||
import { assertBoundUidMatchesExpected, assertClaimExpectedUidReady } from '../../claim/claim-identity.js'
|
||||
import {
|
||||
assertBoundUidMatchesExpected,
|
||||
assertClaimExpectedUidReady,
|
||||
} from '../../claim/claim-identity.js'
|
||||
|
||||
/**
|
||||
* redeem 用例前置条件单测(不打 DB / CT)。
|
||||
@@ -73,10 +76,7 @@ test('redeem 前置:uid 不一致拒绝', () => {
|
||||
})
|
||||
|
||||
test('redeem 闸门函数与 claim-identity 一致', () => {
|
||||
assert.throws(
|
||||
() => assertClaimExpectedUidReady({ context_json: '{}' }),
|
||||
/填写游戏 UID/,
|
||||
)
|
||||
assert.throws(() => assertClaimExpectedUidReady({ context_json: '{}' }), /填写游戏 UID/)
|
||||
assert.throws(
|
||||
() =>
|
||||
assertBoundUidMatchesExpected(
|
||||
|
||||
@@ -20,10 +20,7 @@ import {
|
||||
} from '../../claim/claim-identity.js'
|
||||
import { isKuaishouCloudTask, normalizeKuaishouCloudFlow, type JsonObject } from './domain.js'
|
||||
import { dispatchKuaishouCloudFulfillmentTask } from './dispatch-fulfillment.js'
|
||||
import {
|
||||
completeMockKuaishouCloudTask,
|
||||
isKuaishouCloudMockTask,
|
||||
} from './mock-helpers.js'
|
||||
import { completeMockKuaishouCloudTask, isKuaishouCloudMockTask } from './mock-helpers.js'
|
||||
import { normalizeActor, parseTaskContext } from './task-context.js'
|
||||
|
||||
export type RedeemKuaishouCloudTaskResult = {
|
||||
@@ -74,9 +71,7 @@ export async function redeemKuaishouCloudTask(
|
||||
|
||||
// WAITING_BINDING + UID 匹配:自动升为 ROLE_CONFIRMED,实现一键兑换
|
||||
if (currentStatus === TASK_STATUS.WAITING_BINDING) {
|
||||
const flow = normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(workingTask).kuaishouCloudFulfillment,
|
||||
)
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(workingTask).kuaishouCloudFulfillment)
|
||||
if (!isKuaishouCloudMockTask(workingTask)) {
|
||||
assertBoundUidMatchesExpected(workingTask, flow, {
|
||||
errorCodePrefix,
|
||||
@@ -122,16 +117,12 @@ export async function redeemKuaishouCloudTask(
|
||||
assertClaimExpectedUidReady(workingTask)
|
||||
}
|
||||
|
||||
const lockedTask = await updateTaskStatusIfCurrent(
|
||||
workingTask.id,
|
||||
TASK_STATUS.ROLE_CONFIRMED,
|
||||
{
|
||||
task_status: TASK_STATUS.REDEEMING,
|
||||
user_action_status: 'not_required',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
},
|
||||
)
|
||||
const lockedTask = await updateTaskStatusIfCurrent(workingTask.id, TASK_STATUS.ROLE_CONFIRMED, {
|
||||
task_status: TASK_STATUS.REDEEMING,
|
||||
user_action_status: 'not_required',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
if (!lockedTask) {
|
||||
const latest = (await getTaskById(workingTask.id)) || workingTask
|
||||
@@ -174,8 +165,7 @@ export async function redeemKuaishouCloudTask(
|
||||
throw error
|
||||
}
|
||||
|
||||
const message =
|
||||
error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理'
|
||||
const message = error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理'
|
||||
await updateTask(lockedTask.id, {
|
||||
task_status: TASK_STATUS.MANUAL_REVIEW,
|
||||
user_action_status: 'not_required',
|
||||
|
||||
@@ -23,10 +23,7 @@ import { normalizeActor, parseTaskContext } from './task-context.js'
|
||||
* - 默认(发货后 autoFinalize):退号 + 尝试行业电子凭证核销收尾
|
||||
* - `consumeIndustryVoucher: false`(admin 清理):只退虚拟号,避免占号;与核销无关
|
||||
*/
|
||||
export async function returnKuaishouCloudFulfillmentTask(
|
||||
task: TaskRow,
|
||||
options: JsonObject = {},
|
||||
) {
|
||||
export async function returnKuaishouCloudFulfillmentTask(task: TaskRow, options: JsonObject = {}) {
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
|
||||
statusCode: 409,
|
||||
@@ -79,7 +76,7 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
errorMessage: error instanceof Error ? error.message : String(error || ''),
|
||||
actor,
|
||||
},
|
||||
now
|
||||
now,
|
||||
)
|
||||
} else {
|
||||
throw error
|
||||
@@ -113,8 +110,7 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
} else if (shouldConsumeIndustryVoucher) {
|
||||
const industryResult = hasIndustryVoucher
|
||||
? await consumeKuaishouIndustryVouchersForTask(task, {
|
||||
source:
|
||||
String(options.source || 'system_auto_finalize').trim() || 'system_auto_finalize',
|
||||
source: String(options.source || 'system_auto_finalize').trim() || 'system_auto_finalize',
|
||||
token: String(
|
||||
isPlainObject(taskContext.kuaishouIndustryVoucher)
|
||||
? taskContext.kuaishouIndustryVoucher.token || ''
|
||||
@@ -122,7 +118,11 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
).trim(),
|
||||
consumeTime: Date.now(),
|
||||
})
|
||||
: { ok: true, consumed: [] as Array<Record<string, unknown>>, failed: [] as Array<{ errorMessage?: string }> }
|
||||
: {
|
||||
ok: true,
|
||||
consumed: [] as Array<Record<string, unknown>>,
|
||||
failed: [] as Array<{ errorMessage?: string }>,
|
||||
}
|
||||
|
||||
if (industryResult.ok) {
|
||||
consumeStatus = 'success'
|
||||
@@ -158,8 +158,7 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
if (shouldConsumeIndustryVoucher) {
|
||||
nextTaskStatus = TASK_STATUS.MANUAL_REVIEW
|
||||
nextResultCode = 'kuaishou_cloud_consume_failed'
|
||||
nextResultMessage =
|
||||
consumeErrorMessage || '号码已退还,但电子凭证核销未完成,请人工处理'
|
||||
nextResultMessage = consumeErrorMessage || '号码已退还,但电子凭证核销未完成,请人工处理'
|
||||
} else {
|
||||
nextResultCode = allowConsumeIndustryVoucher
|
||||
? 'kuaishou_cloud_completed_without_eticket_consume'
|
||||
@@ -256,12 +255,15 @@ export async function returnKuaishouCloudFulfillmentTask(
|
||||
}
|
||||
}
|
||||
|
||||
function hasKuaishouIndustryVoucherContext(value: JsonObject): boolean { const voucher = isPlainObject(value.kuaishouIndustryVoucher)
|
||||
? value.kuaishouIndustryVoucher
|
||||
: {}
|
||||
function hasKuaishouIndustryVoucherContext(value: JsonObject): boolean {
|
||||
const voucher = isPlainObject(value.kuaishouIndustryVoucher) ? value.kuaishouIndustryVoucher : {}
|
||||
const voucherCode = String(voucher.voucherCode || voucher.eticketId || '').trim()
|
||||
const status = String(voucher.status || 'UNUSED').trim().toUpperCase()
|
||||
const sendCallbackStatus = String(voucher.sendCallbackStatus || 'success').trim().toLowerCase()
|
||||
const status = String(voucher.status || 'UNUSED')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
const sendCallbackStatus = String(voucher.sendCallbackStatus || 'success')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return Boolean(voucherCode && status !== 'DESTROYED' && sendCallbackStatus === 'success')
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { parseTaskContext as parseTaskContextValue } from "../../../utils/task-json.js";
|
||||
import type { TaskRow } from "../../../types/repository/rows.js";
|
||||
import { parseTaskContext as parseTaskContextValue } from '../../../utils/task-json.js'
|
||||
import type { TaskRow } from '../../../types/repository/rows.js'
|
||||
import type { JsonObject } from '../../../types/json.js'
|
||||
|
||||
export function normalizeActor(actor: unknown) {
|
||||
if (!actor || typeof actor !== "object") {
|
||||
return null;
|
||||
if (!actor || typeof actor !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const current = actor as JsonObject;
|
||||
const source = String(current.source || "").trim();
|
||||
const userId = Number(current.userId || 0) || 0;
|
||||
const username = String(current.username || "").trim();
|
||||
const role = String(current.role || "").trim();
|
||||
const current = actor as JsonObject
|
||||
const source = String(current.source || '').trim()
|
||||
const userId = Number(current.userId || 0) || 0
|
||||
const username = String(current.username || '').trim()
|
||||
const role = String(current.role || '').trim()
|
||||
|
||||
if (!source && !userId && !username && !role) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -22,22 +22,24 @@ export function normalizeActor(actor: unknown) {
|
||||
userId,
|
||||
username,
|
||||
role,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function parseTaskContext(task: Partial<TaskRow> | null | undefined) {
|
||||
return parseTaskContextValue(task);
|
||||
return parseTaskContextValue(task)
|
||||
}
|
||||
|
||||
export function getTaskClaimExpiresAt(task: Partial<TaskRow> | null | undefined) {
|
||||
return task?.claim_expires_at || task?.primary_claim_expires_at || null;
|
||||
return task?.claim_expires_at || task?.primary_claim_expires_at || null
|
||||
}
|
||||
|
||||
export function isClaimExpired(expiredAt: unknown) {
|
||||
if (!expiredAt) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
const timestamp = new Date(expiredAt instanceof Date ? expiredAt : String(expiredAt || "")).getTime();
|
||||
return Number.isFinite(timestamp) && timestamp <= Date.now();
|
||||
const timestamp = new Date(
|
||||
expiredAt instanceof Date ? expiredAt : String(expiredAt || ''),
|
||||
).getTime()
|
||||
return Number.isFinite(timestamp) && timestamp <= Date.now()
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { buildDispatchStockItems } from "./task-finalization.js";
|
||||
import { buildDispatchStockItems } from './task-finalization.js'
|
||||
|
||||
test("buildDispatchStockItems 只计算兑换阶段需要补买的库存缺口", () => {
|
||||
test('buildDispatchStockItems 只计算兑换阶段需要补买的库存缺口', () => {
|
||||
const result = buildDispatchStockItems(
|
||||
[
|
||||
{
|
||||
cloudSkuId: 101,
|
||||
cloudSkuName: "",
|
||||
cloudSkuName: '',
|
||||
quantity: 3,
|
||||
},
|
||||
],
|
||||
@@ -16,38 +16,38 @@ test("buildDispatchStockItems 只计算兑换阶段需要补买的库存缺口",
|
||||
skuItems: [
|
||||
{
|
||||
id: 101,
|
||||
name: "套装-暗影哥特",
|
||||
name: '套装-暗影哥特',
|
||||
price: 88,
|
||||
},
|
||||
],
|
||||
knapsackItems: [
|
||||
{
|
||||
id: 101,
|
||||
name: "套装-暗影哥特",
|
||||
name: '套装-暗影哥特',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(result, [
|
||||
{
|
||||
cloudSkuId: 101,
|
||||
cloudSkuName: "套装-暗影哥特",
|
||||
cloudSkuName: '套装-暗影哥特',
|
||||
quantity: 3,
|
||||
requiredCount: 3,
|
||||
knapsackCount: 1,
|
||||
purchasedCount: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
])
|
||||
})
|
||||
|
||||
test("buildDispatchStockItems 背包足够时不需要补买", () => {
|
||||
test('buildDispatchStockItems 背包足够时不需要补买', () => {
|
||||
const result = buildDispatchStockItems(
|
||||
[
|
||||
{
|
||||
cloudSkuId: 202,
|
||||
cloudSkuName: "套装-浪漫天命",
|
||||
cloudSkuName: '套装-浪漫天命',
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
@@ -55,13 +55,13 @@ test("buildDispatchStockItems 背包足够时不需要补买", () => {
|
||||
knapsackItems: [
|
||||
{
|
||||
id: 202,
|
||||
name: "套装-浪漫天命",
|
||||
name: '套装-浪漫天命',
|
||||
count: 5,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(result[0]?.knapsackCount, 5);
|
||||
assert.equal(result[0]?.purchasedCount, 0);
|
||||
});
|
||||
assert.equal(result[0]?.knapsackCount, 5)
|
||||
assert.equal(result[0]?.purchasedCount, 0)
|
||||
})
|
||||
|
||||
@@ -64,43 +64,56 @@ test('resolveKuaishouFeifeiH5UrlWithUid appends uid query', () => {
|
||||
|
||||
test('buildFeifeiPlatformOrderNo prefers source order number', () => {
|
||||
assert.equal(
|
||||
buildFeifeiPlatformOrderNo(createTask({
|
||||
platform_order_id: 'KS202607080001',
|
||||
task_no: 'DT600a8bd23b8',
|
||||
})),
|
||||
buildFeifeiPlatformOrderNo(
|
||||
createTask({
|
||||
platform_order_id: 'KS202607080001',
|
||||
task_no: 'DT600a8bd23b8',
|
||||
}),
|
||||
),
|
||||
'KS202607080001',
|
||||
)
|
||||
})
|
||||
|
||||
test('buildFeifeiPlatformOrderNo falls back to task number', () => {
|
||||
assert.equal(
|
||||
buildFeifeiPlatformOrderNo(createTask({
|
||||
platform_order_id: '',
|
||||
task_no: 'DT600a8bd23b8',
|
||||
})),
|
||||
buildFeifeiPlatformOrderNo(
|
||||
createTask({
|
||||
platform_order_id: '',
|
||||
task_no: 'DT600a8bd23b8',
|
||||
}),
|
||||
),
|
||||
'DT600a8bd23b8',
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveKuaishouFeifeiPlatformAmount converts order fen amount to yuan', () => {
|
||||
assert.equal(resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 3800,
|
||||
quantity: 1,
|
||||
}), 38)
|
||||
assert.equal(
|
||||
resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 3800,
|
||||
quantity: 1,
|
||||
}),
|
||||
38,
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveKuaishouFeifeiPlatformAmount splits amount by item quantity', () => {
|
||||
assert.equal(resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 7600,
|
||||
quantity: 2,
|
||||
}), 38)
|
||||
assert.equal(
|
||||
resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 7600,
|
||||
quantity: 2,
|
||||
}),
|
||||
38,
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveKuaishouFeifeiPlatformAmount skips empty amount', () => {
|
||||
assert.equal(resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 0,
|
||||
quantity: 1,
|
||||
}), 0)
|
||||
assert.equal(
|
||||
resolveKuaishouFeifeiPlatformAmount({
|
||||
totalAmountFen: 0,
|
||||
quantity: 1,
|
||||
}),
|
||||
0,
|
||||
)
|
||||
})
|
||||
|
||||
function createTask(patch: Partial<TaskRow> = {}): TaskRow {
|
||||
|
||||
@@ -173,19 +173,25 @@ export async function syncKuaishouFeifeiTaskStatus(task: TaskRow) {
|
||||
resultMessage = consumeResult.failed[0]?.errorMessage || '电子凭证核销失败,请人工处理'
|
||||
lastError = resultMessage
|
||||
nextFlow.consumeStatus = 'failed'
|
||||
logIntegration('[kuaishou-feifei]', '飞飞履约完成,但行业电子凭证核销失败', {
|
||||
taskId: task.id,
|
||||
platformOrderNo: nextFlow.platformOrderNo,
|
||||
orderNo: nextFlow.orderNo,
|
||||
voucherCount: consumeResult.vouchers.length,
|
||||
failedCount: consumeResult.failed.length,
|
||||
errorMessage: resultMessage,
|
||||
}, { level: 'warn' })
|
||||
logIntegration(
|
||||
'[kuaishou-feifei]',
|
||||
'飞飞履约完成,但行业电子凭证核销失败',
|
||||
{
|
||||
taskId: task.id,
|
||||
platformOrderNo: nextFlow.platformOrderNo,
|
||||
orderNo: nextFlow.orderNo,
|
||||
voucherCount: consumeResult.vouchers.length,
|
||||
failedCount: consumeResult.failed.length,
|
||||
errorMessage: resultMessage,
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
} else if ([40, 50, 60].includes(order.rechargeStatus)) {
|
||||
nextTaskStatus = order.rechargeStatus === 60 ? TASK_STATUS.CLOSED : TASK_STATUS.MANUAL_REVIEW
|
||||
resultCode = `kuaishou_feifei_status_${order.rechargeStatus}`
|
||||
resultMessage = order.rechargeResultMessage || order.rechargeStatusLabel || 'kuaishou-feifei 履约异常'
|
||||
resultMessage =
|
||||
order.rechargeResultMessage || order.rechargeStatusLabel || 'kuaishou-feifei 履约异常'
|
||||
lastError = resultMessage
|
||||
}
|
||||
|
||||
@@ -229,10 +235,9 @@ export type KuaishouFeifeiFlow = {
|
||||
}
|
||||
|
||||
export function normalizeKuaishouFeifeiFlow(value: unknown): KuaishouFeifeiFlow {
|
||||
const source = value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
const h5 = source.h5 && typeof source.h5 === 'object' ? source.h5 as JsonObject : {}
|
||||
const source =
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
const h5 = source.h5 && typeof source.h5 === 'object' ? (source.h5 as JsonObject) : {}
|
||||
|
||||
return {
|
||||
flowType: 'kuaishou_feifei',
|
||||
@@ -263,10 +268,7 @@ export function resolveKuaishouFeifeiClaimUrl(value: unknown) {
|
||||
}
|
||||
|
||||
/** 在已有 expectedUid 时返回拼好 uid 的 H5 链接。 */
|
||||
export function resolveKuaishouFeifeiH5UrlWithUid(
|
||||
value: unknown,
|
||||
expectedUid?: unknown,
|
||||
) {
|
||||
export function resolveKuaishouFeifeiH5UrlWithUid(value: unknown, expectedUid?: unknown) {
|
||||
const directUrl = resolveKuaishouFeifeiClaimUrl(value)
|
||||
if (!directUrl) {
|
||||
return ''
|
||||
@@ -319,9 +321,7 @@ function mergeKuaishouFeifeiOrder(
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKuaishouFeifeiDirectClaimUrl(
|
||||
flow: KuaishouFeifeiFlow,
|
||||
) {
|
||||
function resolveKuaishouFeifeiDirectClaimUrl(flow: KuaishouFeifeiFlow) {
|
||||
const h5ClaimUrl = flow.h5.rechargeUrl || flow.h5.entryUrl
|
||||
if (h5ClaimUrl) {
|
||||
return h5ClaimUrl
|
||||
@@ -335,7 +335,9 @@ function resolveKuaishouFeifeiDirectClaimUrl(
|
||||
return isLocalClaimUrl(flow.claimUrl) ? '' : flow.claimUrl
|
||||
}
|
||||
|
||||
function resolveKuaishouFeifeiOrderClaimUrl(order: Awaited<ReturnType<typeof createKuaishouFeifeiOrder>>) {
|
||||
function resolveKuaishouFeifeiOrderClaimUrl(
|
||||
order: Awaited<ReturnType<typeof createKuaishouFeifeiOrder>>,
|
||||
) {
|
||||
return String(order.h5.rechargeUrl || order.h5.entryUrl || '').trim()
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,9 @@ test('resolveOrderFulfillmentReadiness 电子凭证发码未确认时阻止履
|
||||
})
|
||||
})
|
||||
|
||||
function createVoucher(patch: Partial<KuaishouIndustryVoucherRow> = {}): KuaishouIndustryVoucherRow {
|
||||
function createVoucher(
|
||||
patch: Partial<KuaishouIndustryVoucherRow> = {},
|
||||
): KuaishouIndustryVoucherRow {
|
||||
return {
|
||||
id: 1,
|
||||
voucher_code: 'ETICKET-1',
|
||||
|
||||
@@ -39,7 +39,9 @@ export async function resolveOrderFulfillmentReadiness(
|
||||
}
|
||||
}
|
||||
|
||||
const blockedVoucher = vouchers.find((voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher))
|
||||
const blockedVoucher = vouchers.find(
|
||||
(voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher),
|
||||
)
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
|
||||
@@ -54,9 +54,9 @@ export async function planFulfillmentTaskForOrderItem({
|
||||
const profileId = Number(profile.profile_id || profile.id || 0)
|
||||
const profileKey = String(profile.profile_key || '').trim()
|
||||
const profileName = String(profile.profile_name || profile.name || '').trim()
|
||||
const executorKey = String(profile.executor_key || FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH)
|
||||
.trim()
|
||||
|| FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
||||
const executorKey =
|
||||
String(profile.executor_key || FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH).trim() ||
|
||||
FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
||||
const fulfillmentConfig = parseJsonObject(profile.config_json)
|
||||
const context = buildFulfillmentTaskContext({
|
||||
order,
|
||||
@@ -229,9 +229,7 @@ function buildFulfillmentTaskContext({
|
||||
? {
|
||||
flowType: 'affiliate_dash',
|
||||
sku: String(affiliateDashConfig.sku || '').trim(),
|
||||
productName: String(
|
||||
affiliateDashConfig.productName || item.sku_name || '',
|
||||
).trim(),
|
||||
productName: String(affiliateDashConfig.productName || item.sku_name || '').trim(),
|
||||
orderNo: '',
|
||||
clientOrderNo: '',
|
||||
orderStatus: '',
|
||||
@@ -279,9 +277,9 @@ async function resolveDynamicFulfillmentProfile(
|
||||
}
|
||||
|
||||
return (
|
||||
await resolveDynamicCloudtentaclesProfile(item, getProfileByKey) ||
|
||||
await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey) ||
|
||||
await resolveDynamicAffiliateDashProfile(item, getProfileByKey)
|
||||
(await resolveDynamicCloudtentaclesProfile(item, getProfileByKey)) ||
|
||||
(await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey)) ||
|
||||
(await resolveDynamicAffiliateDashProfile(item, getProfileByKey))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,23 +8,23 @@ import {
|
||||
} from './product-resolution-service.js'
|
||||
|
||||
test('matchAffiliateDashSku 原样精确命中', () => {
|
||||
const mapping = { '幸运币90个': 'lucky_coin_x90' }
|
||||
const mapping = { 幸运币90个: 'lucky_coin_x90' }
|
||||
assert.equal(matchAffiliateDashSku(mapping, '幸运币90个'), 'lucky_coin_x90')
|
||||
})
|
||||
|
||||
test('matchAffiliateDashSku 全角数字/空格归一命中', () => {
|
||||
const mapping = { '幸运币90个': 'lucky_coin_x90' }
|
||||
const mapping = { 幸运币90个: 'lucky_coin_x90' }
|
||||
assert.equal(matchAffiliateDashSku(mapping, '幸运币90个'), 'lucky_coin_x90')
|
||||
assert.equal(matchAffiliateDashSku(mapping, ' 幸运币 90 个 '), 'lucky_coin_x90')
|
||||
})
|
||||
|
||||
test('matchAffiliateDashSku 大小写归一命中', () => {
|
||||
const mapping = { '星际漫游服装礼包': 'pack_star_roam_outfit' }
|
||||
const mapping = { 星际漫游服装礼包: 'pack_star_roam_outfit' }
|
||||
assert.equal(matchAffiliateDashSku(mapping, '星际漫游服装礼包'), 'pack_star_roam_outfit')
|
||||
})
|
||||
|
||||
test('matchAffiliateDashSku 未命中返回空串', () => {
|
||||
const mapping = { '幸运币90个': 'lucky_coin_x90' }
|
||||
const mapping = { 幸运币90个: 'lucky_coin_x90' }
|
||||
assert.equal(matchAffiliateDashSku(mapping, '神秘新商品'), '')
|
||||
assert.equal(matchAffiliateDashSku(mapping, ''), '')
|
||||
})
|
||||
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
resolveKuaishouFeifeiProductByName,
|
||||
type KuaishouFeifeiProductMatch,
|
||||
} from '../platforms/kuaishou-feifei/product-rule-service.js'
|
||||
import {
|
||||
resolveFulfillmentRoute,
|
||||
type FulfillmentRouteResult,
|
||||
} from './routing-config-service.js'
|
||||
import { resolveFulfillmentRoute, type FulfillmentRouteResult } from './routing-config-service.js'
|
||||
import { getAffiliateDashConfig } from '../platforms/affiliate-dash/config.js'
|
||||
import { listAllAffiliateDashProducts } from '../platforms/affiliate-dash/product-service.js'
|
||||
import type { AffiliateDashSkuMapping } from '../../types/runtime-config.js'
|
||||
@@ -97,9 +94,7 @@ export async function resolveOrderItemForFulfillment({
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI
|
||||
? kuaishouFeifeiMatch?.productCode
|
||||
: '',
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH
|
||||
? affiliateDashMatch?.sku
|
||||
: '',
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH ? affiliateDashMatch?.sku : '',
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
@@ -110,9 +105,7 @@ export async function resolveOrderItemForFulfillment({
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI
|
||||
? kuaishouFeifeiMatch?.skuName
|
||||
: '',
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH
|
||||
? affiliateDashMatch?.sku
|
||||
: '',
|
||||
selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH ? affiliateDashMatch?.sku : '',
|
||||
item.skuName,
|
||||
externalSkuName,
|
||||
resolvedSkuCode,
|
||||
@@ -187,11 +180,13 @@ export async function hasConfiguredOrderItems({
|
||||
items = [],
|
||||
}: HasConfiguredOrderItemsInput): Promise<boolean> {
|
||||
const candidates = await Promise.all(
|
||||
(Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
item,
|
||||
})),
|
||||
(Array.isArray(items) ? items : []).map((item) =>
|
||||
resolveConfiguredItemCandidate({
|
||||
provider,
|
||||
platform,
|
||||
item,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return candidates.some((item) => item.isConfigured)
|
||||
@@ -217,20 +212,9 @@ async function resolveConfiguredItemCandidate({
|
||||
platform = '',
|
||||
item = {},
|
||||
}: ResolveOrderItemForFulfillmentInput): Promise<FulfillmentItemCandidate> {
|
||||
const externalItemId = pickFirstNonEmpty([
|
||||
item.externalItemId,
|
||||
item.itemId,
|
||||
])
|
||||
const externalSkuCode = pickFirstNonEmpty([
|
||||
item.externalSkuCode,
|
||||
item.skuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const externalSkuName = pickFirstNonEmpty([
|
||||
item.externalSkuName,
|
||||
item.skuName,
|
||||
externalSkuCode,
|
||||
])
|
||||
const externalItemId = pickFirstNonEmpty([item.externalItemId, item.itemId])
|
||||
const externalSkuCode = pickFirstNonEmpty([item.externalSkuCode, item.skuCode, externalItemId])
|
||||
const externalSkuName = pickFirstNonEmpty([item.externalSkuName, item.skuName, externalSkuCode])
|
||||
const externalSkuNameNormalized = normalizeProductName(externalSkuName)
|
||||
|
||||
if (isOpen91KuaishouOrder(provider, platform)) {
|
||||
@@ -293,10 +277,7 @@ async function resolveConfiguredItemCandidate({
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedSkuCode = pickFirstNonEmpty([
|
||||
externalSkuCode,
|
||||
externalItemId,
|
||||
])
|
||||
const resolvedSkuCode = pickFirstNonEmpty([externalSkuCode, externalItemId])
|
||||
|
||||
return {
|
||||
externalItemId,
|
||||
@@ -370,22 +351,32 @@ let affiliateDashSkuCache: { skus: Set<string>; fetchedAt: number } | null = nul
|
||||
|
||||
async function getAffiliateDashSkuSet(): Promise<Set<string>> {
|
||||
const now = Date.now()
|
||||
if (affiliateDashSkuCache && now - affiliateDashSkuCache.fetchedAt < AFFILIATE_DASH_SKU_CACHE_TTL_MS) {
|
||||
if (
|
||||
affiliateDashSkuCache &&
|
||||
now - affiliateDashSkuCache.fetchedAt < AFFILIATE_DASH_SKU_CACHE_TTL_MS
|
||||
) {
|
||||
return affiliateDashSkuCache.skus
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await listAllAffiliateDashProducts()
|
||||
const skus = new Set<string>(result.list.map((product) => String(product.sku || '').trim()).filter(Boolean))
|
||||
const skus = new Set<string>(
|
||||
result.list.map((product) => String(product.sku || '').trim()).filter(Boolean),
|
||||
)
|
||||
affiliateDashSkuCache = { skus, fetchedAt: now }
|
||||
return skus
|
||||
} catch (error) {
|
||||
// 拉取失败:返回上次缓存(即使过期)或空集合,匹配 miss 走其他通道,不阻塞下单;
|
||||
// warn 日志便于线上排查(密钥未配/平台不可用都会导致透传降级为 miss)。
|
||||
logIntegration('[affiliate-dash]', 'affiliate-dash 商品列表拉取失败,透传降级为未命中', {
|
||||
cachedSkuCount: affiliateDashSkuCache?.skus.size || 0,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}, { level: 'warn' })
|
||||
logIntegration(
|
||||
'[affiliate-dash]',
|
||||
'affiliate-dash 商品列表拉取失败,透传降级为未命中',
|
||||
{
|
||||
cachedSkuCount: affiliateDashSkuCache?.skus.size || 0,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
return affiliateDashSkuCache?.skus || new Set<string>()
|
||||
}
|
||||
}
|
||||
@@ -394,10 +385,7 @@ async function getAffiliateDashSkuSet(): Promise<Set<string>> {
|
||||
* 91 商品编码为中文名(如「幸运币90个」),映射键查询做归一化:
|
||||
* NFKC(全角→半角)→ 去所有空白 → 小写。先原样精确,再归一化遍历。
|
||||
*/
|
||||
export function matchAffiliateDashSku(
|
||||
mapping: AffiliateDashSkuMapping,
|
||||
productNo: string,
|
||||
): string {
|
||||
export function matchAffiliateDashSku(mapping: AffiliateDashSkuMapping, productNo: string): string {
|
||||
const raw = String(productNo || '').trim()
|
||||
if (!raw) {
|
||||
return ''
|
||||
@@ -425,8 +413,9 @@ export function normalizeAffiliateDashMappingKey(value: unknown): string {
|
||||
}
|
||||
|
||||
function isOpen91KuaishouOrder(provider: unknown, platform: unknown) {
|
||||
return String(provider || '').trim() === '91kaquan' &&
|
||||
String(platform || '').trim() === 'kuaishou'
|
||||
return (
|
||||
String(provider || '').trim() === '91kaquan' && String(platform || '').trim() === 'kuaishou'
|
||||
)
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
@@ -2,10 +2,7 @@ import path from 'node:path'
|
||||
|
||||
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||
import {
|
||||
readAppConfigEntry,
|
||||
saveAppConfigEntry,
|
||||
} from '../config/app-config-store.js'
|
||||
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
|
||||
import { normalizeCloudtentaclesMatchName } from '../order/cloudtentacles-match-utils.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js'
|
||||
@@ -113,11 +110,13 @@ export function normalizeFulfillmentRoutingConfig(rawValue: unknown): Fulfillmen
|
||||
defaultExecutorPriority:
|
||||
defaultExecutorPriority.length > 0 ? defaultExecutorPriority : [...DEFAULT_EXECUTOR_PRIORITY],
|
||||
unmatchedExecutorKey:
|
||||
unmatchedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH ? unmatchedExecutorKey : '',
|
||||
unmatchedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
||||
? unmatchedExecutorKey
|
||||
: '',
|
||||
executors: ROUTABLE_EXECUTOR_KEYS.reduce<Record<string, FulfillmentRoutingExecutorConfig>>(
|
||||
(result, executorKey) => {
|
||||
const executorConfig = isPlainObject(sourceExecutors[executorKey])
|
||||
? sourceExecutors[executorKey] as JsonObject
|
||||
? (sourceExecutors[executorKey] as JsonObject)
|
||||
: {}
|
||||
result[executorKey] = {
|
||||
enabled: executorConfig.enabled !== false,
|
||||
@@ -383,17 +382,19 @@ function findMatchedRoutingRule(productName: unknown, rules: FulfillmentRoutingR
|
||||
return null
|
||||
}
|
||||
|
||||
return rules.find((rule) => {
|
||||
if (rule.enabled === false || !rule.normalizedProductName) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
rules.find((rule) => {
|
||||
if (rule.enabled === false || !rule.normalizedProductName) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (rule.matchType === 'contains') {
|
||||
return normalizedProductName.includes(rule.normalizedProductName)
|
||||
}
|
||||
if (rule.matchType === 'contains') {
|
||||
return normalizedProductName.includes(rule.normalizedProductName)
|
||||
}
|
||||
|
||||
return normalizedProductName === rule.normalizedProductName
|
||||
}) || null
|
||||
return normalizedProductName === rule.normalizedProductName
|
||||
}) || null
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeFulfillmentRoutingRule(rawValue: unknown): FulfillmentRoutingRule | null {
|
||||
@@ -409,7 +410,8 @@ function normalizeFulfillmentRoutingRule(rawValue: unknown): FulfillmentRoutingR
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(source.id || `${normalizedProductName}:${executorKey}`).trim() ||
|
||||
id:
|
||||
String(source.id || `${normalizedProductName}:${executorKey}`).trim() ||
|
||||
`${normalizedProductName}:${executorKey}`,
|
||||
enabled: source.enabled !== false,
|
||||
productName,
|
||||
@@ -446,7 +448,11 @@ function normalizeExecutorPriority(value: unknown) {
|
||||
|
||||
for (const item of rawItems) {
|
||||
const executorKey = normalizeExecutorKey(item)
|
||||
if (!executorKey || seen.has(executorKey) || executorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH) {
|
||||
if (
|
||||
!executorKey ||
|
||||
seen.has(executorKey) ||
|
||||
executorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
|
||||
) {
|
||||
continue
|
||||
}
|
||||
seen.add(executorKey)
|
||||
|
||||
@@ -17,7 +17,9 @@ type BarkSendInput = {
|
||||
}
|
||||
|
||||
export async function sendBarkNotification(input: BarkSendInput) {
|
||||
const serverUrl = String(input.serverUrl || 'https://api.day.app').trim().replace(/\/+$/, '')
|
||||
const serverUrl = String(input.serverUrl || 'https://api.day.app')
|
||||
.trim()
|
||||
.replace(/\/+$/, '')
|
||||
const deviceKey = String(input.recipient?.deviceKey || '').trim()
|
||||
const title = String(input.title || '').trim()
|
||||
const body = String(input.body || '').trim()
|
||||
@@ -88,11 +90,7 @@ export async function sendBarkNotification(input: BarkSendInput) {
|
||||
}
|
||||
|
||||
function buildBarkEndpoint(serverUrl: string, deviceKey: string, title: string, body: string) {
|
||||
const segments = [
|
||||
serverUrl,
|
||||
encodeURIComponent(deviceKey),
|
||||
encodeURIComponent(title),
|
||||
]
|
||||
const segments = [serverUrl, encodeURIComponent(deviceKey), encodeURIComponent(title)]
|
||||
|
||||
if (body) {
|
||||
segments.push(encodeURIComponent(body))
|
||||
@@ -124,7 +122,10 @@ function isBarkFailure(parsed: unknown) {
|
||||
|
||||
function resolveBarkErrorMessage(parsed: unknown, responseText: string, status: number) {
|
||||
if (isPlainObject(parsed)) {
|
||||
return String(parsed.message || parsed.msg || parsed.error || '').trim() || `Bark 通知发送失败,HTTP ${status}`
|
||||
return (
|
||||
String(parsed.message || parsed.msg || parsed.error || '').trim() ||
|
||||
`Bark 通知发送失败,HTTP ${status}`
|
||||
)
|
||||
}
|
||||
|
||||
return String(responseText || '').trim() || `Bark 通知发送失败,HTTP ${status}`
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
createDefaultNotificationConfig,
|
||||
normalizeNotificationConfig,
|
||||
} from './config-service.js'
|
||||
import { createDefaultNotificationConfig, normalizeNotificationConfig } from './config-service.js'
|
||||
|
||||
test('normalizeNotificationConfig normalizes bark and wpush recipients', () => {
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { JsonObject } from '../../types/json.js'
|
||||
|
||||
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||
import {
|
||||
readAppConfigEntry,
|
||||
saveAppConfigEntry,
|
||||
} from '../config/app-config-store.js'
|
||||
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
|
||||
|
||||
const NOTIFICATION_CONFIG_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'notification-config.json')
|
||||
const DEFAULT_BARK_SERVER_URL = 'https://api.day.app'
|
||||
@@ -46,8 +43,9 @@ export function listEnabledBarkRecipients(config: JsonObject = getNotificationCo
|
||||
return []
|
||||
}
|
||||
|
||||
return (Array.isArray(config.channels?.bark?.recipients) ? config.channels.bark.recipients : [])
|
||||
.filter((item: JsonObject) => item.enabled !== false && String(item.deviceKey || '').trim())
|
||||
return (
|
||||
Array.isArray(config.channels?.bark?.recipients) ? config.channels.bark.recipients : []
|
||||
).filter((item: JsonObject) => item.enabled !== false && String(item.deviceKey || '').trim())
|
||||
}
|
||||
|
||||
export function listEnabledWpushRecipients(config: JsonObject = getNotificationConfig()) {
|
||||
@@ -55,8 +53,11 @@ export function listEnabledWpushRecipients(config: JsonObject = getNotificationC
|
||||
return []
|
||||
}
|
||||
|
||||
return (Array.isArray(config.channels?.wpush?.recipients) ? config.channels.wpush.recipients : [])
|
||||
.filter((item: JsonObject) => item.enabled !== false && String(item.apiKey || item.apikey || '').trim())
|
||||
return (
|
||||
Array.isArray(config.channels?.wpush?.recipients) ? config.channels.wpush.recipients : []
|
||||
).filter(
|
||||
(item: JsonObject) => item.enabled !== false && String(item.apiKey || item.apikey || '').trim(),
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeNotificationConfig(rawValue: unknown) {
|
||||
|
||||
@@ -107,11 +107,9 @@ export function notifyKuaishouCloudAssetNotEnough({
|
||||
`商品:${String(skuName || binding.skuName || flowRecord.internalSkuName || '').trim() || '-'}`,
|
||||
].join('\n'),
|
||||
category: 'kuaishou_cloud_asset_not_enough',
|
||||
cooldownKey: [
|
||||
'kuaishou_cloud_asset_not_enough',
|
||||
taskRecord.id || '',
|
||||
binding.skuId || '',
|
||||
].join(':'),
|
||||
cooldownKey: ['kuaishou_cloud_asset_not_enough', taskRecord.id || '', binding.skuId || ''].join(
|
||||
':',
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,9 +133,16 @@ export function notifyCloudtentaclesAuthExpired({
|
||||
`接口:${String(pathname || '').trim() || '-'}`,
|
||||
`错误码:${String(errorCode || '').trim() || '-'}`,
|
||||
`原因:${String(message || '').trim() || '登录态已失效,请到后台重新登录'}`,
|
||||
].filter(Boolean).join('\n'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
category: 'cloudtentacles_auth_expired',
|
||||
cooldownKey: ['cloudtentacles_auth_expired', normalizedSourceKey || 'default', pathname, errorCode].join(':'),
|
||||
cooldownKey: [
|
||||
'cloudtentacles_auth_expired',
|
||||
normalizedSourceKey || 'default',
|
||||
pathname,
|
||||
errorCode,
|
||||
].join(':'),
|
||||
cooldownMs: Number(cooldownSeconds || 600) * 1000,
|
||||
})
|
||||
}
|
||||
@@ -161,9 +166,13 @@ export function notifyCloudtentaclesAssetLow({
|
||||
`当前余额:${Number(asset || 0)}`,
|
||||
`提醒阈值:${Number(threshold || 0)}`,
|
||||
'请及时补充 cloudtentacles 余额,避免自动履约失败。',
|
||||
].filter(Boolean).join('\n'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
category: 'cloudtentacles_asset_low',
|
||||
cooldownKey: ['cloudtentacles_asset_low', normalizedSourceKey || 'default', threshold].join(':'),
|
||||
cooldownKey: ['cloudtentacles_asset_low', normalizedSourceKey || 'default', threshold].join(
|
||||
':',
|
||||
),
|
||||
cooldownMs: Number(cooldownSeconds || 1800) * 1000,
|
||||
})
|
||||
}
|
||||
@@ -186,7 +195,11 @@ export function notifyOpen91PendingConfig({
|
||||
`原因:${String(reason || '').trim() || '未命中履约配置'}`,
|
||||
].join('\n'),
|
||||
category: 'open91_pending_config',
|
||||
cooldownKey: ['open91_pending_config', orderNo || orderRecord.platform_order_id || '', productNo].join(':'),
|
||||
cooldownKey: [
|
||||
'open91_pending_config',
|
||||
orderNo || orderRecord.platform_order_id || '',
|
||||
productNo,
|
||||
].join(':'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -258,9 +271,7 @@ function formatTaskLine(task: unknown) {
|
||||
}
|
||||
|
||||
function toRecord(value: unknown): JsonObject {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
|
||||
}
|
||||
|
||||
function isNotificationCoolingDown(cooldownKey: unknown, cooldownMs = DEFAULT_COOLDOWN_MS) {
|
||||
|
||||
@@ -37,59 +37,79 @@ export async function sendInternalNotification(input: NotificationInput = {}) {
|
||||
}
|
||||
|
||||
const results = [
|
||||
...(await Promise.all(barkRecipients.map(async (recipient: JsonObject) => {
|
||||
try {
|
||||
const result = await sendBarkNotification({
|
||||
recipient,
|
||||
title,
|
||||
body,
|
||||
group: `订单系统/${category}`,
|
||||
...(bark.serverUrl !== undefined ? { serverUrl: bark.serverUrl } : {}),
|
||||
...(input.url !== undefined ? { url: input.url } : {}),
|
||||
})
|
||||
return mapNotificationResult('bark', recipient, recipient.deviceKey, true, '', result.status, result.response)
|
||||
} catch (error) {
|
||||
logWarn('[notification/bark]', 'Bark 内部通知发送失败', {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
error,
|
||||
})
|
||||
return mapNotificationResult(
|
||||
'bark',
|
||||
recipient,
|
||||
recipient.deviceKey,
|
||||
false,
|
||||
error instanceof Error ? error.message : 'Bark 内部通知发送失败',
|
||||
isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0,
|
||||
isPlainObject(error) ? error.context || null : null,
|
||||
)
|
||||
}
|
||||
}))),
|
||||
...(await Promise.all(wpushRecipients.map(async (recipient: JsonObject) => {
|
||||
try {
|
||||
const result = await sendWpushNotification({
|
||||
recipient,
|
||||
title,
|
||||
body,
|
||||
})
|
||||
return mapNotificationResult('wpush', recipient, recipient.apiKey, true, '', result.status, result.response)
|
||||
} catch (error) {
|
||||
logWarn('[notification/wpush]', 'WPush 内部通知发送失败', {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
error,
|
||||
})
|
||||
return mapNotificationResult(
|
||||
'wpush',
|
||||
recipient,
|
||||
recipient.apiKey,
|
||||
false,
|
||||
error instanceof Error ? error.message : 'WPush 内部通知发送失败',
|
||||
isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0,
|
||||
isPlainObject(error) ? error.context || null : null,
|
||||
)
|
||||
}
|
||||
}))),
|
||||
...(await Promise.all(
|
||||
barkRecipients.map(async (recipient: JsonObject) => {
|
||||
try {
|
||||
const result = await sendBarkNotification({
|
||||
recipient,
|
||||
title,
|
||||
body,
|
||||
group: `订单系统/${category}`,
|
||||
...(bark.serverUrl !== undefined ? { serverUrl: bark.serverUrl } : {}),
|
||||
...(input.url !== undefined ? { url: input.url } : {}),
|
||||
})
|
||||
return mapNotificationResult(
|
||||
'bark',
|
||||
recipient,
|
||||
recipient.deviceKey,
|
||||
true,
|
||||
'',
|
||||
result.status,
|
||||
result.response,
|
||||
)
|
||||
} catch (error) {
|
||||
logWarn('[notification/bark]', 'Bark 内部通知发送失败', {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
error,
|
||||
})
|
||||
return mapNotificationResult(
|
||||
'bark',
|
||||
recipient,
|
||||
recipient.deviceKey,
|
||||
false,
|
||||
error instanceof Error ? error.message : 'Bark 内部通知发送失败',
|
||||
isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0,
|
||||
isPlainObject(error) ? error.context || null : null,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)),
|
||||
...(await Promise.all(
|
||||
wpushRecipients.map(async (recipient: JsonObject) => {
|
||||
try {
|
||||
const result = await sendWpushNotification({
|
||||
recipient,
|
||||
title,
|
||||
body,
|
||||
})
|
||||
return mapNotificationResult(
|
||||
'wpush',
|
||||
recipient,
|
||||
recipient.apiKey,
|
||||
true,
|
||||
'',
|
||||
result.status,
|
||||
result.response,
|
||||
)
|
||||
} catch (error) {
|
||||
logWarn('[notification/wpush]', 'WPush 内部通知发送失败', {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
error,
|
||||
})
|
||||
return mapNotificationResult(
|
||||
'wpush',
|
||||
recipient,
|
||||
recipient.apiKey,
|
||||
false,
|
||||
error instanceof Error ? error.message : 'WPush 内部通知发送失败',
|
||||
isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0,
|
||||
isPlainObject(error) ? error.context || null : null,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)),
|
||||
]
|
||||
|
||||
return {
|
||||
|
||||
@@ -66,11 +66,12 @@ test('sendWpushNotification sends POST json payload', async () => {
|
||||
|
||||
test('sendWpushNotification rejects missing api key', async () => {
|
||||
await assert.rejects(
|
||||
() => sendWpushNotification({
|
||||
recipient: {},
|
||||
title: '测试标题',
|
||||
body: '测试内容',
|
||||
}),
|
||||
() =>
|
||||
sendWpushNotification({
|
||||
recipient: {},
|
||||
title: '测试标题',
|
||||
body: '测试内容',
|
||||
}),
|
||||
(error: any) => {
|
||||
assert.equal(error?.errorCode, 'wpush_api_key_required')
|
||||
return true
|
||||
|
||||
@@ -97,7 +97,12 @@ export async function sendWpushNotification(input: WpushSendInput) {
|
||||
|
||||
async function requestViaNodeHttp(
|
||||
url: URL,
|
||||
{ method, headers, body, signal }: {
|
||||
{
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
signal,
|
||||
}: {
|
||||
method: string
|
||||
headers: Record<string, string>
|
||||
body?: string
|
||||
@@ -107,25 +112,29 @@ async function requestViaNodeHttp(
|
||||
const transport = url.protocol === 'https:' ? https : http
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = transport.request(url, {
|
||||
method,
|
||||
headers,
|
||||
rejectUnauthorized: false,
|
||||
}, (response) => {
|
||||
const chunks: Buffer[] = []
|
||||
const request = transport.request(
|
||||
url,
|
||||
{
|
||||
method,
|
||||
headers,
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
(response) => {
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
})
|
||||
|
||||
response.on('end', () => {
|
||||
resolve({
|
||||
ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300,
|
||||
status: Number(response.statusCode || 0),
|
||||
bodyText: Buffer.concat(chunks).toString('utf8'),
|
||||
response.on('data', (chunk) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
response.on('end', () => {
|
||||
resolve({
|
||||
ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300,
|
||||
status: Number(response.statusCode || 0),
|
||||
bodyText: Buffer.concat(chunks).toString('utf8'),
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
request.on('error', reject)
|
||||
|
||||
@@ -135,11 +144,15 @@ async function requestViaNodeHttp(
|
||||
error.name = 'AbortError'
|
||||
request.destroy(error)
|
||||
} else {
|
||||
signal.addEventListener('abort', () => {
|
||||
const error = new Error('Request aborted')
|
||||
error.name = 'AbortError'
|
||||
request.destroy(error)
|
||||
}, { once: true })
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
const error = new Error('Request aborted')
|
||||
error.name = 'AbortError'
|
||||
request.destroy(error)
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +192,10 @@ function isWpushFailure(parsed: unknown) {
|
||||
|
||||
function resolveWpushErrorMessage(parsed: unknown, responseText: string, status: number) {
|
||||
if (isPlainObject(parsed)) {
|
||||
return String(parsed.message || parsed.msg || parsed.error || '').trim() || `WPush 通知发送失败,HTTP ${status}`
|
||||
return (
|
||||
String(parsed.message || parsed.msg || parsed.error || '').trim() ||
|
||||
`WPush 通知发送失败,HTTP ${status}`
|
||||
)
|
||||
}
|
||||
|
||||
return String(responseText || '').trim() || `WPush 通知发送失败,HTTP ${status}`
|
||||
|
||||
@@ -23,7 +23,8 @@ export function getOpen91Config(overrides: Partial<Open91RuntimeConfig> = {}) {
|
||||
shopId: String(config.shopId || OPEN_91_PROVIDER).trim() || OPEN_91_PROVIDER,
|
||||
shopName: String(config.shopName || '91卡券').trim() || '91卡券',
|
||||
timestampToleranceSeconds: Number(config.timestampToleranceSeconds || 600) || 600,
|
||||
cardsEncoding: String(config.cardsEncoding || 'aes-256-ecb-base64').trim() || 'aes-256-ecb-base64',
|
||||
cardsEncoding:
|
||||
String(config.cardsEncoding || 'aes-256-ecb-base64').trim() || 'aes-256-ecb-base64',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ import test from 'node:test'
|
||||
import { resolveIndustryVoucherForTask } from './consume-on-deliver.js'
|
||||
import type { KuaishouIndustryVoucherRow } from '../../types/repository/rows.js'
|
||||
|
||||
function voucher(partial: Partial<KuaishouIndustryVoucherRow> & {
|
||||
voucher_code: string
|
||||
}): KuaishouIndustryVoucherRow {
|
||||
function voucher(
|
||||
partial: Partial<KuaishouIndustryVoucherRow> & {
|
||||
voucher_code: string
|
||||
},
|
||||
): KuaishouIndustryVoucherRow {
|
||||
return {
|
||||
id: partial.id || 1,
|
||||
oid: partial.oid || 'OID-1',
|
||||
@@ -43,18 +45,14 @@ test('resolveIndustryVoucherForTask prefers task_id over unit_index', () => {
|
||||
})
|
||||
|
||||
test('resolveIndustryVoucherForTask falls back to unit_index when task_id missing', () => {
|
||||
const vouchers = [
|
||||
voucher({ id: 1, voucher_code: 'A', task_id: null, unit_index: 2 }),
|
||||
]
|
||||
const vouchers = [voucher({ id: 1, voucher_code: 'A', task_id: null, unit_index: 2 })]
|
||||
|
||||
const matched = resolveIndustryVoucherForTask({ id: 99, unit_index: 2 }, vouchers)
|
||||
assert.equal(matched?.voucher_code, 'A')
|
||||
})
|
||||
|
||||
test('resolveIndustryVoucherForTask returns null when no match', () => {
|
||||
const vouchers = [
|
||||
voucher({ id: 1, voucher_code: 'A', task_id: 1, unit_index: 1 }),
|
||||
]
|
||||
const vouchers = [voucher({ id: 1, voucher_code: 'A', task_id: 1, unit_index: 1 })]
|
||||
|
||||
assert.equal(resolveIndustryVoucherForTask({ id: 2, unit_index: 3 }, vouchers), null)
|
||||
})
|
||||
|
||||
@@ -8,22 +8,14 @@
|
||||
* 交付后会 best-effort 自动准备 Cloud 绑定资源(取号/绑链),避免
|
||||
* 「已核销却停在待准备资源」只能靠后台手动点「准备资源」。
|
||||
*/
|
||||
import {
|
||||
attachKuaishouIndustryVoucherToTask,
|
||||
} from '../platforms/kuaishou-industry/voucher-binding-service.js'
|
||||
import {
|
||||
consumeKuaishouIndustryVoucher,
|
||||
} from '../platforms/kuaishou-industry/voucher-service.js'
|
||||
import { attachKuaishouIndustryVoucherToTask } from '../platforms/kuaishou-industry/voucher-binding-service.js'
|
||||
import { consumeKuaishouIndustryVoucher } from '../platforms/kuaishou-industry/voucher-service.js'
|
||||
import { prepareFulfillmentBinding } from '../fulfillment/executors/registry.js'
|
||||
import { normalizeKuaishouCloudFlow } from '../fulfillment/kuaishou-cloud/index.js'
|
||||
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||
import { logIntegration, logWarn } from '../../utils/logger.js'
|
||||
import { parseTaskContext } from '../../utils/task-json.js'
|
||||
import type {
|
||||
KuaishouIndustryVoucherRow,
|
||||
OrderRow,
|
||||
TaskRow,
|
||||
} from '../../types/repository/rows.js'
|
||||
import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
export const OPEN_91_DELIVER_CONSUME_SOURCE = 'open91_query_deliver'
|
||||
|
||||
@@ -69,7 +61,11 @@ export function resolveIndustryVoucherForTask(
|
||||
}
|
||||
|
||||
function isVoucherConsumed(voucher: Pick<KuaishouIndustryVoucherRow, 'status'>): boolean {
|
||||
return String(voucher.status || '').trim().toUpperCase() === 'CONSUMED'
|
||||
return (
|
||||
String(voucher.status || '')
|
||||
.trim()
|
||||
.toUpperCase() === 'CONSUMED'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,8 +79,9 @@ export async function consumeIndustryVouchersBeforeOpen91Deliver(options: {
|
||||
requestId?: string
|
||||
source?: string
|
||||
}): Promise<Open91DeliverConsumeResult> {
|
||||
const source = String(options.source || OPEN_91_DELIVER_CONSUME_SOURCE).trim()
|
||||
|| OPEN_91_DELIVER_CONSUME_SOURCE
|
||||
const source =
|
||||
String(options.source || OPEN_91_DELIVER_CONSUME_SOURCE).trim() ||
|
||||
OPEN_91_DELIVER_CONSUME_SOURCE
|
||||
const readySet = new Set(
|
||||
(Array.isArray(options.readyTaskIds) ? options.readyTaskIds : [])
|
||||
.map((id) => Number(id))
|
||||
@@ -213,9 +210,7 @@ async function ensureBindingPreparedAfterDeliver(options: {
|
||||
return
|
||||
}
|
||||
|
||||
const flow = normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(options.task).kuaishouCloudFulfillment,
|
||||
)
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(options.task).kuaishouCloudFulfillment)
|
||||
if (flow.binding.prepareStatus === 'ready' && String(flow.binding.bindUrl || '').trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-i
|
||||
import { listTasksByOrderId } from '../../repositories/task-repo.js'
|
||||
import { resolveTaskDeliveryLink } from '../fulfillment/delivery-link-service.js'
|
||||
import { logIntegration } from '../../utils/logger.js'
|
||||
import { asJsonObject, type JsonObject } from '../../types/json.js'
|
||||
import { asJsonObject, type JsonObject } from '../../types/json.js'
|
||||
import {
|
||||
OPEN_91_MANUAL_FAILED_STATUS,
|
||||
OPEN_91_PENDING_CONFIG_STATUS,
|
||||
@@ -114,10 +114,12 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
|
||||
}
|
||||
|
||||
readyTaskIds.push(Number(task.id))
|
||||
cardItems.push(buildOpen91CardItem({
|
||||
claimUrl,
|
||||
expireTime: deliveryLink?.expireTime || '',
|
||||
}))
|
||||
cardItems.push(
|
||||
buildOpen91CardItem({
|
||||
claimUrl,
|
||||
expireTime: deliveryLink?.expireTime || '',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const queryState = resolveOpen91QueryState({
|
||||
|
||||
@@ -64,7 +64,10 @@ export function assertOpen91QueryPayload(payload: JsonObject, config = assertOpe
|
||||
assertOpen91CommonPayload(payload, config)
|
||||
}
|
||||
|
||||
export function assertOpen91Timestamp(timestamp: unknown, toleranceSeconds = assertOpen91Config().timestampToleranceSeconds) {
|
||||
export function assertOpen91Timestamp(
|
||||
timestamp: unknown,
|
||||
toleranceSeconds = assertOpen91Config().timestampToleranceSeconds,
|
||||
) {
|
||||
const nowSeconds = Math.floor(Date.now() / 1000)
|
||||
if (Math.abs(nowSeconds - Number(timestamp)) > Math.max(1, Number(toleranceSeconds) || 600)) {
|
||||
throw createHttpError('timestamp 已过期', {
|
||||
|
||||
@@ -7,11 +7,7 @@ import {
|
||||
signOpen91Payload,
|
||||
verifyOpen91Signature,
|
||||
} from './crypto.js'
|
||||
import {
|
||||
buildOpen91CardItem,
|
||||
formatOpen91ExpireTime,
|
||||
resolveOpen91QueryState,
|
||||
} from './response.js'
|
||||
import { buildOpen91CardItem, formatOpen91ExpireTime, resolveOpen91QueryState } from './response.js'
|
||||
|
||||
test('signOpen91Payload signs only request body fields', () => {
|
||||
const config = {
|
||||
@@ -77,7 +73,10 @@ test('encryptOpen91Cards matches documented AES ECB base64 sample', () => {
|
||||
test('resolveOpen91QueryState returns success only when every task is ready', () => {
|
||||
assert.deepEqual(
|
||||
resolveOpen91QueryState({
|
||||
tasks: [{ id: 1, task_status: 'pending_binding_prepare' }, { id: 2, task_status: 'waiting_binding' }],
|
||||
tasks: [
|
||||
{ id: 1, task_status: 'pending_binding_prepare' },
|
||||
{ id: 2, task_status: 'waiting_binding' },
|
||||
],
|
||||
readyTaskIds: [1, 2],
|
||||
}),
|
||||
{
|
||||
@@ -89,7 +88,10 @@ test('resolveOpen91QueryState returns success only when every task is ready', ()
|
||||
|
||||
assert.deepEqual(
|
||||
resolveOpen91QueryState({
|
||||
tasks: [{ id: 1, task_status: 'pending_binding_prepare' }, { id: 2, task_status: 'waiting_binding' }],
|
||||
tasks: [
|
||||
{ id: 1, task_status: 'pending_binding_prepare' },
|
||||
{ id: 2, task_status: 'waiting_binding' },
|
||||
],
|
||||
readyTaskIds: [1],
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import {
|
||||
OPEN_91_DEFAULT_FAIL_CODE,
|
||||
OPEN_91_SUCCESS_MESSAGE,
|
||||
} from './config.js'
|
||||
import { OPEN_91_DEFAULT_FAIL_CODE, OPEN_91_SUCCESS_MESSAGE } from './config.js'
|
||||
import { isOpen91FailedTaskStatus } from '../../domain/task-status.js'
|
||||
import { normalizeOpen91String } from './payload.js'
|
||||
|
||||
@@ -32,7 +29,10 @@ export function buildOpen91OrderCost(value = 0) {
|
||||
return Number.isFinite(amount) ? amount.toFixed(4) : '0.0000'
|
||||
}
|
||||
|
||||
export function buildOpen91CardItem({ claimUrl = '', expireTime = '' }: { claimUrl?: unknown, expireTime?: unknown } = {}) {
|
||||
export function buildOpen91CardItem({
|
||||
claimUrl = '',
|
||||
expireTime = '',
|
||||
}: { claimUrl?: unknown; expireTime?: unknown } = {}) {
|
||||
const normalizedClaimUrl = normalizeOpen91String(claimUrl)
|
||||
return {
|
||||
cardNo: normalizedClaimUrl,
|
||||
@@ -72,11 +72,14 @@ export function formatOpen91ExpireTime(value: unknown = '') {
|
||||
return `${byType.year}-${byType.month}-${byType.day} ${byType.hour}:${byType.minute}:${byType.second}`
|
||||
}
|
||||
|
||||
export function resolveOpen91QueryState(
|
||||
{ tasks = [], readyTaskIds = [] }: { tasks?: JsonObject[], readyTaskIds?: unknown[] } = {},
|
||||
) {
|
||||
export function resolveOpen91QueryState({
|
||||
tasks = [],
|
||||
readyTaskIds = [],
|
||||
}: { tasks?: JsonObject[]; readyTaskIds?: unknown[] } = {}) {
|
||||
const normalizedTasks = Array.isArray(tasks) ? tasks : []
|
||||
const readyTaskIdSet = new Set((Array.isArray(readyTaskIds) ? readyTaskIds : []).map((item) => Number(item)))
|
||||
const readyTaskIdSet = new Set(
|
||||
(Array.isArray(readyTaskIds) ? readyTaskIds : []).map((item) => Number(item)),
|
||||
)
|
||||
|
||||
if (normalizedTasks.length === 0) {
|
||||
return {
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import {
|
||||
listCloudtentaclesSources,
|
||||
} from '../platforms/cloudtentacles/source-config-service.js'
|
||||
import {
|
||||
getCloudtentaclesSessionStateByKey,
|
||||
} from '../platforms/cloudtentacles/session-state-service.js'
|
||||
import { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js'
|
||||
import { getCloudtentaclesSessionStateByKey } from '../platforms/cloudtentacles/session-state-service.js'
|
||||
import {
|
||||
normalizeCloudtentaclesDeviceId,
|
||||
normalizeCloudtentaclesDeviceType,
|
||||
} from '../platforms/cloudtentacles/defaults.js'
|
||||
import {
|
||||
listCloudtentaclesSku,
|
||||
} from '../platforms/cloudtentacles/catalog-service.js'
|
||||
import { listCloudtentaclesSku } from '../platforms/cloudtentacles/catalog-service.js'
|
||||
import {
|
||||
resolveCloudtentaclesOverrideRule,
|
||||
type CloudtentaclesOverrideRule,
|
||||
@@ -79,9 +73,11 @@ export async function resolveCloudtentaclesSkuByProductName(
|
||||
}
|
||||
|
||||
const listSources = deps.listCloudtentaclesSources || listCloudtentaclesSources
|
||||
const getSessionByKey = deps.getCloudtentaclesSessionStateByKey || getCloudtentaclesSessionStateByKey
|
||||
const getSessionByKey =
|
||||
deps.getCloudtentaclesSessionStateByKey || getCloudtentaclesSessionStateByKey
|
||||
const listSku = deps.listCloudtentaclesSku || listCloudtentaclesSku
|
||||
const resolveOverrideRule = deps.resolveCloudtentaclesOverrideRule || resolveCloudtentaclesOverrideRule
|
||||
const resolveOverrideRule =
|
||||
deps.resolveCloudtentaclesOverrideRule || resolveCloudtentaclesOverrideRule
|
||||
const sourcesConfig = listSources()
|
||||
|
||||
if (sourcesConfig.enabled === false) {
|
||||
@@ -89,8 +85,13 @@ export async function resolveCloudtentaclesSkuByProductName(
|
||||
}
|
||||
|
||||
const sourceContexts = (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : [])
|
||||
.map((source: CloudtentaclesSource) => buildCloudtentaclesSourceContext(source, getSessionByKey))
|
||||
.filter((context): context is NonNullable<ReturnType<typeof buildCloudtentaclesSourceContext>> => Boolean(context))
|
||||
.map((source: CloudtentaclesSource) =>
|
||||
buildCloudtentaclesSourceContext(source, getSessionByKey),
|
||||
)
|
||||
.filter(
|
||||
(context): context is NonNullable<ReturnType<typeof buildCloudtentaclesSourceContext>> =>
|
||||
Boolean(context),
|
||||
)
|
||||
|
||||
if (sourceContexts.length === 0) {
|
||||
return null
|
||||
@@ -98,7 +99,13 @@ export async function resolveCloudtentaclesSkuByProductName(
|
||||
|
||||
const overrideRule = resolveOverrideRule(productName)
|
||||
const overrideMatch = overrideRule
|
||||
? await resolveOverrideMatch(productName, normalizedProductName, overrideRule, sourceContexts, listSku)
|
||||
? await resolveOverrideMatch(
|
||||
productName,
|
||||
normalizedProductName,
|
||||
overrideRule,
|
||||
sourceContexts,
|
||||
listSku,
|
||||
)
|
||||
: null
|
||||
if (overrideMatch) {
|
||||
return overrideMatch
|
||||
@@ -215,7 +222,8 @@ async function resolveOverrideMatch(
|
||||
}
|
||||
|
||||
const firstDeliveryItem = deliveryItems[0]
|
||||
const firstSku = items.find((item) => Number(item.id || 0) === firstDeliveryItem.cloudSkuId) || {}
|
||||
const firstSku =
|
||||
items.find((item) => Number(item.id || 0) === firstDeliveryItem.cloudSkuId) || {}
|
||||
const cloudSourceKeys = rule.sourceKey
|
||||
? [context.sourceKey]
|
||||
: sourceContexts.map((sourceContext) => sourceContext.sourceKey)
|
||||
@@ -246,13 +254,17 @@ async function resolveOverrideMatch(
|
||||
return null
|
||||
}
|
||||
|
||||
function resolveOverrideDeliveryItems(rule: CloudtentaclesOverrideRule, skuItems: CloudtentaclesSku[]) {
|
||||
function resolveOverrideDeliveryItems(
|
||||
rule: CloudtentaclesOverrideRule,
|
||||
skuItems: CloudtentaclesSku[],
|
||||
) {
|
||||
const resolvedItems: CloudtentaclesResolvedDeliveryItem[] = []
|
||||
|
||||
for (const ruleItem of rule.deliveryItems) {
|
||||
const matchedSku = ruleItem.cloudSkuId > 0
|
||||
? skuItems.find((item) => Number(item.id || 0) === ruleItem.cloudSkuId)
|
||||
: findCloudtentaclesSkuByName(skuItems, ruleItem.cloudSkuName)
|
||||
const matchedSku =
|
||||
ruleItem.cloudSkuId > 0
|
||||
? skuItems.find((item) => Number(item.id || 0) === ruleItem.cloudSkuId)
|
||||
: findCloudtentaclesSkuByName(skuItems, ruleItem.cloudSkuName)
|
||||
const cloudSkuId = Number(matchedSku?.id || 0)
|
||||
const cloudSkuName = String(matchedSku?.name || '').trim()
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
normalizeCloudtentaclesOverrideRuleConfig,
|
||||
} from './cloudtentacles-override-rule-service.js'
|
||||
import { normalizeCloudtentaclesOverrideRuleConfig } from './cloudtentacles-override-rule-service.js'
|
||||
|
||||
test('normalizeCloudtentaclesOverrideRuleConfig merges duplicated delivery items', () => {
|
||||
const config = normalizeCloudtentaclesOverrideRuleConfig({
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { JsonObject } from '../../types/json.js'
|
||||
|
||||
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||
import {
|
||||
readAppConfigEntry,
|
||||
saveAppConfigEntry,
|
||||
} from '../config/app-config-store.js'
|
||||
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
|
||||
import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js'
|
||||
|
||||
const CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH = path.join(
|
||||
@@ -49,7 +46,9 @@ export function getCloudtentaclesOverrideRuleConfig(): CloudtentaclesOverrideRul
|
||||
})
|
||||
}
|
||||
|
||||
export function saveCloudtentaclesOverrideRuleConfig(rawValue: unknown): Promise<CloudtentaclesOverrideRuleConfig> {
|
||||
export function saveCloudtentaclesOverrideRuleConfig(
|
||||
rawValue: unknown,
|
||||
): Promise<CloudtentaclesOverrideRuleConfig> {
|
||||
return saveAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.cloudtentaclesOverrideRules,
|
||||
legacyFilePath: CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH,
|
||||
@@ -69,14 +68,19 @@ export function resolveCloudtentaclesOverrideRule(productName: unknown) {
|
||||
return null
|
||||
}
|
||||
|
||||
return config.rules.find((rule) =>
|
||||
rule.enabled !== false &&
|
||||
rule.normalizedProductName === normalizedProductName &&
|
||||
rule.deliveryItems.length > 0
|
||||
) || null
|
||||
return (
|
||||
config.rules.find(
|
||||
(rule) =>
|
||||
rule.enabled !== false &&
|
||||
rule.normalizedProductName === normalizedProductName &&
|
||||
rule.deliveryItems.length > 0,
|
||||
) || null
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeCloudtentaclesOverrideRuleConfig(rawValue: unknown): CloudtentaclesOverrideRuleConfig {
|
||||
export function normalizeCloudtentaclesOverrideRuleConfig(
|
||||
rawValue: unknown,
|
||||
): CloudtentaclesOverrideRuleConfig {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
const rules = Array.isArray(source.rules) ? source.rules : []
|
||||
|
||||
@@ -142,7 +146,10 @@ function mergeDeliveryItems(items: CloudtentaclesOverrideDeliveryItem[]) {
|
||||
const merged = new Map<string, CloudtentaclesOverrideDeliveryItem>()
|
||||
|
||||
for (const item of items) {
|
||||
const key = item.cloudSkuId > 0 ? `id:${item.cloudSkuId}` : `name:${normalizeCloudtentaclesMatchName(item.cloudSkuName)}`
|
||||
const key =
|
||||
item.cloudSkuId > 0
|
||||
? `id:${item.cloudSkuId}`
|
||||
: `name:${normalizeCloudtentaclesMatchName(item.cloudSkuName)}`
|
||||
const existing = merged.get(key)
|
||||
if (existing) {
|
||||
existing.quantity += item.quantity
|
||||
|
||||
@@ -13,7 +13,9 @@ type CreatedTaskInput = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
function stubOrder(patch: Partial<OrderRow> & Pick<OrderRow, 'id' | 'platform_order_id'>): OrderRow {
|
||||
function stubOrder(
|
||||
patch: Partial<OrderRow> & Pick<OrderRow, 'id' | 'platform_order_id'>,
|
||||
): OrderRow {
|
||||
return {
|
||||
id: patch.id,
|
||||
order_id: patch.id,
|
||||
|
||||
@@ -10,10 +10,7 @@ import { preparePaidFulfillmentTask } from '../fulfillment/executors/registry.js
|
||||
import type { FulfillmentPrepareDeps } from '../fulfillment/executors/types.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
import {
|
||||
TASK_STATUS,
|
||||
resolveInitialPaidTaskStatus,
|
||||
} from '../../domain/task-status.js'
|
||||
import { TASK_STATUS, resolveInitialPaidTaskStatus } from '../../domain/task-status.js'
|
||||
import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
type ClaimTokenLike = {
|
||||
@@ -152,10 +149,7 @@ export async function syncDeliveryTasksForOrderWithDeps(
|
||||
return preparedTasks.filter(isTaskRow)
|
||||
}
|
||||
|
||||
async function preparePaidTasks(
|
||||
tasks: TaskRow[],
|
||||
deps: FulfillmentPrepareDeps,
|
||||
) {
|
||||
async function preparePaidTasks(tasks: TaskRow[], deps: FulfillmentPrepareDeps) {
|
||||
const preparedTasks = await Promise.all(
|
||||
tasks.map((task) => preparePaidFulfillmentTask(task, deps)),
|
||||
)
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
isOrderItemConfiguredForFulfillment,
|
||||
mergeSourceOrderState,
|
||||
} from './order-service.js'
|
||||
import { isOrderItemConfiguredForFulfillment, mergeSourceOrderState } from './order-service.js'
|
||||
import type { OrderItemRow } from '../../types/repository/rows.js'
|
||||
|
||||
function buildOrderItem(snapshot: OrderItemRow['item_snapshot_json']): OrderItemRow {
|
||||
@@ -20,17 +17,11 @@ function buildOrderItem(snapshot: OrderItemRow['item_snapshot_json']): OrderItem
|
||||
}
|
||||
|
||||
test('isOrderItemConfiguredForFulfillment accepts PostgreSQL JSONB object results', () => {
|
||||
assert.equal(
|
||||
isOrderItemConfiguredForFulfillment(buildOrderItem({ isConfigured: true })),
|
||||
true,
|
||||
)
|
||||
assert.equal(isOrderItemConfiguredForFulfillment(buildOrderItem({ isConfigured: true })), true)
|
||||
})
|
||||
|
||||
test('isOrderItemConfiguredForFulfillment accepts serialized snapshots', () => {
|
||||
assert.equal(
|
||||
isOrderItemConfiguredForFulfillment(buildOrderItem('{"isConfigured":true}')),
|
||||
true,
|
||||
)
|
||||
assert.equal(isOrderItemConfiguredForFulfillment(buildOrderItem('{"isConfigured":true}')), true)
|
||||
})
|
||||
|
||||
test('isOrderItemConfiguredForFulfillment rejects unconfigured or malformed snapshots', () => {
|
||||
|
||||
@@ -297,7 +297,7 @@ function parseOrderItemSnapshot(value: unknown): Record<string, unknown> | null
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '{}'))
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? parsed as Record<string, unknown>
|
||||
? (parsed as Record<string, unknown>)
|
||||
: null
|
||||
} catch {
|
||||
return null
|
||||
|
||||
@@ -21,14 +21,19 @@ export function getAffiliateDashConfig(overrides: Partial<AffiliateDashRuntimeCo
|
||||
|
||||
return {
|
||||
enabled: config.enabled !== false,
|
||||
baseUrl: String(config.baseUrl || '').trim().replace(/\/+$/, ''),
|
||||
baseUrl: String(config.baseUrl || '')
|
||||
.trim()
|
||||
.replace(/\/+$/, ''),
|
||||
appKey: String(config.appKey || '').trim(),
|
||||
appSecret: String(config.appSecret || '').trim(),
|
||||
callbackSecret: String(config.callbackSecret || '').trim(),
|
||||
timeoutMs: Math.max(1, Number(config.timeoutMs || 10000) || 10000),
|
||||
notifyUrl: String(config.notifyUrl || '').trim(),
|
||||
timestampToleranceSeconds:
|
||||
Math.max(1, Number(config.timestampToleranceSeconds || AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS) || AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS),
|
||||
timestampToleranceSeconds: Math.max(
|
||||
1,
|
||||
Number(config.timestampToleranceSeconds || AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS) ||
|
||||
AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS,
|
||||
),
|
||||
preferredMatchEnabled: config.preferredMatchEnabled !== false,
|
||||
skuMapping: isRecord(config.skuMapping) ? config.skuMapping : {},
|
||||
}
|
||||
|
||||
@@ -95,15 +95,18 @@ export async function affiliateDashRequest(input: {
|
||||
logExternalHttpPacket('[affiliate-dash/http]', 'HTTP 响应失败', responsePacket, {
|
||||
level: 'warn',
|
||||
})
|
||||
throw createHttpError(summarizeAffiliateDashMessage(json) || `affiliate-dash 请求失败: HTTP ${response.status}`, {
|
||||
statusCode: 502,
|
||||
errorCode: 'affiliate_dash_http_failed',
|
||||
context: {
|
||||
status: response.status,
|
||||
message: summarizeAffiliateDashMessage(json),
|
||||
body: text,
|
||||
throw createHttpError(
|
||||
summarizeAffiliateDashMessage(json) || `affiliate-dash 请求失败: HTTP ${response.status}`,
|
||||
{
|
||||
statusCode: 502,
|
||||
errorCode: 'affiliate_dash_http_failed',
|
||||
context: {
|
||||
status: response.status,
|
||||
message: summarizeAffiliateDashMessage(json),
|
||||
body: text,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAffiliateDashSuccessResponse(json)) {
|
||||
|
||||
@@ -70,10 +70,7 @@ export async function getAffiliateDashDelivery(orderNo: string) {
|
||||
return mapAffiliateDashDeliveryInfo(json.data)
|
||||
}
|
||||
|
||||
export async function bindAffiliateDashDelivery(input: {
|
||||
orderNo: string
|
||||
gameAccount: string
|
||||
}) {
|
||||
export async function bindAffiliateDashDelivery(input: { orderNo: string; gameAccount: string }) {
|
||||
const json = await affiliateDashRequest({
|
||||
method: 'POST',
|
||||
pathname: `${ORDERS_PATH}/${encodeURIComponent(input.orderNo)}/delivery/bind`,
|
||||
@@ -82,10 +79,7 @@ export async function bindAffiliateDashDelivery(input: {
|
||||
return mapAffiliateDashBindResult(json.data)
|
||||
}
|
||||
|
||||
export async function getAffiliateDashBindResult(input: {
|
||||
orderNo: string
|
||||
bindUuid: string
|
||||
}) {
|
||||
export async function getAffiliateDashBindResult(input: { orderNo: string; bindUuid: string }) {
|
||||
const json = await affiliateDashRequest({
|
||||
method: 'GET',
|
||||
pathname:
|
||||
|
||||
@@ -3,10 +3,12 @@ import { affiliateDashRequest } from './http-client.js'
|
||||
export type AffiliateDashProduct = ReturnType<typeof mapAffiliateDashProduct>
|
||||
export type AffiliateDashProductListResult = ReturnType<typeof mapAffiliateDashProductList>
|
||||
|
||||
export async function listAffiliateDashProducts(input: {
|
||||
page?: number | undefined
|
||||
size?: number | undefined
|
||||
} = {}) {
|
||||
export async function listAffiliateDashProducts(
|
||||
input: {
|
||||
page?: number | undefined
|
||||
size?: number | undefined
|
||||
} = {},
|
||||
) {
|
||||
const params = new URLSearchParams()
|
||||
if (input.page !== undefined) {
|
||||
params.set('page', String(input.page))
|
||||
@@ -23,9 +25,11 @@ export async function listAffiliateDashProducts(input: {
|
||||
}
|
||||
|
||||
/** 翻页拉取全部可售商品(默认每页 100)。 */
|
||||
export async function listAllAffiliateDashProducts(input: {
|
||||
pageSize?: number | undefined
|
||||
} = {}) {
|
||||
export async function listAllAffiliateDashProducts(
|
||||
input: {
|
||||
pageSize?: number | undefined
|
||||
} = {},
|
||||
) {
|
||||
const size = Math.max(1, Number(input.pageSize || 100) || 100)
|
||||
const products: AffiliateDashProduct[] = []
|
||||
let page = 1
|
||||
|
||||
@@ -86,10 +86,7 @@ test('verifyCallbackSign accepts valid sign and rejects tampering', () => {
|
||||
const timestamp = '1783394218'
|
||||
const sign = buildCallbackSign({ callbackSecret, rawBody, timestamp })
|
||||
|
||||
assert.equal(
|
||||
verifyCallbackSign({ callbackSecret, rawBody, timestamp, sign }),
|
||||
true,
|
||||
)
|
||||
assert.equal(verifyCallbackSign({ callbackSecret, rawBody, timestamp, sign }), true)
|
||||
assert.equal(
|
||||
verifyCallbackSign({
|
||||
callbackSecret,
|
||||
|
||||
@@ -96,7 +96,12 @@ export function verifyCallbackSign(input: {
|
||||
timestamp: input.timestamp,
|
||||
})
|
||||
|
||||
return timingSafeEqualString(expected, String(input.sign || '').trim().toLowerCase())
|
||||
return timingSafeEqualString(
|
||||
expected,
|
||||
String(input.sign || '')
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
)
|
||||
}
|
||||
|
||||
export function timingSafeEqualString(left: string, right: string): boolean {
|
||||
|
||||
@@ -35,7 +35,9 @@ export function getAffiliateDashSourceConfig(): AffiliateDashSourceConfig {
|
||||
})
|
||||
}
|
||||
|
||||
export function saveAffiliateDashSourceConfig(rawValue: unknown): Promise<AffiliateDashSourceConfig> {
|
||||
export function saveAffiliateDashSourceConfig(
|
||||
rawValue: unknown,
|
||||
): Promise<AffiliateDashSourceConfig> {
|
||||
return saveAppConfigEntry({
|
||||
configKey: AFFILIATE_DASH_CONFIG_KEY,
|
||||
value: rawValue,
|
||||
@@ -48,7 +50,9 @@ export function normalizeAffiliateDashSourceConfig(rawValue: unknown): Affiliate
|
||||
|
||||
return {
|
||||
enabled: source.enabled !== false,
|
||||
baseUrl: String(source.baseUrl || '').trim().replace(/\/+$/, ''),
|
||||
baseUrl: String(source.baseUrl || '')
|
||||
.trim()
|
||||
.replace(/\/+$/, ''),
|
||||
appKey: String(source.appKey || '').trim(),
|
||||
appSecret: String(source.appSecret || '').trim(),
|
||||
callbackSecret: String(source.callbackSecret || '').trim(),
|
||||
|
||||
@@ -59,7 +59,8 @@ test('verifyAffiliateDashCallback rejects tampered body', () => {
|
||||
|
||||
assert.throws(
|
||||
() => verifyAffiliateDashCallback(input, { callbackSecret: CALLBACK_SECRET }),
|
||||
(error: Error & { errorCode?: string }) => error.errorCode === 'affiliate_dash_callback_sign_invalid',
|
||||
(error: Error & { errorCode?: string }) =>
|
||||
error.errorCode === 'affiliate_dash_callback_sign_invalid',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -107,10 +108,9 @@ test('verifyAffiliateDashCallback passes non-JSON body as long as sign matches',
|
||||
const timestamp = String(Math.floor(Date.now() / 1000))
|
||||
const rawBody = 'not-json'
|
||||
|
||||
const result = verifyAffiliateDashCallback(
|
||||
makeCallback({ rawBody, timestamp }),
|
||||
{ callbackSecret: CALLBACK_SECRET },
|
||||
)
|
||||
const result = verifyAffiliateDashCallback(makeCallback({ rawBody, timestamp }), {
|
||||
callbackSecret: CALLBACK_SECRET,
|
||||
})
|
||||
|
||||
assert.equal(result.event, '')
|
||||
assert.deepEqual(result.data, {})
|
||||
|
||||
@@ -75,12 +75,14 @@ export function verifyAffiliateDashCallback(
|
||||
})
|
||||
}
|
||||
|
||||
if (!verifyCallbackSign({
|
||||
callbackSecret: config.callbackSecret,
|
||||
rawBody,
|
||||
timestamp,
|
||||
sign,
|
||||
})) {
|
||||
if (
|
||||
!verifyCallbackSign({
|
||||
callbackSecret: config.callbackSecret,
|
||||
rawBody,
|
||||
timestamp,
|
||||
sign,
|
||||
})
|
||||
) {
|
||||
throw createHttpError('affiliate-dash 回调验签失败', {
|
||||
statusCode: 401,
|
||||
errorCode: 'affiliate_dash_callback_sign_invalid',
|
||||
|
||||
@@ -4,7 +4,11 @@ import { resolveCloudtentaclesConfig } from './helpers.js'
|
||||
import type { JsonObject } from '../../../types/json.js'
|
||||
|
||||
export async function getCloudtentaclesAsset(payload: JsonObject = {}) {
|
||||
const token = requireToken(payload.token, 'cloudtentacles 余额查询缺少 token', 'cloudtentacles_asset_missing_token')
|
||||
const token = requireToken(
|
||||
payload.token,
|
||||
'cloudtentacles 余额查询缺少 token',
|
||||
'cloudtentacles_asset_missing_token',
|
||||
)
|
||||
const config = resolveCloudtentaclesConfig(payload)
|
||||
|
||||
const result = await cloudtentaclesRequest(config.assetPath, {
|
||||
@@ -14,7 +18,10 @@ export async function getCloudtentaclesAsset(payload: JsonObject = {}) {
|
||||
contentType: 'application/json',
|
||||
sourceKey: payload.sourceKey || payload.resolvedSourceKey,
|
||||
accountLabel: payload.accountLabel,
|
||||
context: { operation: 'asset_get', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' },
|
||||
context: {
|
||||
operation: 'asset_get',
|
||||
sourceKey: payload.sourceKey || payload.resolvedSourceKey || '',
|
||||
},
|
||||
businessErrorStatusCode: 401,
|
||||
businessErrorCode: 'cloudtentacles_asset_failed',
|
||||
})
|
||||
@@ -27,7 +34,11 @@ export async function getCloudtentaclesAsset(payload: JsonObject = {}) {
|
||||
}
|
||||
|
||||
export async function getCloudtentaclesCategories(payload: JsonObject = {}) {
|
||||
const token = requireToken(payload.token, 'cloudtentacles 分类查询缺少 token', 'cloudtentacles_categories_missing_token')
|
||||
const token = requireToken(
|
||||
payload.token,
|
||||
'cloudtentacles 分类查询缺少 token',
|
||||
'cloudtentacles_categories_missing_token',
|
||||
)
|
||||
const config = resolveCloudtentaclesConfig(payload)
|
||||
|
||||
const result = await cloudtentaclesRequest(config.categoriesPath, {
|
||||
@@ -37,7 +48,10 @@ export async function getCloudtentaclesCategories(payload: JsonObject = {}) {
|
||||
contentType: 'application/json',
|
||||
sourceKey: payload.sourceKey || payload.resolvedSourceKey,
|
||||
accountLabel: payload.accountLabel,
|
||||
context: { operation: 'categories_list', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' },
|
||||
context: {
|
||||
operation: 'categories_list',
|
||||
sourceKey: payload.sourceKey || payload.resolvedSourceKey || '',
|
||||
},
|
||||
businessErrorStatusCode: 401,
|
||||
businessErrorCode: 'cloudtentacles_categories_failed',
|
||||
})
|
||||
@@ -53,7 +67,11 @@ export async function getCloudtentaclesCategories(payload: JsonObject = {}) {
|
||||
}
|
||||
|
||||
export async function listCloudtentaclesSku(payload: JsonObject = {}) {
|
||||
const token = requireToken(payload.token, 'cloudtentacles SKU 列表查询缺少 token', 'cloudtentacles_sku_list_missing_token')
|
||||
const token = requireToken(
|
||||
payload.token,
|
||||
'cloudtentacles SKU 列表查询缺少 token',
|
||||
'cloudtentacles_sku_list_missing_token',
|
||||
)
|
||||
const config = resolveCloudtentaclesConfig(payload)
|
||||
|
||||
const result = await cloudtentaclesRequest(config.skuListPath, {
|
||||
@@ -63,7 +81,10 @@ export async function listCloudtentaclesSku(payload: JsonObject = {}) {
|
||||
contentType: 'application/json',
|
||||
sourceKey: payload.sourceKey || payload.resolvedSourceKey,
|
||||
accountLabel: payload.accountLabel,
|
||||
context: { operation: 'sku_list', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' },
|
||||
context: {
|
||||
operation: 'sku_list',
|
||||
sourceKey: payload.sourceKey || payload.resolvedSourceKey || '',
|
||||
},
|
||||
businessErrorStatusCode: 401,
|
||||
businessErrorCode: 'cloudtentacles_sku_list_failed',
|
||||
})
|
||||
@@ -79,8 +100,16 @@ export async function listCloudtentaclesSku(payload: JsonObject = {}) {
|
||||
}
|
||||
|
||||
export async function buyCloudtentaclesSku(payload: JsonObject = {}) {
|
||||
const token = requireToken(payload.token, 'cloudtentacles 购买 SKU 缺少 token', 'cloudtentacles_sku_buy_missing_token')
|
||||
const skuId = requireId(payload.id, 'cloudtentacles 购买 SKU 缺少商品 id', 'cloudtentacles_sku_buy_missing_id')
|
||||
const token = requireToken(
|
||||
payload.token,
|
||||
'cloudtentacles 购买 SKU 缺少 token',
|
||||
'cloudtentacles_sku_buy_missing_token',
|
||||
)
|
||||
const skuId = requireId(
|
||||
payload.id,
|
||||
'cloudtentacles 购买 SKU 缺少商品 id',
|
||||
'cloudtentacles_sku_buy_missing_id',
|
||||
)
|
||||
const count = requireCount(payload.count)
|
||||
const config = resolveCloudtentaclesConfig(payload)
|
||||
|
||||
@@ -114,8 +143,16 @@ export async function buyCloudtentaclesSku(payload: JsonObject = {}) {
|
||||
}
|
||||
|
||||
export async function useCloudtentaclesSku(payload: JsonObject = {}) {
|
||||
const token = requireToken(payload.token, 'cloudtentacles 发货缺少 token', 'cloudtentacles_sku_use_missing_token')
|
||||
const skuId = requireId(payload.id, 'cloudtentacles 发货缺少商品 id', 'cloudtentacles_sku_use_missing_id')
|
||||
const token = requireToken(
|
||||
payload.token,
|
||||
'cloudtentacles 发货缺少 token',
|
||||
'cloudtentacles_sku_use_missing_token',
|
||||
)
|
||||
const skuId = requireId(
|
||||
payload.id,
|
||||
'cloudtentacles 发货缺少商品 id',
|
||||
'cloudtentacles_sku_use_missing_id',
|
||||
)
|
||||
const virtualNumberId = requireId(
|
||||
payload.virtualNumberId,
|
||||
'cloudtentacles 发货缺少虚拟号 id',
|
||||
|
||||
@@ -5,7 +5,9 @@ import { createHttpError } from '../../../utils/http.js'
|
||||
import { resolveCloudtentaclesConfig } from './helpers.js'
|
||||
|
||||
export function md5CloudtentaclesPassword(password: unknown) {
|
||||
return createHash('md5').update(String(password || ''), 'utf8').digest('hex')
|
||||
return createHash('md5')
|
||||
.update(String(password || ''), 'utf8')
|
||||
.digest('hex')
|
||||
}
|
||||
|
||||
export function encryptCloudtentaclesPayload(payload: JsonObject = {}, options: JsonObject = {}) {
|
||||
@@ -20,7 +22,8 @@ export function encryptCloudtentaclesPayload(payload: JsonObject = {}, options:
|
||||
}
|
||||
|
||||
const timestamp = Number(options.timestamp || Date.now())
|
||||
const randomValue = String(options.randomValue || Math.random().toString(16)).trim() || Math.random().toString(16)
|
||||
const randomValue =
|
||||
String(options.randomValue || Math.random().toString(16)).trim() || Math.random().toString(16)
|
||||
const normalizedPayload = removeEmptyFields(payload)
|
||||
const plaintext = JSON.stringify({
|
||||
t: timestamp,
|
||||
@@ -59,6 +62,8 @@ function removeEmptyFields(payload: unknown) {
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(payload).filter(([, value]) => value !== '' && value !== null && typeof value !== 'undefined'),
|
||||
Object.entries(payload).filter(
|
||||
([, value]) => value !== '' && value !== null && typeof value !== 'undefined',
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { getCloudtentaclesAsset, listCloudtentaclesSku, buyCloudtentaclesSku } from './catalog-service.js'
|
||||
import {
|
||||
getCloudtentaclesAsset,
|
||||
listCloudtentaclesSku,
|
||||
buyCloudtentaclesSku,
|
||||
} from './catalog-service.js'
|
||||
import { getCloudtentaclesKnapsack } from './knapsack-service.js'
|
||||
import type { JsonObject } from '../../../types/json.js'
|
||||
import {
|
||||
@@ -55,15 +59,22 @@ export async function runCloudtentaclesFullDebugFlow(payload: JsonObject = {}) {
|
||||
}
|
||||
|
||||
await sleep(350)
|
||||
const beforeKnapsack = await runFlowStep('查询购买前背包', () => getCloudtentaclesKnapsack(payload))
|
||||
const beforeKnapsack = await runFlowStep('查询购买前背包', () =>
|
||||
getCloudtentaclesKnapsack(payload),
|
||||
)
|
||||
const beforeItem = findKnapsackItem(beforeKnapsack.items, skuId)
|
||||
|
||||
await sleep(500)
|
||||
const buyResult = await runFlowStep('购买 SKU', () => buyCloudtentaclesSku({
|
||||
...payload,
|
||||
id: skuId,
|
||||
count: skuCount,
|
||||
}), { retries: 1, retryDelayMs: 1200 })
|
||||
const buyResult = await runFlowStep(
|
||||
'购买 SKU',
|
||||
() =>
|
||||
buyCloudtentaclesSku({
|
||||
...payload,
|
||||
id: skuId,
|
||||
count: skuCount,
|
||||
}),
|
||||
{ retries: 1, retryDelayMs: 1200 },
|
||||
)
|
||||
|
||||
await sleep(1200)
|
||||
const afterAsset = await runFlowStep('查询购买后余额', () => getCloudtentaclesAsset(payload), {
|
||||
@@ -71,10 +82,14 @@ export async function runCloudtentaclesFullDebugFlow(payload: JsonObject = {}) {
|
||||
retryDelayMs: 1200,
|
||||
})
|
||||
await sleep(1200)
|
||||
const afterKnapsack = await runFlowStep('查询购买后背包', () => getCloudtentaclesKnapsack(payload), {
|
||||
retries: 2,
|
||||
retryDelayMs: 1200,
|
||||
})
|
||||
const afterKnapsack = await runFlowStep(
|
||||
'查询购买后背包',
|
||||
() => getCloudtentaclesKnapsack(payload),
|
||||
{
|
||||
retries: 2,
|
||||
retryDelayMs: 1200,
|
||||
},
|
||||
)
|
||||
const afterItem = findKnapsackItem(afterKnapsack.items, skuId)
|
||||
|
||||
const beforeCount = Number(beforeItem?.count || 0)
|
||||
@@ -82,17 +97,25 @@ export async function runCloudtentaclesFullDebugFlow(payload: JsonObject = {}) {
|
||||
const knapsackIncreased = afterCount >= beforeCount + skuCount
|
||||
|
||||
if (!knapsackIncreased) {
|
||||
throw createHttpError(`购买后背包校验失败,购买前 ${beforeCount},购买后 ${afterCount},期望至少 ${beforeCount + skuCount}`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'cloudtentacles_full_flow_knapsack_not_updated',
|
||||
})
|
||||
throw createHttpError(
|
||||
`购买后背包校验失败,购买前 ${beforeCount},购买后 ${afterCount},期望至少 ${beforeCount + skuCount}`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'cloudtentacles_full_flow_knapsack_not_updated',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
await sleep(700)
|
||||
const appointed = await runFlowStep('申请虚拟号', () => appointCloudtentaclesVirtualNumber({
|
||||
...payload,
|
||||
key: vnKey,
|
||||
}), { retries: 1, retryDelayMs: 1200 })
|
||||
const appointed = await runFlowStep(
|
||||
'申请虚拟号',
|
||||
() =>
|
||||
appointCloudtentaclesVirtualNumber({
|
||||
...payload,
|
||||
key: vnKey,
|
||||
}),
|
||||
{ retries: 1, retryDelayMs: 1200 },
|
||||
)
|
||||
const appointedId = Number(appointed.item?.id || 0)
|
||||
const appointedPhone = String(appointed.item?.phone || '').trim()
|
||||
|
||||
@@ -104,33 +127,53 @@ export async function runCloudtentaclesFullDebugFlow(payload: JsonObject = {}) {
|
||||
}
|
||||
|
||||
await sleep(1200)
|
||||
const generateCodeResult = await runFlowStep('生成登录码', () => generateCloudtentaclesLoginCode({
|
||||
...payload,
|
||||
key: vnKey,
|
||||
id: appointedId,
|
||||
}), { retries: 1, retryDelayMs: 1200 })
|
||||
const generateCodeResult = await runFlowStep(
|
||||
'生成登录码',
|
||||
() =>
|
||||
generateCloudtentaclesLoginCode({
|
||||
...payload,
|
||||
key: vnKey,
|
||||
id: appointedId,
|
||||
}),
|
||||
{ retries: 1, retryDelayMs: 1200 },
|
||||
)
|
||||
|
||||
await sleep(1600)
|
||||
const fetchedCode = await runFlowStep('获取验证码', () => fetchCloudtentaclesVirtualNumberCode({
|
||||
...payload,
|
||||
key: vnKey,
|
||||
phone: appointedPhone,
|
||||
}), { retries: 2, retryDelayMs: 1500 })
|
||||
const fetchedCode = await runFlowStep(
|
||||
'获取验证码',
|
||||
() =>
|
||||
fetchCloudtentaclesVirtualNumberCode({
|
||||
...payload,
|
||||
key: vnKey,
|
||||
phone: appointedPhone,
|
||||
}),
|
||||
{ retries: 2, retryDelayMs: 1500 },
|
||||
)
|
||||
|
||||
await sleep(1000)
|
||||
const verified = await runFlowStep('校验验证码', () => verifyCloudtentaclesLoginCode({
|
||||
...payload,
|
||||
key: vnKey,
|
||||
id: appointedId,
|
||||
code: fetchedCode.code,
|
||||
}), { retries: 1, retryDelayMs: 1200 })
|
||||
const verified = await runFlowStep(
|
||||
'校验验证码',
|
||||
() =>
|
||||
verifyCloudtentaclesLoginCode({
|
||||
...payload,
|
||||
key: vnKey,
|
||||
id: appointedId,
|
||||
code: fetchedCode.code,
|
||||
}),
|
||||
{ retries: 1, retryDelayMs: 1200 },
|
||||
)
|
||||
|
||||
await sleep(1000)
|
||||
const bindUrlResult = await runFlowStep('获取兑换链接', () => getCloudtentaclesBindUrl({
|
||||
...payload,
|
||||
key: vnKey,
|
||||
id: appointedId,
|
||||
}), { retries: 1, retryDelayMs: 1200 })
|
||||
const bindUrlResult = await runFlowStep(
|
||||
'获取兑换链接',
|
||||
() =>
|
||||
getCloudtentaclesBindUrl({
|
||||
...payload,
|
||||
key: vnKey,
|
||||
id: appointedId,
|
||||
}),
|
||||
{ retries: 1, retryDelayMs: 1200 },
|
||||
)
|
||||
|
||||
return {
|
||||
sku: {
|
||||
@@ -173,7 +216,10 @@ function normalizePositiveInteger(value: unknown, fallback: number) {
|
||||
}
|
||||
|
||||
function findKnapsackItem(items: unknown, skuId: unknown) {
|
||||
return (Array.isArray(items) ? items : []).find((item) => Number(item.id || 0) === Number(skuId)) || null
|
||||
return (
|
||||
(Array.isArray(items) ? items : []).find((item) => Number(item.id || 0) === Number(skuId)) ||
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
async function runFlowStep<T>(
|
||||
@@ -184,7 +230,8 @@ async function runFlowStep<T>(
|
||||
const rawRetries = Number(options.retries)
|
||||
const rawRetryDelayMs = Number(options.retryDelayMs)
|
||||
const retries = Number.isInteger(rawRetries) && rawRetries > 0 ? rawRetries : 0
|
||||
const retryDelayMs = Number.isFinite(rawRetryDelayMs) && rawRetryDelayMs > 0 ? rawRetryDelayMs : 1000
|
||||
const retryDelayMs =
|
||||
Number.isFinite(rawRetryDelayMs) && rawRetryDelayMs > 0 ? rawRetryDelayMs : 1000
|
||||
|
||||
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
||||
try {
|
||||
@@ -221,7 +268,9 @@ function wrapStepError(label: string, error: unknown) {
|
||||
|
||||
if (error && typeof error === 'object' && 'statusCode' in error) {
|
||||
const statusCode = Number(Reflect.get(error, 'statusCode') || 500)
|
||||
const errorCode = String(Reflect.get(error, 'errorCode') || 'cloudtentacles_full_flow_step_failed')
|
||||
const errorCode = String(
|
||||
Reflect.get(error, 'errorCode') || 'cloudtentacles_full_flow_step_failed',
|
||||
)
|
||||
|
||||
return createHttpError(`cloudtentacles ${label}失败:${message}`, {
|
||||
statusCode,
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
export const DEFAULT_CLOUDTENTACLES_DEVICE_ID =
|
||||
"08bc9d8c-fd15-48ea-bc00-8d754076cafc";
|
||||
export const DEFAULT_CLOUDTENTACLES_DEVICE_TYPE = 1;
|
||||
export const DEFAULT_CLOUDTENTACLES_DEVICE_ID = '08bc9d8c-fd15-48ea-bc00-8d754076cafc'
|
||||
export const DEFAULT_CLOUDTENTACLES_DEVICE_TYPE = 1
|
||||
|
||||
export function normalizeCloudtentaclesDeviceId(value: unknown) {
|
||||
const normalized = String(value || "").trim();
|
||||
return normalized && normalized !== "-"
|
||||
? normalized
|
||||
: DEFAULT_CLOUDTENTACLES_DEVICE_ID;
|
||||
const normalized = String(value || '').trim()
|
||||
return normalized && normalized !== '-' ? normalized : DEFAULT_CLOUDTENTACLES_DEVICE_ID
|
||||
}
|
||||
|
||||
export function normalizeCloudtentaclesDeviceType(value: unknown) {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed > 0
|
||||
? parsed
|
||||
: DEFAULT_CLOUDTENTACLES_DEVICE_TYPE;
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_CLOUDTENTACLES_DEVICE_TYPE
|
||||
}
|
||||
|
||||
@@ -52,46 +52,98 @@ export function resolveCloudtentaclesConfig(overrides: Partial<CloudtentaclesRun
|
||||
return {
|
||||
baseUrl: normalizeBaseUrl(overrides.baseUrl || baseConfig.baseUrl || 'https://123.207.217.176'),
|
||||
timeoutMs: normalizePositiveInteger(overrides.timeoutMs || baseConfig.timeoutMs, 5000),
|
||||
sendSmsPath: normalizePath(overrides.sendSmsPath || baseConfig.sendSmsPath, '/public/verif_code'),
|
||||
sendSmsPath: normalizePath(
|
||||
overrides.sendSmsPath || baseConfig.sendSmsPath,
|
||||
'/public/verif_code',
|
||||
),
|
||||
loginPath: normalizePath(overrides.loginPath || baseConfig.loginPath, '/public/login'),
|
||||
userInfoPath: normalizePath(overrides.userInfoPath || baseConfig.userInfoPath, '/user/info'),
|
||||
assetPath: normalizePath(overrides.assetPath || baseConfig.assetPath, '/user/get_asset'),
|
||||
permissionPath: normalizePath(overrides.permissionPath || baseConfig.permissionPath, '/user/get_permission'),
|
||||
categoriesPath: normalizePath(overrides.categoriesPath || baseConfig.categoriesPath, '/categories/get'),
|
||||
permissionPath: normalizePath(
|
||||
overrides.permissionPath || baseConfig.permissionPath,
|
||||
'/user/get_permission',
|
||||
),
|
||||
categoriesPath: normalizePath(
|
||||
overrides.categoriesPath || baseConfig.categoriesPath,
|
||||
'/categories/get',
|
||||
),
|
||||
skuListPath: normalizePath(overrides.skuListPath || baseConfig.skuListPath, '/sku/list'),
|
||||
skuBuyPath: normalizePath(overrides.skuBuyPath || baseConfig.skuBuyPath, '/sku/buy'),
|
||||
skuUsePath: normalizePath(overrides.skuUsePath || baseConfig.skuUsePath, '/sku/use'),
|
||||
knapsackPath: normalizePath(overrides.knapsackPath || baseConfig.knapsackPath, '/user/get_knapsack'),
|
||||
knapsackPath: normalizePath(
|
||||
overrides.knapsackPath || baseConfig.knapsackPath,
|
||||
'/user/get_knapsack',
|
||||
),
|
||||
vnListPath: normalizePath(overrides.vnListPath || baseConfig.vnListPath, '/vn/list'),
|
||||
vnAppointPath: normalizePath(overrides.vnAppointPath || baseConfig.vnAppointPath, '/vn/appoint'),
|
||||
vnAppointPath: normalizePath(
|
||||
overrides.vnAppointPath || baseConfig.vnAppointPath,
|
||||
'/vn/appoint',
|
||||
),
|
||||
vnGenerateLoginCodePath: normalizePath(
|
||||
overrides.vnGenerateLoginCodePath || baseConfig.vnGenerateLoginCodePath,
|
||||
'/vn/generate_login_code',
|
||||
),
|
||||
vnVerifCodePath: normalizePath(overrides.vnVerifCodePath || baseConfig.vnVerifCodePath, '/public/vn_verif_code'),
|
||||
vnVerifCodePath: normalizePath(
|
||||
overrides.vnVerifCodePath || baseConfig.vnVerifCodePath,
|
||||
'/public/vn_verif_code',
|
||||
),
|
||||
vnVerifyLoginCodePath: normalizePath(
|
||||
overrides.vnVerifyLoginCodePath || baseConfig.vnVerifyLoginCodePath,
|
||||
'/vn/verify_login_code',
|
||||
),
|
||||
vnBindUrlPath: normalizePath(overrides.vnBindUrlPath || baseConfig.vnBindUrlPath, '/vn/bind_url'),
|
||||
vnBindInfoPath: normalizePath(overrides.vnBindInfoPath || baseConfig.vnBindInfoPath, '/vn/bind_info'),
|
||||
vnBindUrlPath: normalizePath(
|
||||
overrides.vnBindUrlPath || baseConfig.vnBindUrlPath,
|
||||
'/vn/bind_url',
|
||||
),
|
||||
vnBindInfoPath: normalizePath(
|
||||
overrides.vnBindInfoPath || baseConfig.vnBindInfoPath,
|
||||
'/vn/bind_info',
|
||||
),
|
||||
vnBackPath: normalizePath(overrides.vnBackPath || baseConfig.vnBackPath, '/vn/back'),
|
||||
bindUrlTtlSeconds: normalizePositiveInteger(overrides.bindUrlTtlSeconds || baseConfig.bindUrlTtlSeconds, 600),
|
||||
bindUrlTtlSeconds: normalizePositiveInteger(
|
||||
overrides.bindUrlTtlSeconds || baseConfig.bindUrlTtlSeconds,
|
||||
600,
|
||||
),
|
||||
bindUrlProbeIntervalSeconds: normalizePositiveInteger(
|
||||
overrides.bindUrlProbeIntervalSeconds || baseConfig.bindUrlProbeIntervalSeconds,
|
||||
30,
|
||||
),
|
||||
bindUrlProbeTimeoutMs: normalizePositiveInteger(overrides.bindUrlProbeTimeoutMs || baseConfig.bindUrlProbeTimeoutMs, 5000),
|
||||
bindUrlProbeUserAgent: String(overrides.bindUrlProbeUserAgent || baseConfig.bindUrlProbeUserAgent || '').trim(),
|
||||
bindUrlProbeEndpoint: String(overrides.bindUrlProbeEndpoint || baseConfig.bindUrlProbeEndpoint || 'https://comm.ams.game.qq.com/ide/').trim(),
|
||||
bindUrlProbeChartId: String(overrides.bindUrlProbeChartId || baseConfig.bindUrlProbeChartId || '323794').trim(),
|
||||
bindUrlProbeSubChartId: String(overrides.bindUrlProbeSubChartId || baseConfig.bindUrlProbeSubChartId || '323794').trim(),
|
||||
bindUrlProbeIdeToken: String(overrides.bindUrlProbeIdeToken || baseConfig.bindUrlProbeIdeToken || 'z90Syo').trim(),
|
||||
bindUrlProbeActivityUrl: String(overrides.bindUrlProbeActivityUrl || baseConfig.bindUrlProbeActivityUrl || 'http%3A%2F%2Fgp.qq.com%2Fcp%2Fa20240828cmcc%2F').trim(),
|
||||
bindUrlProbeReferer: String(overrides.bindUrlProbeReferer || baseConfig.bindUrlProbeReferer || 'https://gp.qq.com/').trim(),
|
||||
bindUrlProbeExtraCookie: String(overrides.bindUrlProbeExtraCookie || baseConfig.bindUrlProbeExtraCookie || '').trim(),
|
||||
bindUrlProbeTimeoutMs: normalizePositiveInteger(
|
||||
overrides.bindUrlProbeTimeoutMs || baseConfig.bindUrlProbeTimeoutMs,
|
||||
5000,
|
||||
),
|
||||
bindUrlProbeUserAgent: String(
|
||||
overrides.bindUrlProbeUserAgent || baseConfig.bindUrlProbeUserAgent || '',
|
||||
).trim(),
|
||||
bindUrlProbeEndpoint: String(
|
||||
overrides.bindUrlProbeEndpoint ||
|
||||
baseConfig.bindUrlProbeEndpoint ||
|
||||
'https://comm.ams.game.qq.com/ide/',
|
||||
).trim(),
|
||||
bindUrlProbeChartId: String(
|
||||
overrides.bindUrlProbeChartId || baseConfig.bindUrlProbeChartId || '323794',
|
||||
).trim(),
|
||||
bindUrlProbeSubChartId: String(
|
||||
overrides.bindUrlProbeSubChartId || baseConfig.bindUrlProbeSubChartId || '323794',
|
||||
).trim(),
|
||||
bindUrlProbeIdeToken: String(
|
||||
overrides.bindUrlProbeIdeToken || baseConfig.bindUrlProbeIdeToken || 'z90Syo',
|
||||
).trim(),
|
||||
bindUrlProbeActivityUrl: String(
|
||||
overrides.bindUrlProbeActivityUrl ||
|
||||
baseConfig.bindUrlProbeActivityUrl ||
|
||||
'http%3A%2F%2Fgp.qq.com%2Fcp%2Fa20240828cmcc%2F',
|
||||
).trim(),
|
||||
bindUrlProbeReferer: String(
|
||||
overrides.bindUrlProbeReferer || baseConfig.bindUrlProbeReferer || 'https://gp.qq.com/',
|
||||
).trim(),
|
||||
bindUrlProbeExtraCookie: String(
|
||||
overrides.bindUrlProbeExtraCookie || baseConfig.bindUrlProbeExtraCookie || '',
|
||||
).trim(),
|
||||
publicKeyPem: normalizePem(overrides.publicKeyPem || baseConfig.publicKeyPem),
|
||||
clientSource: String(overrides.clientSource || baseConfig.clientSource || 'ct-client').trim() || 'ct-client',
|
||||
clientSource:
|
||||
String(overrides.clientSource || baseConfig.clientSource || 'ct-client').trim() ||
|
||||
'ct-client',
|
||||
deviceId: normalizeCloudtentaclesDeviceId(overrides.deviceId ?? baseConfig.deviceId),
|
||||
deviceType: normalizeCloudtentaclesDeviceType(overrides.deviceType ?? baseConfig.deviceType),
|
||||
}
|
||||
@@ -102,7 +154,10 @@ export function buildCloudtentaclesUrl(
|
||||
pathname: unknown,
|
||||
searchParams: JsonObject | null = null,
|
||||
) {
|
||||
const url = new URL(normalizePath(pathname, '/'), normalizeBaseUrl(baseUrl) || 'https://123.207.217.176')
|
||||
const url = new URL(
|
||||
normalizePath(pathname, '/'),
|
||||
normalizeBaseUrl(baseUrl) || 'https://123.207.217.176',
|
||||
)
|
||||
|
||||
if (searchParams && typeof searchParams === 'object') {
|
||||
for (const [key, value] of Object.entries(searchParams)) {
|
||||
@@ -146,7 +201,9 @@ export function buildCloudtentaclesHeaders({
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value: unknown) {
|
||||
return String(value || '').trim().replace(/\/+$/, '')
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
function normalizePath(value: unknown, fallback: string) {
|
||||
@@ -175,7 +232,12 @@ function normalizeHeaderMap(extra: unknown) {
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(extra)
|
||||
.map(([key, value]) => [String(key || '').trim().toLowerCase(), String(value || '').trim()])
|
||||
.map(([key, value]) => [
|
||||
String(key || '')
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
String(value || '').trim(),
|
||||
])
|
||||
.filter(([key, value]) => key && value),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,9 +24,8 @@ test('cloudtentaclesRequest retries high-frequency business errors', async () =>
|
||||
const server = await createMockServer(() => {
|
||||
callCount += 1
|
||||
return {
|
||||
body: callCount === 1
|
||||
? { code: 1, message: 'high-frequency Request' }
|
||||
: { code: 0, data: 'ok' },
|
||||
body:
|
||||
callCount === 1 ? { code: 1, message: 'high-frequency Request' } : { code: 0, data: 'ok' },
|
||||
}
|
||||
})
|
||||
|
||||
@@ -155,15 +154,16 @@ async function createMockServer(handler: () => MockResponse) {
|
||||
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address?.port}`,
|
||||
close: () => new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
close: () =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
}),
|
||||
resolve()
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user