移除快手小店以及 api

This commit is contained in:
yml2213
2026-07-09 21:03:39 +08:00
parent a075b8acf7
commit ffbc19a9db
38 changed files with 110 additions and 2724 deletions
@@ -6,7 +6,6 @@ import {
isPlainObject,
pickFirstNonEmpty,
resolveCloudtentaclesAdminContext,
resolveKuaishouEticketAdminContext,
} from './context.js'
test('pickFirstNonEmpty returns first trimmed non-empty string value', () => {
@@ -21,52 +20,6 @@ test('isPlainObject only accepts non-array objects', () => {
assert.equal(isPlainObject('x'), false)
})
test('resolveKuaishouEticketAdminContext prefers explicit payload values', () => {
const context = resolveKuaishouEticketAdminContext(
{
baseUrl: ' https://override.example.com ',
shopId: ' shop-2 ',
cookie: ' cookie-2 ',
},
{
savedSource: { baseUrl: 'https://saved.example.com' },
findShopConfig(requestedShopId) {
assert.equal(requestedShopId, 'shop-2')
return { shopId: 'shop-2', cookie: 'saved-cookie-2' }
},
getFirstAvailableShop() {
throw new Error('should not use first available shop')
},
},
)
assert.deepEqual(context, {
baseUrl: 'https://override.example.com',
cookie: 'cookie-2',
shopId: 'shop-2',
shop: { shopId: 'shop-2', cookie: 'saved-cookie-2' },
})
})
test('resolveKuaishouEticketAdminContext falls back to first available shop', () => {
const context = resolveKuaishouEticketAdminContext(
{},
{
savedSource: { baseUrl: ' https://saved.example.com ' },
getFirstAvailableShop() {
return { shopId: 'shop-1', cookie: 'cookie-1' }
},
},
)
assert.deepEqual(context, {
baseUrl: 'https://saved.example.com',
cookie: 'cookie-1',
shopId: 'shop-1',
shop: { shopId: 'shop-1', cookie: 'cookie-1' },
})
})
test('resolveCloudtentaclesAdminContext merges payload source and persisted session', () => {
const context = resolveCloudtentaclesAdminContext(
{
@@ -15,32 +15,6 @@ export function isPlainObject(value: unknown): value is JsonObject {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
export function resolveKuaishouEticketAdminContext(payload: JsonObject = {}, options: JsonObject = {}) {
const savedSource = options.savedSource || {};
const findShopConfig =
(options.findShopConfig || (() => null)) as (shopId: string, source: JsonObject) => unknown;
const getFirstAvailableShop =
(options.getFirstAvailableShop || (() => null)) as (source: JsonObject) => unknown;
const defaultBaseUrl = String(
options.defaultBaseUrl || "https://s.kwaixiaodian.com"
).trim();
const requestedShopId = String(payload.shopId || "").trim();
const configuredShop = (requestedShopId
? findShopConfig(requestedShopId, savedSource)
: getFirstAvailableShop(savedSource)) as JsonObject | null;
return {
baseUrl: pickFirstNonEmpty([
payload.baseUrl,
savedSource.baseUrl,
defaultBaseUrl,
]),
cookie: pickFirstNonEmpty([payload.cookie, configuredShop?.cookie]),
shopId: pickFirstNonEmpty([requestedShopId, configuredShop?.shopId]),
shop: configuredShop,
};
}
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
import {
@@ -3,7 +3,6 @@ import assert from 'node:assert/strict'
import {
mapAdminCloudtentaclesSession,
mapAdminKuaishouEticketSourceConfig,
maskPhone,
maskSecret,
} from './mappers.js'
@@ -20,30 +19,6 @@ test('maskPhone hides middle digits for mobile numbers', () => {
assert.equal(maskPhone(''), '')
})
test('mapAdminKuaishouEticketSourceConfig maps shops and masks cookies', () => {
assert.deepEqual(
mapAdminKuaishouEticketSourceConfig(
{ enabled: true, baseUrl: ' https://s.kwaixiaodian.com ' },
[{ shopId: ' 1 ', kshopName: ' A店 ', cookie: 'cookie-abcdef123456', enabled: true }],
),
{
enabled: true,
baseUrl: 'https://s.kwaixiaodian.com',
shops: [
{
shopId: '1',
kshopName: 'A店',
cookie: 'cookie-abcdef123456',
cookieMasked: 'cookie****123456',
hasCookie: true,
userAvatar: '',
enabled: true,
},
],
},
)
})
test('mapAdminCloudtentaclesSession derives masked fields and token presence', () => {
assert.deepEqual(
mapAdminCloudtentaclesSession({
@@ -17,26 +17,6 @@ export function maskPhone(value: unknown) {
return maskPhoneValue(value, { maskShort: false });
}
export function mapAdminKuaishouEticketShopItem(item: JsonObject = {}) {
return {
shopId: String(item.shopId || "").trim(),
kshopName: String(item.kshopName || "").trim(),
cookie: String(item.cookie || "").trim(),
cookieMasked: maskSecret(item.cookie),
hasCookie: Boolean(String(item.cookie || "").trim()),
userAvatar: String(item.userAvatar || "").trim(),
enabled: item.enabled !== false,
};
}
export function mapAdminKuaishouEticketSourceConfig(config: JsonObject = {}, shops: JsonObject[] = []) {
return {
enabled: config.enabled !== false,
baseUrl: String(config.baseUrl || "").trim(),
shops: shops.map((item) => mapAdminKuaishouEticketShopItem(item)),
};
}
export function mapAdminCloudtentaclesSourceConfig(config: JsonObject = {}) {
return {
key: String(config.key || "").trim(),
@@ -1,80 +0,0 @@
import {
consumeKuaishouEticket,
queryKuaishouEticketConsumeDetail,
} from '../../platforms/kuaishou-eticket/consume-service.js'
import {
findKuaishouEticketShopConfig,
getFirstAvailableKuaishouEticketShop,
getKuaishouEticketSourceConfig,
getKuaishouEticketSourceFilePath,
listKuaishouEticketShopConfigs,
saveKuaishouEticketSourceConfig,
} from '../../platforms/kuaishou-eticket/source-config-service.js'
import { queryKuaishouEticketStableInfo } from '../../platforms/kuaishou-eticket/info-service.js'
import { resolveKuaishouEticketAdminContext } from './cloudtentacles/context.js'
import { mapAdminKuaishouEticketSourceConfig } from './cloudtentacles/mappers.js'
type JsonObject = Record<string, any>
function resolveAdminKuaishouEticketContext(payload: JsonObject = {}) {
return resolveKuaishouEticketAdminContext(payload, {
savedSource: getKuaishouEticketSourceConfig(),
findShopConfig: findKuaishouEticketShopConfig,
getFirstAvailableShop: getFirstAvailableKuaishouEticketShop,
})
}
export function getAdminKuaishouEticketSourceConfig() {
const config = getKuaishouEticketSourceConfig()
return {
filePath: getKuaishouEticketSourceFilePath(),
source: mapAdminKuaishouEticketSourceConfig(config, listKuaishouEticketShopConfigs(config)),
}
}
export function updateAdminKuaishouEticketSourceConfig(payload: JsonObject = {}) {
const saved = saveKuaishouEticketSourceConfig({
enabled: payload.enabled !== false,
baseUrl: String(payload.baseUrl || '').trim() || 'https://s.kwaixiaodian.com',
shops: Array.isArray(payload.shops) ? payload.shops : [],
})
return {
filePath: getKuaishouEticketSourceFilePath(),
source: mapAdminKuaishouEticketSourceConfig(saved, listKuaishouEticketShopConfigs(saved)),
}
}
export async function queryAdminKuaishouEticketDetail(payload: JsonObject = {}) {
const context = resolveAdminKuaishouEticketContext(payload)
return queryKuaishouEticketConsumeDetail({
baseUrl: context.baseUrl,
cookie: context.cookie,
eTicketId: String(payload.eTicketId || '').trim(),
})
}
export async function queryAdminKuaishouEticketShopInfo(payload: JsonObject = {}) {
const context = resolveAdminKuaishouEticketContext(payload)
return queryKuaishouEticketStableInfo({
baseUrl: context.baseUrl,
cookie: context.cookie,
})
}
export async function consumeAdminKuaishouEticket(payload: JsonObject = {}) {
const context = resolveAdminKuaishouEticketContext(payload)
return consumeKuaishouEticket({
baseUrl: context.baseUrl,
cookie: context.cookie,
eTicketId: String(payload.eTicketId || '').trim(),
oid: String(payload.oid || '').trim(),
formToken: String(payload.formToken || '').trim(),
num: payload.num,
storeId: String(payload.storeId || '').trim(),
})
}
@@ -17,18 +17,12 @@ import {
backCloudtentaclesVirtualNumber,
getCloudtentaclesBindInfo,
} from '../../platforms/cloudtentacles/virtual-number-service.js'
import { consumeKuaishouEticket } from '../../platforms/kuaishou-eticket/consume-service.js'
import { isKuaishouEticketMockTicketCode } from '../../platforms/kuaishou-eticket/mock-ticket-service.js'
import { resendKuaishouIndustryVoucherSendCallback } from '../../platforms/kuaishou-industry/send-code-service.js'
import { consumeKuaishouIndustryVoucher } from '../../platforms/kuaishou-industry/voucher-service.js'
import {
attachKuaishouIndustryVoucherToTask,
bindKuaishouIndustryVouchersToOrderTasks,
} from '../../platforms/kuaishou-industry/voucher-binding-service.js'
import {
getKuaishouEticketSourceConfig,
resolveKuaishouEticketShopConfig,
} from '../../platforms/kuaishou-eticket/source-config-service.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS } from '../../../domain/task-status.js'
@@ -58,7 +52,6 @@ import type {
AdminViewerSessionInput,
} from '../../../types/admin/read-inputs.js'
import type {
AdminTaskKuaishouCloudDispatchInput,
AdminTaskKuaishouIndustryConsumeInput,
} from '../../../types/admin/write-inputs.js'
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
@@ -246,7 +239,6 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(
export async function dispatchAdminTaskKuaishouCloudFulfillment(
taskId: AdminEntityIdInput,
payload: AdminTaskKuaishouCloudDispatchInput = {},
session: AdminViewerSessionInput | null = null,
): Promise<AdminTaskActionResponse> {
const task = await getRequiredTask(taskId)
@@ -281,11 +273,15 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
})
}
const ticketCode = String(payload.ticketCode || '').trim()
const persistedTicketCode = String(flow.ticket.code || '').trim()
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
const industryVoucherCode = String(
voucherContext.voucherCode || voucherContext.eticketId || '',
).trim()
const resolvedTicketCode = persistedTicketCode || industryVoucherCode
if (!persistedTicketCode && !ticketCode) {
throw createHttpError('客户还没有在领取页提交核销码,暂时不能直接发货', {
if (!resolvedTicketCode) {
throw createHttpError('旧快手小店核销流程已停用,请改用行业电子凭证处理', {
statusCode: 409,
errorCode: 'admin_task_kuaishou_cloud_missing_ticket_code',
})
@@ -322,16 +318,9 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
...syncedFlow,
ticket: {
...syncedFlow.ticket,
code: ticketCode || persistedTicketCode,
capturedAt: ticketCode ? now : syncedFlow.ticket.capturedAt,
capturedBy:
ticketCode && session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: syncedFlow.ticket.capturedBy,
code: resolvedTicketCode,
capturedAt: resolvedTicketCode ? (syncedFlow.ticket.capturedAt || now) : syncedFlow.ticket.capturedAt,
capturedBy: syncedFlow.ticket.capturedBy,
},
dispatch: {
...syncedFlow.dispatch,
@@ -366,7 +355,7 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
task.id,
'kuaishou_cloud_dispatched',
{
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
ticketCodeMasked: maskCode(resolvedTicketCode),
skuId: syncedFlow.binding.skuId,
vnId: syncedFlow.binding.vnId,
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
@@ -548,77 +537,18 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
id: flow.binding.vnId,
})
const order = await getOrderById(task.order_id)
const ticketCode = String(flow.ticket.code || '').trim()
const shopId = String(flow.consume.shopId || order?.shop_id || '').trim()
const shopName = String(flow.consume.shopName || order?.shop_name || '').trim()
const mockTicketCode = isKuaishouEticketMockTicketCode(ticketCode)
const eticketSource = getKuaishouEticketSourceConfig()
const shopConfig = resolveKuaishouEticketShopConfig({
shopId,
shopName,
})
let consumeStatus = 'pending'
let consumeErrorMessage = ''
let consumedAt = null
let nextTaskStatus = 'completed'
let nextResultCode = 'kuaishou_cloud_completed'
let nextResultMessage = 'cloudtentacles 发货、退号并完成快手核销'
const consumeAlreadyCompleted = flow.consume.status === 'success'
if (consumeAlreadyCompleted) {
consumeStatus = 'success'
consumedAt = flow.consume.consumedAt || now
} else if (!order) {
consumeStatus = 'failed'
consumeErrorMessage = '任务关联订单不存在,无法执行快手核销'
} else if (!ticketCode) {
consumeStatus = 'failed'
consumeErrorMessage = '客户未提交有效核销码,无法执行快手核销'
} else if (mockTicketCode) {
const consumeResult = await consumeKuaishouEticket({
eTicketId: ticketCode,
oid: String(flow.ticket.oid || '').trim(),
formToken: String(flow.ticket.formToken || '').trim(),
})
consumeStatus = consumeResult.consumed ? 'success' : 'failed'
consumedAt = consumeResult.consumed ? now : null
consumeErrorMessage = String(consumeResult.errorMessage || '').trim()
} else if (
!shopConfig ||
shopConfig.enabled === false ||
!String(shopConfig.cookie || '').trim()
) {
consumeStatus = 'failed'
consumeErrorMessage = '订单对应快手小店缺少可用 Cookie,无法执行快手核销'
} else {
try {
const consumeResult = await consumeKuaishouEticket({
baseUrl: eticketSource.baseUrl,
cookie: shopConfig.cookie,
eTicketId: ticketCode,
oid: String(flow.ticket.oid || '').trim(),
formToken: String(flow.ticket.formToken || '').trim(),
})
if (consumeResult.consumed) {
consumeStatus = 'success'
consumedAt = now
} else {
consumeStatus = 'failed'
consumeErrorMessage = String(consumeResult.errorMessage || '快手核销失败').trim()
}
} catch (error) {
consumeStatus = 'failed'
consumeErrorMessage = error instanceof Error ? error.message : '快手核销失败'
}
}
if (consumeStatus !== 'success') {
nextTaskStatus = 'manual_review'
nextResultCode = 'kuaishou_cloud_consume_failed'
nextResultMessage = consumeErrorMessage || '号码已退还,但快手核销未完成,请人工处理'
}
const consumeStatus = consumeAlreadyCompleted ? 'success' : 'skipped'
let consumeErrorMessage = ''
const consumedAt = consumeAlreadyCompleted ? flow.consume.consumedAt || now : null
const nextTaskStatus = 'completed'
const nextResultCode = consumeAlreadyCompleted
? 'kuaishou_cloud_completed'
: 'kuaishou_cloud_completed_without_eticket_consume'
const nextResultMessage = consumeAlreadyCompleted
? 'cloudtentacles 发货、退号并完成电子凭证核销'
: 'cloudtentacles 发货、退号并收口'
const nextContext = {
...taskContext,
@@ -639,8 +569,8 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
consume: {
...flow.consume,
status: consumeStatus,
shopId: shopId || flow.consume.shopId,
shopName,
shopId: flow.consume.shopId,
shopName: flow.consume.shopName,
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
consumedAt,
errorMessage: consumeErrorMessage,
@@ -653,7 +583,7 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
delivery_status: 'delivered',
result_code: nextResultCode,
result_message: nextResultMessage,
redeemed_at: consumeStatus === 'success' ? now : task.redeemed_at,
redeemed_at: now,
last_error: consumeErrorMessage,
context_json: JSON.stringify(nextContext),
updated_at: now,
@@ -669,18 +599,19 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
now,
)
const consumeEventType = consumeAlreadyCompleted
? 'kuaishou_cloud_consume_already_completed'
: 'kuaishou_cloud_consume_skipped'
await createTaskEvent(
task.id,
consumeAlreadyCompleted
? 'kuaishou_cloud_consume_already_completed'
: consumeStatus === 'success'
? 'kuaishou_cloud_consumed'
: 'kuaishou_cloud_consume_failed',
consumeEventType,
{
ticketCodeMasked: maskCode(ticketCode),
shopId,
shopName,
shopId: flow.consume.shopId,
shopName: flow.consume.shopName,
consumeStatus,
consumeMode: 'legacy_writeoff_disabled',
errorMessage: consumeErrorMessage,
},
now,
@@ -961,7 +892,7 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
}
const taskContext = parseTaskContext(task)
const voucherContext = 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)
@@ -1000,6 +931,12 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
})
}
function normalizeAdminTaskIndustryVoucherContext(value: unknown): Record<string, any> {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, any>
: {}
}
async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise<KuaishouIndustryVoucherRow[]> {
const normalizedOid = String(platformOrderId || '').trim()
if (!normalizedOid) {