移除了 khhao 平台相关
This commit is contained in:
@@ -78,13 +78,6 @@ module.exports = {
|
|||||||
timestampToleranceSeconds: 600,
|
timestampToleranceSeconds: 600,
|
||||||
cardsEncoding: 'aes-256-ecb-base64',
|
cardsEncoding: 'aes-256-ecb-base64',
|
||||||
},
|
},
|
||||||
khhao: {
|
|
||||||
baseUrl: 'https://admin.khhao.com',
|
|
||||||
timeoutMs: 5000,
|
|
||||||
loginPath: '/c/login/index.php',
|
|
||||||
captchaPath: '/verify_img.php',
|
|
||||||
orderListPath: '/c/payOrder/get.php',
|
|
||||||
},
|
|
||||||
cloudtentacles: {
|
cloudtentacles: {
|
||||||
baseUrl: 'https://123.207.217.176',
|
baseUrl: 'https://123.207.217.176',
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"baseUrl": "https://admin.khhao.com",
|
|
||||||
"username": "order_check",
|
|
||||||
"password": "order_check_test1",
|
|
||||||
"maxCaptchaAttempts": 3,
|
|
||||||
"autoSync": {
|
|
||||||
"enabled": false,
|
|
||||||
"intervalMinutes": 3,
|
|
||||||
"pages": 2,
|
|
||||||
"pageSize": 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
|
|
||||||
"lastRunStartedAt": "2026-05-11T14:09:54.989Z",
|
|
||||||
"lastRunFinishedAt": "2026-05-11T14:09:56.241Z",
|
|
||||||
"lastRunStatus": "success",
|
|
||||||
"lastErrorMessage": "",
|
|
||||||
"fetchedCount": 40,
|
|
||||||
"syncedCount": 0,
|
|
||||||
"ignoredCount": 40,
|
|
||||||
"lastOrderCreatedAt": "2026-05-11T14:09:50.000Z",
|
|
||||||
"watchMode": "normal",
|
|
||||||
"watchOrders": []
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,12 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"id": "khhao 订单取样导入",
|
"id": "91卡券订单取样导入",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"priority": 100,
|
"priority": 100,
|
||||||
"provider": "91kaquan",
|
"provider": "91kaquan",
|
||||||
"platform": "kuaishou",
|
"platform": "kuaishou",
|
||||||
"shopId": "91kaquan",
|
"shopId": "91kaquan",
|
||||||
"khhaoShopId": "",
|
|
||||||
"internalSkuCode": "套装-浪漫天命",
|
"internalSkuCode": "套装-浪漫天命",
|
||||||
"internalSkuName": "套装-浪漫天命",
|
"internalSkuName": "套装-浪漫天命",
|
||||||
"externalSkuCode": "183999074510936",
|
"externalSkuCode": "183999074510936",
|
||||||
@@ -26,32 +25,6 @@
|
|||||||
"kuaishouConsumeShopId": "3676797936",
|
"kuaishouConsumeShopId": "3676797936",
|
||||||
"kuaishouConsumeShopName": "小鱼儿游戏服务店",
|
"kuaishouConsumeShopName": "小鱼儿游戏服务店",
|
||||||
"notes": "从 91卡券订单 2613300010204561 导入"
|
"notes": "从 91卡券订单 2613300010204561 导入"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "套装-浪漫天命",
|
|
||||||
"enabled": true,
|
|
||||||
"priority": 100,
|
|
||||||
"provider": "khhao",
|
|
||||||
"platform": "kuaishou",
|
|
||||||
"shopId": "4269276762",
|
|
||||||
"khhaoShopId": "10",
|
|
||||||
"internalSkuCode": "套装-浪漫天命",
|
|
||||||
"internalSkuName": "套装-浪漫天命",
|
|
||||||
"externalSkuCode": "952",
|
|
||||||
"externalItemId": "952",
|
|
||||||
"externalSkuName": "测试2",
|
|
||||||
"resolvedSkuName": "测试2",
|
|
||||||
"cloudSourceKey": "default",
|
|
||||||
"cloudSkuId": 28,
|
|
||||||
"cloudSkuName": "套装-浪漫天命",
|
|
||||||
"vnKey": "1",
|
|
||||||
"autoBuyEnabled": true,
|
|
||||||
"minAssetReserve": 0,
|
|
||||||
"autoReturnNumberAfterDispatch": true,
|
|
||||||
"autoConsumeAfterDispatch": true,
|
|
||||||
"kuaishouConsumeShopId": "4269276762",
|
|
||||||
"kuaishouConsumeShopName": "稚嫩游戏交易店",
|
|
||||||
"notes": "从 khhao 订单 2612400002308936 导入"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"platform": "xianyu",
|
"platform": "xianyu",
|
||||||
"shopId": "2209880145223",
|
"shopId": "2209880145223",
|
||||||
"shopName": "",
|
"shopName": "",
|
||||||
"khhaoShopId": "",
|
|
||||||
"skuCode": "海底捞第五人格皮肤",
|
"skuCode": "海底捞第五人格皮肤",
|
||||||
"skuName": "海底捞第五人格皮肤",
|
"skuName": "海底捞第五人格皮肤",
|
||||||
"profileKey": "tencent_claim_redeem",
|
"profileKey": "tencent_claim_redeem",
|
||||||
@@ -25,7 +24,6 @@
|
|||||||
"platform": "xianyu",
|
"platform": "xianyu",
|
||||||
"shopId": "2209880145223",
|
"shopId": "2209880145223",
|
||||||
"shopName": "",
|
"shopName": "",
|
||||||
"khhaoShopId": "",
|
|
||||||
"skuCode": "三角洲海底捞动作",
|
"skuCode": "三角洲海底捞动作",
|
||||||
"skuName": "海底捞干员庆生动作1个",
|
"skuName": "海底捞干员庆生动作1个",
|
||||||
"profileKey": "tencent_claim_redeem",
|
"profileKey": "tencent_claim_redeem",
|
||||||
|
|||||||
@@ -270,31 +270,6 @@ function applyEnvOverrides(baseConfig) {
|
|||||||
nextConfig.platforms.ninetyone.cardsEncoding = ninetyoneCardsEncoding
|
nextConfig.platforms.ninetyone.cardsEncoding = ninetyoneCardsEncoding
|
||||||
}
|
}
|
||||||
|
|
||||||
const khhaoBaseUrl = String(process.env.KHHAO_BASE_URL || '').trim()
|
|
||||||
if (khhaoBaseUrl) {
|
|
||||||
nextConfig.platforms.khhao.baseUrl = khhaoBaseUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
const khhaoTimeoutMs = parseInteger(process.env.KHHAO_TIMEOUT_MS)
|
|
||||||
if (khhaoTimeoutMs !== null) {
|
|
||||||
nextConfig.platforms.khhao.timeoutMs = khhaoTimeoutMs
|
|
||||||
}
|
|
||||||
|
|
||||||
const khhaoLoginPath = String(process.env.KHHAO_LOGIN_PATH || '').trim()
|
|
||||||
if (khhaoLoginPath) {
|
|
||||||
nextConfig.platforms.khhao.loginPath = khhaoLoginPath
|
|
||||||
}
|
|
||||||
|
|
||||||
const khhaoCaptchaPath = String(process.env.KHHAO_CAPTCHA_PATH || '').trim()
|
|
||||||
if (khhaoCaptchaPath) {
|
|
||||||
nextConfig.platforms.khhao.captchaPath = khhaoCaptchaPath
|
|
||||||
}
|
|
||||||
|
|
||||||
const khhaoOrderListPath = String(process.env.KHHAO_ORDER_LIST_PATH || '').trim()
|
|
||||||
if (khhaoOrderListPath) {
|
|
||||||
nextConfig.platforms.khhao.orderListPath = khhaoOrderListPath
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudtentaclesBaseUrl = String(process.env.CLOUDTENTACLES_BASE_URL || '').trim()
|
const cloudtentaclesBaseUrl = String(process.env.CLOUDTENTACLES_BASE_URL || '').trim()
|
||||||
if (cloudtentaclesBaseUrl) {
|
if (cloudtentaclesBaseUrl) {
|
||||||
nextConfig.platforms.cloudtentacles.baseUrl = cloudtentaclesBaseUrl
|
nextConfig.platforms.cloudtentacles.baseUrl = cloudtentaclesBaseUrl
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import open91Router from './routes/open-91.js'
|
|||||||
import webhooksRouter from './routes/webhooks.js'
|
import webhooksRouter from './routes/webhooks.js'
|
||||||
import { ensureAdminUsersBootstrapped } from './services/admin/admin-auth-service.js'
|
import { ensureAdminUsersBootstrapped } from './services/admin/admin-auth-service.js'
|
||||||
import { ensureFulfillmentCatalogBootstrapped } from './services/bootstrap/fulfillment-bootstrap-service.js'
|
import { ensureFulfillmentCatalogBootstrapped } from './services/bootstrap/fulfillment-bootstrap-service.js'
|
||||||
import { startKhhaoAutoSyncLoop, stopKhhaoAutoSyncLoop } from './services/platforms/khhao/auto-sync-service.js'
|
|
||||||
import { closeLocalOcrWorker, warmupLocalOcrWorker } from './services/session/ocr.js'
|
import { closeLocalOcrWorker, warmupLocalOcrWorker } from './services/session/ocr.js'
|
||||||
import { closeAllTencentBrowserSessions, warmupTencentBrowser } from './services/session/session.js'
|
import { closeAllTencentBrowserSessions, warmupTencentBrowser } from './services/session/session.js'
|
||||||
import { buildSuccessPayload } from './utils/http.js'
|
import { buildSuccessPayload } from './utils/http.js'
|
||||||
@@ -163,7 +162,6 @@ async function bootstrapCoreServices() {
|
|||||||
attempt: startupState.core.attemptCount,
|
attempt: startupState.core.attemptCount,
|
||||||
})
|
})
|
||||||
|
|
||||||
startKhhaoAutoSyncLoop()
|
|
||||||
void bootstrapBrowser()
|
void bootstrapBrowser()
|
||||||
void bootstrapOcr()
|
void bootstrapOcr()
|
||||||
break
|
break
|
||||||
@@ -372,12 +370,6 @@ async function shutdown(signal) {
|
|||||||
logError('[shutdown]', 'failed to close local OCR worker', error)
|
logError('[shutdown]', 'failed to close local OCR worker', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
stopKhhaoAutoSyncLoop()
|
|
||||||
} catch (error) {
|
|
||||||
logError('[shutdown]', 'failed to stop khhao auto sync loop', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!server.listening) {
|
if (!server.listening) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
useAdminCloudtentaclesSku,
|
useAdminCloudtentaclesSku,
|
||||||
getAdminAgisoShopConfigs,
|
getAdminAgisoShopConfigs,
|
||||||
getAdminFulfillmentBindingConfigs,
|
getAdminFulfillmentBindingConfigs,
|
||||||
getAdminKhhaoSourceConfig,
|
|
||||||
getAdminNinetyoneOrders,
|
getAdminNinetyoneOrders,
|
||||||
consumeAdminKuaishouEticket,
|
consumeAdminKuaishouEticket,
|
||||||
failAdminNinetyoneOrder,
|
failAdminNinetyoneOrder,
|
||||||
@@ -27,18 +26,14 @@ import {
|
|||||||
lookupAdminFulfillmentBindingOrder,
|
lookupAdminFulfillmentBindingOrder,
|
||||||
queryAdminKuaishouEticketShopInfo,
|
queryAdminKuaishouEticketShopInfo,
|
||||||
queryAdminKuaishouEticketDetail,
|
queryAdminKuaishouEticketDetail,
|
||||||
queryAdminKhhaoOrders,
|
|
||||||
retryAdminNinetyoneOrder,
|
retryAdminNinetyoneOrder,
|
||||||
runAdminCloudtentaclesFullFlow,
|
runAdminCloudtentaclesFullFlow,
|
||||||
sendAdminCloudtentaclesSmsCode,
|
sendAdminCloudtentaclesSmsCode,
|
||||||
syncAdminKhhaoOrders,
|
|
||||||
testAdminCloudtentaclesLogin,
|
testAdminCloudtentaclesLogin,
|
||||||
testAdminKhhaoLogin,
|
|
||||||
updateAdminKuaishouCloudFulfillmentConfig,
|
updateAdminKuaishouCloudFulfillmentConfig,
|
||||||
updateAdminKuaishouEticketSourceConfig,
|
updateAdminKuaishouEticketSourceConfig,
|
||||||
updateAdminCloudtentaclesSourceConfig,
|
updateAdminCloudtentaclesSourceConfig,
|
||||||
updateAdminAgisoShopConfigs,
|
updateAdminAgisoShopConfigs,
|
||||||
updateAdminKhhaoSourceConfig,
|
|
||||||
updateAdminFulfillmentBindingConfigs,
|
updateAdminFulfillmentBindingConfigs,
|
||||||
verifyAdminCloudtentaclesLoginCode,
|
verifyAdminCloudtentaclesLoginCode,
|
||||||
validateAdminCloudtentaclesSession,
|
validateAdminCloudtentaclesSession,
|
||||||
@@ -48,9 +43,6 @@ import { createJsonHandler, requireAdminRoles } from './shared.js'
|
|||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminAgisoShopConfigRouteBody} AdminAgisoShopConfigRouteBody */
|
/** @typedef {import('../../types/admin-route-inputs.js').AdminAgisoShopConfigRouteBody} AdminAgisoShopConfigRouteBody */
|
||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminFulfillmentBindingConfigRouteBody} AdminFulfillmentBindingConfigRouteBody */
|
/** @typedef {import('../../types/admin-route-inputs.js').AdminFulfillmentBindingConfigRouteBody} AdminFulfillmentBindingConfigRouteBody */
|
||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminFulfillmentBindingLookupRouteBody} AdminFulfillmentBindingLookupRouteBody */
|
/** @typedef {import('../../types/admin-route-inputs.js').AdminFulfillmentBindingLookupRouteBody} AdminFulfillmentBindingLookupRouteBody */
|
||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoOrderQueryRouteBody} AdminKhhaoOrderQueryRouteBody */
|
|
||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoSourceConfigRouteBody} AdminKhhaoSourceConfigRouteBody */
|
|
||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoTestLoginRouteBody} AdminKhhaoTestLoginRouteBody */
|
|
||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminEntityRouteParams} AdminEntityRouteParams */
|
/** @typedef {import('../../types/admin-route-inputs.js').AdminEntityRouteParams} AdminEntityRouteParams */
|
||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketSourceConfigRouteBody} AdminKuaishouEticketSourceConfigRouteBody */
|
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketSourceConfigRouteBody} AdminKuaishouEticketSourceConfigRouteBody */
|
||||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketDetailQueryRouteBody} AdminKuaishouEticketDetailQueryRouteBody */
|
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketDetailQueryRouteBody} AdminKuaishouEticketDetailQueryRouteBody */
|
||||||
@@ -133,111 +125,6 @@ router.post('/platform-config/kuaishou-eticket-source', createJsonHandler(
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
|
||||||
router.get('/platform-config/khhao-source', createJsonHandler(
|
|
||||||
() => getAdminKhhaoSourceConfig(),
|
|
||||||
{
|
|
||||||
successMessage: 'ok',
|
|
||||||
errorMessage: '读取 khhao 来源配置失败',
|
|
||||||
scope: '[admin/platform-config/khhao-source]',
|
|
||||||
},
|
|
||||||
))
|
|
||||||
|
|
||||||
router.post('/platform-config/khhao-source', createJsonHandler(
|
|
||||||
(req) => updateAdminKhhaoSourceConfig(/** @type {AdminKhhaoSourceConfigRouteBody} */ (req.body)),
|
|
||||||
{
|
|
||||||
successMessage: 'khhao 来源配置已保存',
|
|
||||||
errorMessage: '保存 khhao 来源配置失败',
|
|
||||||
scope: '[admin/platform-config/khhao-source]',
|
|
||||||
audit: (_req, data) => {
|
|
||||||
const result = /** @type {{ filePath?: string, source?: { username?: string, enabled?: boolean } }} */ (data)
|
|
||||||
return {
|
|
||||||
action: 'platform_khhao_source_updated',
|
|
||||||
targetType: 'platform_config',
|
|
||||||
targetId: 'khhao_source',
|
|
||||||
data: {
|
|
||||||
filePath: String(result.filePath || '').trim(),
|
|
||||||
username: String(result.source?.username || '').trim(),
|
|
||||||
enabled: Boolean(result.source?.enabled),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
))
|
|
||||||
|
|
||||||
router.post('/platform-config/khhao/test-login', createJsonHandler(
|
|
||||||
(req) => testAdminKhhaoLogin(/** @type {AdminKhhaoTestLoginRouteBody} */ (req.body)),
|
|
||||||
{
|
|
||||||
successMessage: 'khhao 登录测试成功',
|
|
||||||
errorMessage: 'khhao 登录测试失败',
|
|
||||||
scope: '[admin/platform-config/khhao/test-login]',
|
|
||||||
audit: (req, data) => {
|
|
||||||
const body = /** @type {AdminKhhaoTestLoginRouteBody} */ (req.body)
|
|
||||||
const result = /** @type {{ baseUrl?: string, username?: string, attempt?: number, session?: { cookieCount?: number } }} */ (data)
|
|
||||||
return {
|
|
||||||
action: 'platform_khhao_test_login',
|
|
||||||
targetType: 'platform_config',
|
|
||||||
targetId: String(result.username || body.username || '').trim() || 'khhao',
|
|
||||||
data: {
|
|
||||||
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
|
|
||||||
attempt: Number(result.attempt || 0),
|
|
||||||
cookieCount: Number(result.session?.cookieCount || 0),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
))
|
|
||||||
|
|
||||||
router.post('/platform-config/khhao/query-orders', createJsonHandler(
|
|
||||||
(req) => queryAdminKhhaoOrders(/** @type {AdminKhhaoOrderQueryRouteBody} */ (req.body)),
|
|
||||||
{
|
|
||||||
successMessage: 'khhao 订单查询成功',
|
|
||||||
errorMessage: 'khhao 订单查询失败',
|
|
||||||
scope: '[admin/platform-config/khhao/query-orders]',
|
|
||||||
audit: (req, data) => {
|
|
||||||
const body = /** @type {AdminKhhaoOrderQueryRouteBody} */ (req.body)
|
|
||||||
const result = /** @type {{ page?: number, limit?: number, itemCount?: number, total?: number, baseUrl?: string }} */ (data)
|
|
||||||
return {
|
|
||||||
action: 'platform_khhao_query_orders',
|
|
||||||
targetType: 'platform_config',
|
|
||||||
targetId: String(body.username || '').trim() || 'khhao',
|
|
||||||
data: {
|
|
||||||
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
|
|
||||||
page: Number(result.page || 1),
|
|
||||||
limit: Number(result.limit || 50),
|
|
||||||
itemCount: Number(result.itemCount || 0),
|
|
||||||
total: Number(result.total || 0),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
))
|
|
||||||
|
|
||||||
router.post('/platform-config/khhao/sync-orders', createJsonHandler(
|
|
||||||
(req) => syncAdminKhhaoOrders(/** @type {AdminKhhaoOrderQueryRouteBody} */ (req.body)),
|
|
||||||
{
|
|
||||||
successMessage: 'khhao 订单同步成功',
|
|
||||||
errorMessage: 'khhao 订单同步失败',
|
|
||||||
scope: '[admin/platform-config/khhao/sync-orders]',
|
|
||||||
audit: (req, data) => {
|
|
||||||
const body = /** @type {AdminKhhaoOrderQueryRouteBody} */ (req.body)
|
|
||||||
const result = /** @type {{ page?: number, limit?: number, fetchedCount?: number, syncedCount?: number, ignoredCount?: number, baseUrl?: string }} */ (data)
|
|
||||||
return {
|
|
||||||
action: 'platform_khhao_sync_orders',
|
|
||||||
targetType: 'platform_config',
|
|
||||||
targetId: String(body.username || '').trim() || 'khhao',
|
|
||||||
data: {
|
|
||||||
baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
|
|
||||||
page: Number(result.page || 1),
|
|
||||||
limit: Number(result.limit || 50),
|
|
||||||
fetchedCount: Number(result.fetchedCount || 0),
|
|
||||||
syncedCount: Number(result.syncedCount || 0),
|
|
||||||
ignoredCount: Number(result.ignoredCount || 0),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
))
|
|
||||||
|
|
||||||
router.get('/platform-config/ninetyone/orders', createJsonHandler(
|
router.get('/platform-config/ninetyone/orders', createJsonHandler(
|
||||||
(req) => getAdminNinetyoneOrders(req.query),
|
(req) => getAdminNinetyoneOrders(req.query),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export function mapAdminFulfillmentBindingConfigItem(item) {
|
|||||||
platform: String(item?.platform || '').trim(),
|
platform: String(item?.platform || '').trim(),
|
||||||
shopId: String(item?.shopId || '').trim(),
|
shopId: String(item?.shopId || '').trim(),
|
||||||
shopName: String(item?.shopName || '').trim(),
|
shopName: String(item?.shopName || '').trim(),
|
||||||
khhaoShopId: String(item?.khhaoShopId || '').trim(),
|
|
||||||
skuCode: String(item?.skuCode || '').trim(),
|
skuCode: String(item?.skuCode || '').trim(),
|
||||||
skuName: String(item?.skuName || '').trim(),
|
skuName: String(item?.skuName || '').trim(),
|
||||||
profileKey: String(item?.profileKey || '').trim(),
|
profileKey: String(item?.profileKey || '').trim(),
|
||||||
@@ -66,19 +65,6 @@ export function mapAdminFulfillmentBindingConfigItem(item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveFulfillmentBindingMatchShopId({
|
|
||||||
provider = '',
|
|
||||||
platform = '',
|
|
||||||
shopId = '',
|
|
||||||
khhaoShopId = '',
|
|
||||||
} = {}) {
|
|
||||||
if (String(provider || '').trim() === 'khhao' && String(platform || '').trim() === 'kuaishou') {
|
|
||||||
return String(khhaoShopId || shopId || '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(shopId || '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function matchesObservedProduct(binding, observed) {
|
export function matchesObservedProduct(binding, observed) {
|
||||||
const provider = String(binding?.provider || '').trim()
|
const provider = String(binding?.provider || '').trim()
|
||||||
const platform = String(binding?.platform || '').trim()
|
const platform = String(binding?.platform || '').trim()
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
mapAdminFulfillmentBindingConfigItem,
|
mapAdminFulfillmentBindingConfigItem,
|
||||||
mapAdminObservedProductItem,
|
mapAdminObservedProductItem,
|
||||||
matchesObservedProduct,
|
matchesObservedProduct,
|
||||||
resolveFulfillmentBindingMatchShopId,
|
|
||||||
} from './domain.js'
|
} from './domain.js'
|
||||||
|
|
||||||
test('mapAdminAgisoMessagingDefaults normalizes escaped newlines', () => {
|
test('mapAdminAgisoMessagingDefaults normalizes escaped newlines', () => {
|
||||||
@@ -78,7 +77,6 @@ test('mapAdminFulfillmentBindingConfigItem normalizes binding shape', () => {
|
|||||||
platform: 'xianyu',
|
platform: 'xianyu',
|
||||||
shopId: 'shop-1',
|
shopId: 'shop-1',
|
||||||
shopName: '',
|
shopName: '',
|
||||||
khhaoShopId: '',
|
|
||||||
skuCode: 'sku-1',
|
skuCode: 'sku-1',
|
||||||
skuName: '',
|
skuName: '',
|
||||||
profileKey: '',
|
profileKey: '',
|
||||||
@@ -95,28 +93,6 @@ test('mapAdminFulfillmentBindingConfigItem normalizes binding shape', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resolveFulfillmentBindingMatchShopId prefers khhaoShopId for khhao kuaishou bindings', () => {
|
|
||||||
assert.equal(
|
|
||||||
resolveFulfillmentBindingMatchShopId({
|
|
||||||
provider: 'khhao',
|
|
||||||
platform: 'kuaishou',
|
|
||||||
shopId: 'shop-a',
|
|
||||||
khhaoShopId: 'khhao-1',
|
|
||||||
}),
|
|
||||||
'khhao-1',
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
resolveFulfillmentBindingMatchShopId({
|
|
||||||
provider: 'agiso',
|
|
||||||
platform: 'xianyu',
|
|
||||||
shopId: 'shop-a',
|
|
||||||
khhaoShopId: 'khhao-1',
|
|
||||||
}),
|
|
||||||
'shop-a',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('matchesObservedProduct honors provider platform shop and external fields', () => {
|
test('matchesObservedProduct honors provider platform shop and external fields', () => {
|
||||||
const binding = {
|
const binding = {
|
||||||
provider: 'agiso',
|
provider: 'agiso',
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
saveOrderFulfillmentBindingConfigs,
|
saveOrderFulfillmentBindingConfigs,
|
||||||
} from '../../order/fulfillment-binding-config-service.js'
|
} from '../../order/fulfillment-binding-config-service.js'
|
||||||
import { enrichAgisoXianyuTradeOrder } from '../../platforms/agiso/xianyu/order-detail-service.js'
|
import { enrichAgisoXianyuTradeOrder } from '../../platforms/agiso/xianyu/order-detail-service.js'
|
||||||
import { getKuaishouEticketSourceConfig, resolveKuaishouEticketShopConfig } from '../../platforms/kuaishou-eticket/source-config-service.js'
|
|
||||||
import { syncConfiguredFulfillmentBindings } from '../../bootstrap/fulfillment-bootstrap-service.js'
|
import { syncConfiguredFulfillmentBindings } from '../../bootstrap/fulfillment-bootstrap-service.js'
|
||||||
import { getFulfillmentProfileByKey } from '../../../repositories/fulfillment-profile-repo.js'
|
import { getFulfillmentProfileByKey } from '../../../repositories/fulfillment-profile-repo.js'
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
@@ -78,8 +77,6 @@ export async function updateAdminFulfillmentBindingConfigs(
|
|||||||
) {
|
) {
|
||||||
const bindingsInput = Array.isArray(payload.bindings) ? payload.bindings : []
|
const bindingsInput = Array.isArray(payload.bindings) ? payload.bindings : []
|
||||||
const normalizedBindings = await validateAdminFulfillmentBindingConfigs(bindingsInput, {
|
const normalizedBindings = await validateAdminFulfillmentBindingConfigs(bindingsInput, {
|
||||||
kuaishouEticketSource: getKuaishouEticketSourceConfig(),
|
|
||||||
resolveKuaishouEticketShopConfig,
|
|
||||||
getFulfillmentProfileByKey,
|
getFulfillmentProfileByKey,
|
||||||
})
|
})
|
||||||
assertLegacyBindingConfigsOnly(normalizedBindings)
|
assertLegacyBindingConfigsOnly(normalizedBindings)
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { queryKhhaoOrderList } from '../../platforms/khhao/order-query-service.js'
|
|
||||||
import { syncKhhaoOrders } from '../../platforms/khhao/order-sync-service.js'
|
|
||||||
import { getKhhaoSourceConfig, getKhhaoSourcesFilePath, saveKhhaoSourceConfig } from '../../platforms/khhao/source-config-service.js'
|
|
||||||
import { ensureKhhaoSyncBaseline, getKhhaoSyncState, getKhhaoSyncStateFilePath } from '../../platforms/khhao/sync-state-service.js'
|
|
||||||
import { loginKhhaoSession } from '../../platforms/khhao/session-service.js'
|
|
||||||
import {
|
|
||||||
buildAdminKhhaoLoginPayload,
|
|
||||||
buildAdminKhhaoLoginResult,
|
|
||||||
buildAdminKhhaoOrderQueryResult,
|
|
||||||
buildAdminKhhaoSyncPayload,
|
|
||||||
resolveAdminKhhaoCredentialPayload,
|
|
||||||
} from './khhao.js'
|
|
||||||
import { normalizeAdminKhhaoSourceConfigPayload } from './writes.js'
|
|
||||||
import {
|
|
||||||
mapAdminKhhaoSourceConfig,
|
|
||||||
mapAdminKhhaoSyncState,
|
|
||||||
} from './mappers.js'
|
|
||||||
|
|
||||||
/** @typedef {import('../../../types/admin-write-inputs.js').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigInput */
|
|
||||||
/** @typedef {import('../../../types/admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryInput */
|
|
||||||
/** @typedef {import('../../../types/admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginInput */
|
|
||||||
|
|
||||||
export function getAdminKhhaoSourceConfig() {
|
|
||||||
const config = getKhhaoSourceConfig()
|
|
||||||
const syncState = getKhhaoSyncState()
|
|
||||||
|
|
||||||
return {
|
|
||||||
filePath: getKhhaoSourcesFilePath(),
|
|
||||||
stateFilePath: getKhhaoSyncStateFilePath(),
|
|
||||||
source: mapAdminKhhaoSourceConfig(config),
|
|
||||||
syncState: mapAdminKhhaoSyncState(syncState),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {AdminKhhaoSourceConfigInput} [payload] */
|
|
||||||
export function updateAdminKhhaoSourceConfig(payload = /** @type {AdminKhhaoSourceConfigInput} */ ({})) {
|
|
||||||
const saved = saveKhhaoSourceConfig(normalizeAdminKhhaoSourceConfigPayload(payload))
|
|
||||||
const syncState = saved.autoSync?.enabled ? ensureKhhaoSyncBaseline() : getKhhaoSyncState()
|
|
||||||
|
|
||||||
return {
|
|
||||||
filePath: getKhhaoSourcesFilePath(),
|
|
||||||
stateFilePath: getKhhaoSyncStateFilePath(),
|
|
||||||
source: mapAdminKhhaoSourceConfig(saved),
|
|
||||||
syncState: mapAdminKhhaoSyncState(syncState),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {AdminKhhaoTestLoginInput} [payload] */
|
|
||||||
export async function testAdminKhhaoLogin(payload = /** @type {AdminKhhaoTestLoginInput} */ ({})) {
|
|
||||||
const session = await loginKhhaoSession(buildAdminKhhaoLoginPayload(payload))
|
|
||||||
return buildAdminKhhaoLoginResult(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {AdminKhhaoOrderQueryInput} [payload] */
|
|
||||||
export async function queryAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQueryInput} */ ({})) {
|
|
||||||
const credential = resolveAdminKhhaoCredentialPayload(payload)
|
|
||||||
const session = await loginKhhaoSession(buildAdminKhhaoLoginPayload(payload, {
|
|
||||||
requestIdScope: 'query-orders',
|
|
||||||
}))
|
|
||||||
|
|
||||||
const result = await queryKhhaoOrderList({
|
|
||||||
baseUrl: credential.baseUrl,
|
|
||||||
page: payload.page,
|
|
||||||
limit: payload.limit,
|
|
||||||
session,
|
|
||||||
})
|
|
||||||
|
|
||||||
return buildAdminKhhaoOrderQueryResult(session, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {AdminKhhaoOrderQueryInput} [payload] */
|
|
||||||
export async function syncAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQueryInput} */ ({})) {
|
|
||||||
return syncKhhaoOrders(buildAdminKhhaoSyncPayload(payload))
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { getKhhaoSourceConfig } from '../../platforms/khhao/source-config-service.js'
|
|
||||||
import { mapKhhaoOrderPreviewList } from '../../platforms/khhao/order-mapper-service.js'
|
|
||||||
import { maskSecret } from './mappers.js'
|
|
||||||
|
|
||||||
export function resolveAdminKhhaoCredentialPayload(
|
|
||||||
payload = {},
|
|
||||||
options = {},
|
|
||||||
) {
|
|
||||||
const savedSource = /** @type {Record<string, unknown>} */ (
|
|
||||||
options.savedSource || getKhhaoSourceConfig()
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: String(payload.baseUrl || savedSource.baseUrl || '').trim(),
|
|
||||||
username: String(payload.username || savedSource.username || '').trim(),
|
|
||||||
password: String(payload.password || savedSource.password || '').trim(),
|
|
||||||
maxCaptchaAttempts: payload.maxCaptchaAttempts || savedSource.maxCaptchaAttempts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildAdminKhhaoLoginPayload(
|
|
||||||
payload = {},
|
|
||||||
options = {},
|
|
||||||
) {
|
|
||||||
const credential = resolveAdminKhhaoCredentialPayload(payload, options)
|
|
||||||
const requestIdScope = String(options.requestIdScope || 'test-login').trim() || 'test-login'
|
|
||||||
|
|
||||||
return {
|
|
||||||
...credential,
|
|
||||||
includeImageBase64: payload.includeImageBase64,
|
|
||||||
requestId: `admin-khhao-${requestIdScope}:${credential.username || 'anonymous'}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildAdminKhhaoSyncPayload(
|
|
||||||
payload = {},
|
|
||||||
options = {},
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...payload,
|
|
||||||
...resolveAdminKhhaoCredentialPayload(payload, options),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildAdminKhhaoLoginResult(session = {}) {
|
|
||||||
const cookieKeys = Object.keys(session.cookieMap || {})
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: String(session.baseUrl || '').trim(),
|
|
||||||
username: String(session.username || '').trim(),
|
|
||||||
loggedInAt: String(session.loggedInAt || '').trim(),
|
|
||||||
attempt: Number(session.attempt || 0),
|
|
||||||
responseMessage: String(session.responseMessage || '').trim(),
|
|
||||||
captcha: {
|
|
||||||
recognizedText: String(session.captchaText || '').trim(),
|
|
||||||
imageBase64: String(session.captchaImageBase64 || '').trim(),
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
cookieKeys,
|
|
||||||
cookieCount: cookieKeys.length,
|
|
||||||
cookieHeaderMasked: maskSecret(session.cookieHeader),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildAdminKhhaoOrderQueryResult(
|
|
||||||
session = {},
|
|
||||||
result = {},
|
|
||||||
) {
|
|
||||||
const items = Array.isArray(result.items) ? result.items : []
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: String(session.baseUrl || '').trim(),
|
|
||||||
page: Number(result.page || 1),
|
|
||||||
limit: Number(result.limit || 0),
|
|
||||||
total: Number(result.total || 0),
|
|
||||||
itemCount: items.length,
|
|
||||||
items: mapKhhaoOrderPreviewList(items),
|
|
||||||
rawItems: items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import test from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
|
||||||
|
|
||||||
import {
|
|
||||||
buildAdminKhhaoLoginPayload,
|
|
||||||
buildAdminKhhaoLoginResult,
|
|
||||||
buildAdminKhhaoOrderQueryResult,
|
|
||||||
buildAdminKhhaoSyncPayload,
|
|
||||||
resolveAdminKhhaoCredentialPayload,
|
|
||||||
} from './khhao.js'
|
|
||||||
|
|
||||||
test('resolveAdminKhhaoCredentialPayload merges payload and saved source defaults', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
resolveAdminKhhaoCredentialPayload(
|
|
||||||
{
|
|
||||||
baseUrl: ' https://override.example.com ',
|
|
||||||
maxCaptchaAttempts: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
savedSource: {
|
|
||||||
baseUrl: 'https://saved.example.com',
|
|
||||||
username: 'saved-user',
|
|
||||||
password: 'saved-pass',
|
|
||||||
maxCaptchaAttempts: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
{
|
|
||||||
baseUrl: 'https://override.example.com',
|
|
||||||
username: 'saved-user',
|
|
||||||
password: 'saved-pass',
|
|
||||||
maxCaptchaAttempts: 5,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('buildAdminKhhaoLoginPayload appends request id and includeImageBase64', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
buildAdminKhhaoLoginPayload(
|
|
||||||
{
|
|
||||||
username: ' user ',
|
|
||||||
includeImageBase64: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
requestIdScope: 'query-orders',
|
|
||||||
savedSource: {
|
|
||||||
baseUrl: 'https://saved.example.com',
|
|
||||||
password: 'saved-pass',
|
|
||||||
maxCaptchaAttempts: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
{
|
|
||||||
baseUrl: 'https://saved.example.com',
|
|
||||||
username: 'user',
|
|
||||||
password: 'saved-pass',
|
|
||||||
maxCaptchaAttempts: 3,
|
|
||||||
includeImageBase64: true,
|
|
||||||
requestId: 'admin-khhao-query-orders:user',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('buildAdminKhhaoSyncPayload merges normalized credential fields into payload', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
buildAdminKhhaoSyncPayload(
|
|
||||||
{
|
|
||||||
page: 2,
|
|
||||||
limit: 20,
|
|
||||||
username: ' user ',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
savedSource: {
|
|
||||||
baseUrl: 'https://saved.example.com',
|
|
||||||
password: 'saved-pass',
|
|
||||||
maxCaptchaAttempts: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
{
|
|
||||||
page: 2,
|
|
||||||
limit: 20,
|
|
||||||
username: 'user',
|
|
||||||
baseUrl: 'https://saved.example.com',
|
|
||||||
password: 'saved-pass',
|
|
||||||
maxCaptchaAttempts: 4,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('buildAdminKhhaoLoginResult assembles login response payload', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
buildAdminKhhaoLoginResult({
|
|
||||||
baseUrl: ' https://admin.khhao.com ',
|
|
||||||
username: ' test-user ',
|
|
||||||
loggedInAt: ' 2026-05-04T12:00:00.000Z ',
|
|
||||||
attempt: 2,
|
|
||||||
responseMessage: ' success ',
|
|
||||||
captchaText: ' abcd ',
|
|
||||||
captchaImageBase64: ' image-data ',
|
|
||||||
cookieMap: {
|
|
||||||
PHPSESSID: 'a',
|
|
||||||
foo: 'b',
|
|
||||||
},
|
|
||||||
cookieHeader: 'abcdef1234567890',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
baseUrl: 'https://admin.khhao.com',
|
|
||||||
username: 'test-user',
|
|
||||||
loggedInAt: '2026-05-04T12:00:00.000Z',
|
|
||||||
attempt: 2,
|
|
||||||
responseMessage: 'success',
|
|
||||||
captcha: {
|
|
||||||
recognizedText: 'abcd',
|
|
||||||
imageBase64: 'image-data',
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
cookieKeys: ['PHPSESSID', 'foo'],
|
|
||||||
cookieCount: 2,
|
|
||||||
cookieHeaderMasked: 'abcdef****567890',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('buildAdminKhhaoOrderQueryResult maps preview list and preserves raw items', () => {
|
|
||||||
const rawItems = [
|
|
||||||
{
|
|
||||||
ordersn: ' A1 ',
|
|
||||||
pingtai: '3',
|
|
||||||
pingtaiName: '快手',
|
|
||||||
shopid: '1001',
|
|
||||||
shopName: '店铺A',
|
|
||||||
goodid: 'g1',
|
|
||||||
goodName: '商品A',
|
|
||||||
sku: 'sku-1',
|
|
||||||
num: '2',
|
|
||||||
fee: '12.34',
|
|
||||||
status: '2',
|
|
||||||
statusName: '<span>已支付</span>',
|
|
||||||
addtime: '2026-05-04 10:00:00',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const result = buildAdminKhhaoOrderQueryResult(
|
|
||||||
{
|
|
||||||
baseUrl: ' https://admin.khhao.com ',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
page: 2,
|
|
||||||
limit: 20,
|
|
||||||
total: 50,
|
|
||||||
items: rawItems,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.equal(result.baseUrl, 'https://admin.khhao.com')
|
|
||||||
assert.equal(result.page, 2)
|
|
||||||
assert.equal(result.limit, 20)
|
|
||||||
assert.equal(result.total, 50)
|
|
||||||
assert.equal(result.itemCount, 1)
|
|
||||||
assert.equal(result.rawItems, rawItems)
|
|
||||||
assert.equal(result.items[0].platformOrderId, 'A1')
|
|
||||||
assert.equal(result.items[0].platform, 'kuaishou')
|
|
||||||
assert.equal(result.items[0].itemTitle, '商品A')
|
|
||||||
})
|
|
||||||
@@ -26,47 +26,6 @@ export function maskPhone(value) {
|
|||||||
return `${normalized.slice(0, 3)}****${normalized.slice(-4)}`
|
return `${normalized.slice(0, 3)}****${normalized.slice(-4)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapAdminKhhaoSourceConfig(config = {}) {
|
|
||||||
return {
|
|
||||||
enabled: config.enabled !== false,
|
|
||||||
baseUrl: String(config.baseUrl || '').trim(),
|
|
||||||
username: String(config.username || '').trim(),
|
|
||||||
password: String(config.password || '').trim(),
|
|
||||||
maxCaptchaAttempts: Number(config.maxCaptchaAttempts || 3) || 3,
|
|
||||||
autoSync: {
|
|
||||||
enabled: config.autoSync?.enabled === true,
|
|
||||||
intervalMinutes: Number(config.autoSync?.intervalMinutes || 3) || 3,
|
|
||||||
pages: Number(config.autoSync?.pages || 2) || 2,
|
|
||||||
pageSize: Number(config.autoSync?.pageSize || 20) || 20,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapAdminKhhaoSyncState(syncState = {}) {
|
|
||||||
return {
|
|
||||||
syncFromCreatedAt: String(syncState.syncFromCreatedAt || '').trim(),
|
|
||||||
lastRunStartedAt: String(syncState.lastRunStartedAt || '').trim(),
|
|
||||||
lastRunFinishedAt: String(syncState.lastRunFinishedAt || '').trim(),
|
|
||||||
lastRunStatus: String(syncState.lastRunStatus || '').trim(),
|
|
||||||
lastErrorMessage: String(syncState.lastErrorMessage || '').trim(),
|
|
||||||
fetchedCount: Number(syncState.fetchedCount || 0),
|
|
||||||
syncedCount: Number(syncState.syncedCount || 0),
|
|
||||||
ignoredCount: Number(syncState.ignoredCount || 0),
|
|
||||||
watchMode: String(syncState.watchMode || 'normal').trim() || 'normal',
|
|
||||||
watchOrders: Array.isArray(syncState.watchOrders)
|
|
||||||
? syncState.watchOrders.map((item) => ({
|
|
||||||
platformOrderId: String(item?.platformOrderId || '').trim(),
|
|
||||||
shopId: String(item?.shopId || '').trim(),
|
|
||||||
shopName: String(item?.shopName || '').trim(),
|
|
||||||
itemTitle: String(item?.itemTitle || '').trim(),
|
|
||||||
payStatus: String(item?.payStatus || '').trim(),
|
|
||||||
detectedAt: String(item?.detectedAt || '').trim(),
|
|
||||||
lastSeenAt: String(item?.lastSeenAt || '').trim(),
|
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapAdminKuaishouEticketShopItem(item = {}) {
|
export function mapAdminKuaishouEticketShopItem(item = {}) {
|
||||||
return {
|
return {
|
||||||
shopId: String(item.shopId || '').trim(),
|
shopId: String(item.shopId || '').trim(),
|
||||||
@@ -118,10 +77,9 @@ export function mapAdminKuaishouCloudFulfillmentItem(item = {}) {
|
|||||||
id: String(item.id || '').trim(),
|
id: String(item.id || '').trim(),
|
||||||
enabled: item.enabled !== false,
|
enabled: item.enabled !== false,
|
||||||
priority: Number(item.priority || 100) || 100,
|
priority: Number(item.priority || 100) || 100,
|
||||||
provider: String(item.provider || 'khhao').trim() || 'khhao',
|
provider: String(item.provider || '91kaquan').trim() || '91kaquan',
|
||||||
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
|
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
|
||||||
shopId: String(item.shopId || '').trim(),
|
shopId: String(item.shopId || '').trim(),
|
||||||
khhaoShopId: String(item.khhaoShopId || '').trim(),
|
|
||||||
internalSkuCode: String(item.internalSkuCode || '').trim(),
|
internalSkuCode: String(item.internalSkuCode || '').trim(),
|
||||||
internalSkuName: String(item.internalSkuName || '').trim(),
|
internalSkuName: String(item.internalSkuName || '').trim(),
|
||||||
externalSkuCode: String(item.externalSkuCode || '').trim(),
|
externalSkuCode: String(item.externalSkuCode || '').trim(),
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import assert from 'node:assert/strict'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
mapAdminCloudtentaclesSession,
|
mapAdminCloudtentaclesSession,
|
||||||
mapAdminKhhaoSourceConfig,
|
|
||||||
mapAdminKhhaoSyncState,
|
|
||||||
mapAdminKuaishouCloudFulfillmentSource,
|
mapAdminKuaishouCloudFulfillmentSource,
|
||||||
mapAdminKuaishouEticketSourceConfig,
|
mapAdminKuaishouEticketSourceConfig,
|
||||||
maskPhone,
|
maskPhone,
|
||||||
@@ -23,79 +21,6 @@ test('maskPhone hides middle digits for mobile numbers', () => {
|
|||||||
assert.equal(maskPhone(''), '')
|
assert.equal(maskPhone(''), '')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('mapAdminKhhaoSourceConfig normalizes defaults and nested auto sync values', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
mapAdminKhhaoSourceConfig({
|
|
||||||
enabled: true,
|
|
||||||
baseUrl: ' https://admin.khhao.com ',
|
|
||||||
username: ' user ',
|
|
||||||
password: ' pass ',
|
|
||||||
maxCaptchaAttempts: '0',
|
|
||||||
autoSync: {
|
|
||||||
enabled: true,
|
|
||||||
intervalMinutes: '',
|
|
||||||
pages: '0',
|
|
||||||
pageSize: '50',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
baseUrl: 'https://admin.khhao.com',
|
|
||||||
username: 'user',
|
|
||||||
password: 'pass',
|
|
||||||
maxCaptchaAttempts: 3,
|
|
||||||
autoSync: {
|
|
||||||
enabled: true,
|
|
||||||
intervalMinutes: 3,
|
|
||||||
pages: 2,
|
|
||||||
pageSize: 50,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('mapAdminKhhaoSyncState normalizes watch orders and fallback values', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
mapAdminKhhaoSyncState({
|
|
||||||
watchMode: '',
|
|
||||||
fetchedCount: '8',
|
|
||||||
watchOrders: [
|
|
||||||
{
|
|
||||||
platformOrderId: ' A1 ',
|
|
||||||
shopId: ' S1 ',
|
|
||||||
shopName: ' 店铺 ',
|
|
||||||
itemTitle: ' 商品 ',
|
|
||||||
payStatus: ' unpaid ',
|
|
||||||
detectedAt: ' t1 ',
|
|
||||||
lastSeenAt: ' t2 ',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
syncFromCreatedAt: '',
|
|
||||||
lastRunStartedAt: '',
|
|
||||||
lastRunFinishedAt: '',
|
|
||||||
lastRunStatus: '',
|
|
||||||
lastErrorMessage: '',
|
|
||||||
fetchedCount: 8,
|
|
||||||
syncedCount: 0,
|
|
||||||
ignoredCount: 0,
|
|
||||||
watchMode: 'normal',
|
|
||||||
watchOrders: [
|
|
||||||
{
|
|
||||||
platformOrderId: 'A1',
|
|
||||||
shopId: 'S1',
|
|
||||||
shopName: '店铺',
|
|
||||||
itemTitle: '商品',
|
|
||||||
payStatus: 'unpaid',
|
|
||||||
detectedAt: 't1',
|
|
||||||
lastSeenAt: 't2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('mapAdminKuaishouEticketSourceConfig maps shops and masks cookies', () => {
|
test('mapAdminKuaishouEticketSourceConfig maps shops and masks cookies', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
mapAdminKuaishouEticketSourceConfig(
|
mapAdminKuaishouEticketSourceConfig(
|
||||||
@@ -169,10 +94,9 @@ test('mapAdminKuaishouCloudFulfillmentSource normalizes items and defaults', ()
|
|||||||
id: 'item-1',
|
id: 'item-1',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
provider: 'khhao',
|
provider: '91kaquan',
|
||||||
platform: 'kuaishou',
|
platform: 'kuaishou',
|
||||||
shopId: '',
|
shopId: '',
|
||||||
khhaoShopId: '',
|
|
||||||
internalSkuCode: '',
|
internalSkuCode: '',
|
||||||
internalSkuName: '',
|
internalSkuName: '',
|
||||||
externalSkuCode: '',
|
externalSkuCode: '',
|
||||||
|
|||||||
@@ -3,14 +3,6 @@ export {
|
|||||||
updateAdminAgisoShopConfigs,
|
updateAdminAgisoShopConfigs,
|
||||||
} from './agiso-service.js'
|
} from './agiso-service.js'
|
||||||
|
|
||||||
export {
|
|
||||||
getAdminKhhaoSourceConfig,
|
|
||||||
updateAdminKhhaoSourceConfig,
|
|
||||||
testAdminKhhaoLogin,
|
|
||||||
queryAdminKhhaoOrders,
|
|
||||||
syncAdminKhhaoOrders,
|
|
||||||
} from './khhao-service.js'
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getAdminNinetyoneOrders,
|
getAdminNinetyoneOrders,
|
||||||
retryAdminNinetyoneOrder,
|
retryAdminNinetyoneOrder,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
import { normalizeProductName } from '../../order/product-match-service.js'
|
import { normalizeProductName } from '../../order/product-match-service.js'
|
||||||
import { isPlainObject } from './context.js'
|
import { isPlainObject } from './context.js'
|
||||||
import { resolveFulfillmentBindingMatchShopId } from './domain.js'
|
|
||||||
|
|
||||||
export function assertAdminFulfillmentBindingsInput(bindings) {
|
export function assertAdminFulfillmentBindingsInput(bindings) {
|
||||||
if (Array.isArray(bindings)) {
|
if (Array.isArray(bindings)) {
|
||||||
@@ -21,10 +20,6 @@ export function normalizeAdminFulfillmentBindingItem(
|
|||||||
options = {},
|
options = {},
|
||||||
) {
|
) {
|
||||||
const index = Number(options.index || 0)
|
const index = Number(options.index || 0)
|
||||||
const kuaishouEticketSource = /** @type {Record<string, unknown>} */ (options.kuaishouEticketSource || {})
|
|
||||||
const resolveKuaishouEticketShopConfig = /** @type {(shop: Record<string, unknown>, source: Record<string, unknown>) => Record<string, unknown> | null} */ (
|
|
||||||
options.resolveKuaishouEticketShopConfig || (() => null)
|
|
||||||
)
|
|
||||||
if (!isPlainObject(rawBinding)) {
|
if (!isPlainObject(rawBinding)) {
|
||||||
throw createHttpError(`第 ${index + 1} 条规则格式不正确`, {
|
throw createHttpError(`第 ${index + 1} 条规则格式不正确`, {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
@@ -34,9 +29,8 @@ export function normalizeAdminFulfillmentBindingItem(
|
|||||||
|
|
||||||
const provider = String(rawBinding.provider || 'agiso').trim() || 'agiso'
|
const provider = String(rawBinding.provider || 'agiso').trim() || 'agiso'
|
||||||
const platform = String(rawBinding.platform || '').trim()
|
const platform = String(rawBinding.platform || '').trim()
|
||||||
let shopId = String(rawBinding.shopId || '').trim()
|
const shopId = String(rawBinding.shopId || '').trim()
|
||||||
let shopName = String(rawBinding.shopName || '').trim()
|
const shopName = String(rawBinding.shopName || '').trim()
|
||||||
const khhaoShopId = String(rawBinding.khhaoShopId || '').trim()
|
|
||||||
const skuCode = String(rawBinding.skuCode || '').trim()
|
const skuCode = String(rawBinding.skuCode || '').trim()
|
||||||
const skuName = String(rawBinding.skuName || '').trim()
|
const skuName = String(rawBinding.skuName || '').trim()
|
||||||
const profileKey = String(rawBinding.profileKey || '').trim() || 'manual_review'
|
const profileKey = String(rawBinding.profileKey || '').trim() || 'manual_review'
|
||||||
@@ -46,44 +40,6 @@ export function normalizeAdminFulfillmentBindingItem(
|
|||||||
const externalSkuName = String(match.externalSkuName || '').trim()
|
const externalSkuName = String(match.externalSkuName || '').trim()
|
||||||
const config = isPlainObject(rawBinding.config) ? { ...rawBinding.config } : {}
|
const config = isPlainObject(rawBinding.config) ? { ...rawBinding.config } : {}
|
||||||
|
|
||||||
if (provider === 'khhao' && platform === 'kuaishou') {
|
|
||||||
if (!khhaoShopId) {
|
|
||||||
throw createHttpError(`第 ${index + 1} 条快手规则缺少 khhao 店铺 ID`, {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'admin_fulfillment_bindings_missing_khhao_shop_id',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchedShop = resolveKuaishouEticketShopConfig(
|
|
||||||
{
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
},
|
|
||||||
kuaishouEticketSource,
|
|
||||||
)
|
|
||||||
const matchedShopId = String(matchedShop?.shopId || '').trim()
|
|
||||||
const matchedShopName = String(matchedShop?.kshopName || '').trim()
|
|
||||||
|
|
||||||
if (!matchedShopId || !matchedShopName) {
|
|
||||||
throw createHttpError(
|
|
||||||
`第 ${index + 1} 条快手规则的店铺未匹配到已配置的快手官方店铺,请先到“平台配置”补齐 Cookie 后再选择`,
|
|
||||||
{
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'admin_fulfillment_bindings_kuaishou_shop_not_configured',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
shopId = matchedShopId
|
|
||||||
shopName = matchedShopName
|
|
||||||
config.kuaishouShop = {
|
|
||||||
...(isPlainObject(config.kuaishouShop) ? config.kuaishouShop : {}),
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
khhaoShopId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skuCode) {
|
if (!skuCode) {
|
||||||
throw createHttpError(`第 ${index + 1} 条规则缺少内部履约 SKU`, {
|
throw createHttpError(`第 ${index + 1} 条规则缺少内部履约 SKU`, {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
@@ -103,7 +59,6 @@ export function normalizeAdminFulfillmentBindingItem(
|
|||||||
platform,
|
platform,
|
||||||
shopId,
|
shopId,
|
||||||
shopName,
|
shopName,
|
||||||
khhaoShopId,
|
|
||||||
skuCode,
|
skuCode,
|
||||||
skuName,
|
skuName,
|
||||||
profileKey,
|
profileKey,
|
||||||
@@ -123,12 +78,7 @@ export function buildAdminFulfillmentBindingUniqueKey(binding) {
|
|||||||
return [
|
return [
|
||||||
String(binding?.provider || '').trim(),
|
String(binding?.provider || '').trim(),
|
||||||
String(binding?.platform || '').trim(),
|
String(binding?.platform || '').trim(),
|
||||||
resolveFulfillmentBindingMatchShopId({
|
String(binding?.shopId || '').trim(),
|
||||||
provider: binding?.provider,
|
|
||||||
platform: binding?.platform,
|
|
||||||
shopId: binding?.shopId,
|
|
||||||
khhaoShopId: binding?.khhaoShopId,
|
|
||||||
}),
|
|
||||||
String(binding?.match?.externalItemId || '').trim(),
|
String(binding?.match?.externalItemId || '').trim(),
|
||||||
String(binding?.match?.externalSkuCode || '').trim(),
|
String(binding?.match?.externalSkuCode || '').trim(),
|
||||||
normalizeProductName(String(binding?.match?.externalSkuName || '').trim()),
|
normalizeProductName(String(binding?.match?.externalSkuName || '').trim()),
|
||||||
@@ -142,10 +92,6 @@ export async function validateAdminFulfillmentBindingConfigs(
|
|||||||
) {
|
) {
|
||||||
assertAdminFulfillmentBindingsInput(bindings)
|
assertAdminFulfillmentBindingsInput(bindings)
|
||||||
|
|
||||||
const kuaishouEticketSource = /** @type {Record<string, unknown>} */ (options.kuaishouEticketSource || {})
|
|
||||||
const resolveKuaishouEticketShopConfig = /** @type {(shop: Record<string, unknown>, source: Record<string, unknown>) => Record<string, unknown> | null} */ (
|
|
||||||
options.resolveKuaishouEticketShopConfig || (() => null)
|
|
||||||
)
|
|
||||||
const getFulfillmentProfileByKey = /** @type {(profileKey: string) => Promise<unknown>} */ (
|
const getFulfillmentProfileByKey = /** @type {(profileKey: string) => Promise<unknown>} */ (
|
||||||
options.getFulfillmentProfileByKey || (async () => null)
|
options.getFulfillmentProfileByKey || (async () => null)
|
||||||
)
|
)
|
||||||
@@ -153,11 +99,7 @@ export async function validateAdminFulfillmentBindingConfigs(
|
|||||||
const normalizedBindings = []
|
const normalizedBindings = []
|
||||||
|
|
||||||
for (const [index, rawBinding] of bindings.entries()) {
|
for (const [index, rawBinding] of bindings.entries()) {
|
||||||
const normalized = normalizeAdminFulfillmentBindingItem(rawBinding, {
|
const normalized = normalizeAdminFulfillmentBindingItem(rawBinding, { index })
|
||||||
index,
|
|
||||||
kuaishouEticketSource,
|
|
||||||
resolveKuaishouEticketShopConfig,
|
|
||||||
})
|
|
||||||
|
|
||||||
const profile = await getFulfillmentProfileByKey(normalized.profileKey)
|
const profile = await getFulfillmentProfileByKey(normalized.profileKey)
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ test('assertAdminFulfillmentBindingsInput rejects non-array payloads', () => {
|
|||||||
assert.throws(() => assertAdminFulfillmentBindingsInput({}), /履约配置格式不正确/)
|
assert.throws(() => assertAdminFulfillmentBindingsInput({}), /履约配置格式不正确/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('normalizeAdminFulfillmentBindingItem normalizes kuaishou binding shop mapping', () => {
|
test('normalizeAdminFulfillmentBindingItem normalizes generic binding fields', () => {
|
||||||
const result = normalizeAdminFulfillmentBindingItem(
|
const result = normalizeAdminFulfillmentBindingItem(
|
||||||
{
|
{
|
||||||
provider: 'khhao',
|
provider: ' agiso ',
|
||||||
platform: 'kuaishou',
|
platform: ' xianyu ',
|
||||||
shopId: 'legacy-shop',
|
shopId: ' shop-1 ',
|
||||||
shopName: '旧店铺',
|
shopName: ' 店铺 ',
|
||||||
khhaoShopId: 'khhao-1',
|
|
||||||
skuCode: 'sku-1',
|
skuCode: 'sku-1',
|
||||||
match: {
|
match: {
|
||||||
externalSkuCode: 'ext-1',
|
externalSkuCode: 'ext-1',
|
||||||
@@ -31,24 +30,14 @@ test('normalizeAdminFulfillmentBindingItem normalizes kuaishou binding shop mapp
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{ index: 0 },
|
||||||
index: 0,
|
|
||||||
kuaishouEticketSource: { source: true },
|
|
||||||
resolveKuaishouEticketShopConfig() {
|
|
||||||
return {
|
|
||||||
shopId: 'ks-100',
|
|
||||||
kshopName: '快手店铺A',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.deepEqual(result, {
|
assert.deepEqual(result, {
|
||||||
provider: 'khhao',
|
provider: 'agiso',
|
||||||
platform: 'kuaishou',
|
platform: 'xianyu',
|
||||||
shopId: 'ks-100',
|
shopId: 'shop-1',
|
||||||
shopName: '快手店铺A',
|
shopName: '店铺',
|
||||||
khhaoShopId: 'khhao-1',
|
|
||||||
skuCode: 'sku-1',
|
skuCode: 'sku-1',
|
||||||
skuName: '',
|
skuName: '',
|
||||||
profileKey: 'manual_review',
|
profileKey: 'manual_review',
|
||||||
@@ -57,9 +46,6 @@ test('normalizeAdminFulfillmentBindingItem normalizes kuaishou binding shop mapp
|
|||||||
config: {
|
config: {
|
||||||
kuaishouShop: {
|
kuaishouShop: {
|
||||||
note: 'keep',
|
note: 'keep',
|
||||||
shopId: 'ks-100',
|
|
||||||
shopName: '快手店铺A',
|
|
||||||
khhaoShopId: 'khhao-1',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
match: {
|
match: {
|
||||||
@@ -104,10 +90,9 @@ test('normalizeAdminFulfillmentBindingItem rejects missing sku and match conditi
|
|||||||
test('buildAdminFulfillmentBindingUniqueKey uses normalized external sku name and shop id rules', () => {
|
test('buildAdminFulfillmentBindingUniqueKey uses normalized external sku name and shop id rules', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
buildAdminFulfillmentBindingUniqueKey({
|
buildAdminFulfillmentBindingUniqueKey({
|
||||||
provider: 'khhao',
|
provider: '91kaquan',
|
||||||
platform: 'kuaishou',
|
platform: 'kuaishou',
|
||||||
shopId: 'shop-a',
|
shopId: 'shop-a',
|
||||||
khhaoShopId: 'khhao-a',
|
|
||||||
skuCode: 'sku-1',
|
skuCode: 'sku-1',
|
||||||
match: {
|
match: {
|
||||||
externalItemId: '',
|
externalItemId: '',
|
||||||
@@ -115,7 +100,7 @@ test('buildAdminFulfillmentBindingUniqueKey uses normalized external sku name an
|
|||||||
externalSkuName: ' 礼包 A ',
|
externalSkuName: ' 礼包 A ',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
'khhao::kuaishou::khhao-a::::::礼包 a::sku-1',
|
'91kaquan::kuaishou::shop-a::::::礼包 a::sku-1',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -88,22 +88,6 @@ export function buildAdminAgisoMessagingSavePayload(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeAdminKhhaoSourceConfigPayload(payload = {}) {
|
|
||||||
return {
|
|
||||||
enabled: payload.enabled !== false,
|
|
||||||
baseUrl: String(payload.baseUrl || '').trim() || 'https://admin.khhao.com',
|
|
||||||
username: String(payload.username || '').trim(),
|
|
||||||
password: String(payload.password || '').trim(),
|
|
||||||
maxCaptchaAttempts: Number(payload.maxCaptchaAttempts || 3) || 3,
|
|
||||||
autoSync: {
|
|
||||||
enabled: payload.autoSync?.enabled === true,
|
|
||||||
intervalMinutes: Number(payload.autoSync?.intervalMinutes || 3) || 3,
|
|
||||||
pages: Number(payload.autoSync?.pages || 2) || 2,
|
|
||||||
pageSize: Number(payload.autoSync?.pageSize || 20) || 20,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeAdminCloudtentaclesSourceConfigPayload(payload = {}) {
|
export function normalizeAdminCloudtentaclesSourceConfigPayload(payload = {}) {
|
||||||
return {
|
return {
|
||||||
enabled: payload.enabled !== false,
|
enabled: payload.enabled !== false,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
buildAdminAgisoMessagingSavePayload,
|
buildAdminAgisoMessagingSavePayload,
|
||||||
buildAdminAgisoShopConfigUpdate,
|
buildAdminAgisoShopConfigUpdate,
|
||||||
normalizeAdminCloudtentaclesSourceConfigPayload,
|
normalizeAdminCloudtentaclesSourceConfigPayload,
|
||||||
normalizeAdminKhhaoSourceConfigPayload,
|
|
||||||
} from './writes.js'
|
} from './writes.js'
|
||||||
|
|
||||||
test('buildAdminAgisoShopConfigUpdate merges item fields and drops explicit empty templates', () => {
|
test('buildAdminAgisoShopConfigUpdate merges item fields and drops explicit empty templates', () => {
|
||||||
@@ -102,37 +101,6 @@ test('buildAdminAgisoMessagingSavePayload updates defaults and keeps only shops
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('normalizeAdminKhhaoSourceConfigPayload applies defaults and numeric fallbacks', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
normalizeAdminKhhaoSourceConfigPayload({
|
|
||||||
enabled: true,
|
|
||||||
baseUrl: ' https://admin.khhao.com ',
|
|
||||||
username: ' user ',
|
|
||||||
password: ' pass ',
|
|
||||||
maxCaptchaAttempts: 0,
|
|
||||||
autoSync: {
|
|
||||||
enabled: true,
|
|
||||||
intervalMinutes: '',
|
|
||||||
pages: '0',
|
|
||||||
pageSize: '50',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
baseUrl: 'https://admin.khhao.com',
|
|
||||||
username: 'user',
|
|
||||||
password: 'pass',
|
|
||||||
maxCaptchaAttempts: 3,
|
|
||||||
autoSync: {
|
|
||||||
enabled: true,
|
|
||||||
intervalMinutes: 3,
|
|
||||||
pages: 2,
|
|
||||||
pageSize: 50,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('normalizeAdminCloudtentaclesSourceConfigPayload applies defaults', () => {
|
test('normalizeAdminCloudtentaclesSourceConfigPayload applies defaults', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
normalizeAdminCloudtentaclesSourceConfigPayload({
|
normalizeAdminCloudtentaclesSourceConfigPayload({
|
||||||
|
|||||||
@@ -165,26 +165,17 @@ export async function syncConfiguredFulfillmentBindings(profileMap = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveBindingMatchShopIds(binding = {}) {
|
export function resolveBindingMatchShopIds(binding = {}) {
|
||||||
if (String(binding.provider || '').trim() === 'khhao' && String(binding.platform || '').trim() === 'kuaishou') {
|
|
||||||
const candidates = [...new Set([
|
|
||||||
String(binding.khhaoShopId || '').trim(),
|
|
||||||
String(binding.shopId || '').trim(),
|
|
||||||
].filter(Boolean))]
|
|
||||||
return candidates.length > 0 ? candidates : ['']
|
|
||||||
}
|
|
||||||
|
|
||||||
return [String(binding.shopId || '').trim()]
|
return [String(binding.shopId || '').trim()]
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBindingRuntimeConfig(binding = {}) {
|
function resolveBindingRuntimeConfig(binding = {}) {
|
||||||
const baseConfig = isPlainObject(binding.config) ? { ...binding.config } : {}
|
const baseConfig = isPlainObject(binding.config) ? { ...binding.config } : {}
|
||||||
|
|
||||||
if (String(binding.provider || '').trim() === 'khhao' && String(binding.platform || '').trim() === 'kuaishou') {
|
if (String(binding.provider || '').trim() === '91kaquan' && String(binding.platform || '').trim() === 'kuaishou') {
|
||||||
baseConfig.kuaishouShop = {
|
baseConfig.kuaishouShop = {
|
||||||
...(isPlainObject(baseConfig.kuaishouShop) ? baseConfig.kuaishouShop : {}),
|
...(isPlainObject(baseConfig.kuaishouShop) ? baseConfig.kuaishouShop : {}),
|
||||||
shopId: String(binding.shopId || '').trim(),
|
shopId: String(binding.shopId || '').trim(),
|
||||||
shopName: String(binding.shopName || '').trim(),
|
shopName: String(binding.shopName || '').trim(),
|
||||||
khhaoShopId: String(binding.khhaoShopId || '').trim(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ import assert from 'node:assert/strict'
|
|||||||
|
|
||||||
import { resolveBindingMatchShopIds } from './fulfillment-bootstrap-service.js'
|
import { resolveBindingMatchShopIds } from './fulfillment-bootstrap-service.js'
|
||||||
|
|
||||||
test('resolveBindingMatchShopIds returns dual shop ids for khhao kuaishou bindings', () => {
|
test('resolveBindingMatchShopIds returns configured shop id for 91kaquan kuaishou bindings', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
resolveBindingMatchShopIds({
|
resolveBindingMatchShopIds({
|
||||||
provider: 'khhao',
|
provider: '91kaquan',
|
||||||
platform: 'kuaishou',
|
platform: 'kuaishou',
|
||||||
shopId: '4269276762',
|
shopId: '91kaquan',
|
||||||
khhaoShopId: '10',
|
|
||||||
}),
|
}),
|
||||||
['10', '4269276762'],
|
['91kaquan'],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function filterLegacyOrderFulfillmentBindings(bindings = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isKuaishouCloudFulfillmentBinding(binding = {}) {
|
export function isKuaishouCloudFulfillmentBinding(binding = {}) {
|
||||||
return String(binding?.provider || '').trim() === 'khhao'
|
return String(binding?.provider || '').trim() === '91kaquan'
|
||||||
&& String(binding?.platform || '').trim() === 'kuaishou'
|
&& String(binding?.platform || '').trim() === 'kuaishou'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,6 @@ function normalizeOrderFulfillmentBinding(rawValue) {
|
|||||||
const platform = String(rawValue.platform || '').trim()
|
const platform = String(rawValue.platform || '').trim()
|
||||||
const shopId = String(rawValue.shopId || '').trim()
|
const shopId = String(rawValue.shopId || '').trim()
|
||||||
const shopName = String(rawValue.shopName || '').trim()
|
const shopName = String(rawValue.shopName || '').trim()
|
||||||
const khhaoShopId = String(rawValue.khhaoShopId || '').trim()
|
|
||||||
const skuCode = String(rawValue.skuCode || '').trim()
|
const skuCode = String(rawValue.skuCode || '').trim()
|
||||||
const skuName = String(rawValue.skuName || '').trim()
|
const skuName = String(rawValue.skuName || '').trim()
|
||||||
const profileKey = String(rawValue.profileKey || '').trim() || 'manual_review'
|
const profileKey = String(rawValue.profileKey || '').trim() || 'manual_review'
|
||||||
@@ -88,7 +87,6 @@ function normalizeOrderFulfillmentBinding(rawValue) {
|
|||||||
platform,
|
platform,
|
||||||
shopId,
|
shopId,
|
||||||
shopName,
|
shopName,
|
||||||
khhaoShopId,
|
|
||||||
skuCode,
|
skuCode,
|
||||||
skuName,
|
skuName,
|
||||||
profileKey,
|
profileKey,
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import {
|
|||||||
isKuaishouCloudFulfillmentBinding,
|
isKuaishouCloudFulfillmentBinding,
|
||||||
} from './fulfillment-binding-config-service.js'
|
} from './fulfillment-binding-config-service.js'
|
||||||
|
|
||||||
test('isKuaishouCloudFulfillmentBinding only marks khhao kuaishou rules as cloud rules', () => {
|
test('isKuaishouCloudFulfillmentBinding only marks 91kaquan kuaishou rules as cloud rules', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
isKuaishouCloudFulfillmentBinding({
|
isKuaishouCloudFulfillmentBinding({
|
||||||
provider: 'khhao',
|
provider: '91kaquan',
|
||||||
platform: 'kuaishou',
|
platform: 'kuaishou',
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
@@ -24,7 +24,7 @@ test('isKuaishouCloudFulfillmentBinding only marks khhao kuaishou rules as cloud
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('filterLegacyOrderFulfillmentBindings keeps agiso rules and removes khhao kuaishou rules', () => {
|
test('filterLegacyOrderFulfillmentBindings keeps agiso rules and removes 91kaquan kuaishou rules', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
filterLegacyOrderFulfillmentBindings([
|
filterLegacyOrderFulfillmentBindings([
|
||||||
{
|
{
|
||||||
@@ -34,10 +34,9 @@ test('filterLegacyOrderFulfillmentBindings keeps agiso rules and removes khhao k
|
|||||||
skuCode: 'sku-a',
|
skuCode: 'sku-a',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provider: 'khhao',
|
provider: '91kaquan',
|
||||||
platform: 'kuaishou',
|
platform: 'kuaishou',
|
||||||
shopId: '4269276762',
|
shopId: '91kaquan',
|
||||||
khhaoShopId: '10',
|
|
||||||
skuCode: 'sku-b',
|
skuCode: 'sku-b',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ export function mapKuaishouCloudFulfillmentItemsToBindings(config = {}) {
|
|||||||
return items
|
return items
|
||||||
.filter((item) => item && item.enabled !== false)
|
.filter((item) => item && item.enabled !== false)
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
provider: String(item.provider || 'khhao').trim() || 'khhao',
|
provider: String(item.provider || '91kaquan').trim() || '91kaquan',
|
||||||
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
|
platform: String(item.platform || 'kuaishou').trim() || 'kuaishou',
|
||||||
shopId: String(item.shopId || '').trim(),
|
shopId: String(item.shopId || '').trim(),
|
||||||
khhaoShopId: String(item.khhaoShopId || '').trim(),
|
|
||||||
skuCode: String(item.internalSkuCode || '').trim(),
|
skuCode: String(item.internalSkuCode || '').trim(),
|
||||||
skuName: String(item.internalSkuName || '').trim(),
|
skuName: String(item.internalSkuName || '').trim(),
|
||||||
profileKey: 'kuaishou_ct_assisted',
|
profileKey: 'kuaishou_ct_assisted',
|
||||||
@@ -121,10 +120,9 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue) {
|
|||||||
id: String(rawValue.id || internalSkuCode).trim() || internalSkuCode,
|
id: String(rawValue.id || internalSkuCode).trim() || internalSkuCode,
|
||||||
enabled: rawValue.enabled !== false,
|
enabled: rawValue.enabled !== false,
|
||||||
priority: normalizePriority(rawValue.priority),
|
priority: normalizePriority(rawValue.priority),
|
||||||
provider: String(rawValue.provider || 'khhao').trim() || 'khhao',
|
provider: String(rawValue.provider || '91kaquan').trim() || '91kaquan',
|
||||||
platform: String(rawValue.platform || 'kuaishou').trim() || 'kuaishou',
|
platform: String(rawValue.platform || 'kuaishou').trim() || 'kuaishou',
|
||||||
shopId: String(rawValue.shopId || '').trim(),
|
shopId: String(rawValue.shopId || '').trim(),
|
||||||
khhaoShopId: String(rawValue.khhaoShopId || '').trim(),
|
|
||||||
internalSkuCode,
|
internalSkuCode,
|
||||||
internalSkuName: String(rawValue.internalSkuName || '').trim() || internalSkuCode,
|
internalSkuName: String(rawValue.internalSkuName || '').trim() || internalSkuCode,
|
||||||
externalSkuCode,
|
externalSkuCode,
|
||||||
|
|||||||
@@ -3,16 +3,15 @@ import assert from 'node:assert/strict'
|
|||||||
|
|
||||||
import { mapKuaishouCloudFulfillmentItemsToBindings } from './kuaishou-cloud-fulfillment-config-service.js'
|
import { mapKuaishouCloudFulfillmentItemsToBindings } from './kuaishou-cloud-fulfillment-config-service.js'
|
||||||
|
|
||||||
test('mapKuaishouCloudFulfillmentItemsToBindings preserves khhao shop id', () => {
|
test('mapKuaishouCloudFulfillmentItemsToBindings maps 91kaquan rules', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
mapKuaishouCloudFulfillmentItemsToBindings({
|
mapKuaishouCloudFulfillmentItemsToBindings({
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
provider: 'khhao',
|
provider: '91kaquan',
|
||||||
platform: 'kuaishou',
|
platform: 'kuaishou',
|
||||||
shopId: '4269276762',
|
shopId: '91kaquan',
|
||||||
khhaoShopId: '10',
|
|
||||||
internalSkuCode: 'sku-1',
|
internalSkuCode: 'sku-1',
|
||||||
internalSkuName: '内部商品',
|
internalSkuName: '内部商品',
|
||||||
externalSkuCode: '952',
|
externalSkuCode: '952',
|
||||||
@@ -24,10 +23,9 @@ test('mapKuaishouCloudFulfillmentItemsToBindings preserves khhao shop id', () =>
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
provider: 'khhao',
|
provider: '91kaquan',
|
||||||
platform: 'kuaishou',
|
platform: 'kuaishou',
|
||||||
shopId: '4269276762',
|
shopId: '91kaquan',
|
||||||
khhaoShopId: '10',
|
|
||||||
skuCode: 'sku-1',
|
skuCode: 'sku-1',
|
||||||
skuName: '内部商品',
|
skuName: '内部商品',
|
||||||
profileKey: 'kuaishou_ct_assisted',
|
profileKey: 'kuaishou_ct_assisted',
|
||||||
|
|||||||
@@ -1,227 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { getKhhaoSourceConfig } from './source-config-service.js'
|
|
||||||
import { ensureKhhaoSyncBaseline, getKhhaoSyncState, saveKhhaoSyncState } from './sync-state-service.js'
|
|
||||||
import { syncKhhaoOrders } from './order-sync-service.js'
|
|
||||||
import { logError, logInfo, logWarn } from '../../../utils/logger.js'
|
|
||||||
import { nowIso } from '../../../utils/time.js'
|
|
||||||
|
|
||||||
let timer = null
|
|
||||||
let running = false
|
|
||||||
const BOOST_INTERVAL_MIN_MS = 3_000
|
|
||||||
const BOOST_INTERVAL_MAX_MS = 5_000
|
|
||||||
|
|
||||||
export function startKhhaoAutoSyncLoop() {
|
|
||||||
scheduleNextRun(5_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stopKhhaoAutoSyncLoop() {
|
|
||||||
if (timer) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
timer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runKhhaoAutoSyncCycle() {
|
|
||||||
const source = getKhhaoSourceConfig()
|
|
||||||
const currentState = getKhhaoSyncState()
|
|
||||||
|
|
||||||
if (!source.enabled || !source.autoSync?.enabled) {
|
|
||||||
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (running) {
|
|
||||||
logWarn('[khhao/auto-sync]', '检测到上一次自动同步仍在运行,本轮跳过')
|
|
||||||
scheduleNextRun(resolveNextDelayMs(source, currentState))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!String(source.username || '').trim() || !String(source.password || '').trim()) {
|
|
||||||
logWarn('[khhao/auto-sync]', 'khhao 自动同步已启用,但缺少账号或密码')
|
|
||||||
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
running = true
|
|
||||||
const baselineState = ensureKhhaoSyncBaseline()
|
|
||||||
const startedAt = nowIso()
|
|
||||||
|
|
||||||
saveKhhaoSyncState({
|
|
||||||
...baselineState,
|
|
||||||
lastRunStartedAt: startedAt,
|
|
||||||
lastRunStatus: 'running',
|
|
||||||
lastErrorMessage: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await syncKhhaoOrders({
|
|
||||||
baseUrl: source.baseUrl,
|
|
||||||
username: source.username,
|
|
||||||
password: source.password,
|
|
||||||
maxCaptchaAttempts: source.maxCaptchaAttempts,
|
|
||||||
pages: source.autoSync.pages,
|
|
||||||
limit: source.autoSync.pageSize,
|
|
||||||
createdAfter: baselineState.syncFromCreatedAt,
|
|
||||||
})
|
|
||||||
|
|
||||||
const latestCreatedAt = resolveLatestOrderCreatedAt(result.results)
|
|
||||||
const nextWatchOrders = reconcileKhhaoWatchOrders(baselineState.watchOrders, result.results, startedAt)
|
|
||||||
const watchMode = nextWatchOrders.length > 0 ? 'boosted' : 'normal'
|
|
||||||
|
|
||||||
saveKhhaoSyncState({
|
|
||||||
...getKhhaoSyncState(),
|
|
||||||
syncFromCreatedAt: baselineState.syncFromCreatedAt,
|
|
||||||
lastRunStartedAt: startedAt,
|
|
||||||
lastRunFinishedAt: nowIso(),
|
|
||||||
lastRunStatus: 'success',
|
|
||||||
lastErrorMessage: '',
|
|
||||||
fetchedCount: result.fetchedCount,
|
|
||||||
syncedCount: result.syncedCount,
|
|
||||||
ignoredCount: result.ignoredCount,
|
|
||||||
lastOrderCreatedAt: latestCreatedAt,
|
|
||||||
watchMode,
|
|
||||||
watchOrders: nextWatchOrders,
|
|
||||||
})
|
|
||||||
|
|
||||||
logInfo('[khhao/auto-sync]', 'khhao 自动同步完成', {
|
|
||||||
fetchedCount: result.fetchedCount,
|
|
||||||
syncedCount: result.syncedCount,
|
|
||||||
ignoredCount: result.ignoredCount,
|
|
||||||
syncFromCreatedAt: baselineState.syncFromCreatedAt,
|
|
||||||
watchMode,
|
|
||||||
watchOrderCount: nextWatchOrders.length,
|
|
||||||
watchOrders: nextWatchOrders.map((item) => ({
|
|
||||||
platformOrderId: item.platformOrderId,
|
|
||||||
shopName: item.shopName,
|
|
||||||
payStatus: item.payStatus,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error || 'khhao 自动同步失败')
|
|
||||||
saveKhhaoSyncState({
|
|
||||||
...getKhhaoSyncState(),
|
|
||||||
syncFromCreatedAt: baselineState.syncFromCreatedAt,
|
|
||||||
lastRunStartedAt: startedAt,
|
|
||||||
lastRunFinishedAt: nowIso(),
|
|
||||||
lastRunStatus: 'failed',
|
|
||||||
lastErrorMessage: message,
|
|
||||||
})
|
|
||||||
logError('[khhao/auto-sync]', 'khhao 自动同步失败', error)
|
|
||||||
} finally {
|
|
||||||
running = false
|
|
||||||
scheduleNextRun(resolveNextDelayMs(source, getKhhaoSyncState()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleNextRun(delayMs) {
|
|
||||||
stopKhhaoAutoSyncLoop()
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
void runKhhaoAutoSyncCycle()
|
|
||||||
}, delayMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveIntervalMs(value) {
|
|
||||||
const minutes = Number(value)
|
|
||||||
const normalized = Number.isInteger(minutes) && minutes > 0 ? minutes : 3
|
|
||||||
return normalized * 60 * 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveNextDelayMs(source, syncState) {
|
|
||||||
if (hasActiveKhhaoWatchOrders(syncState?.watchOrders)) {
|
|
||||||
return resolveBoostIntervalMs()
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolveIntervalMs(source?.autoSync?.intervalMinutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveBoostIntervalMs() {
|
|
||||||
const delta = BOOST_INTERVAL_MAX_MS - BOOST_INTERVAL_MIN_MS
|
|
||||||
return BOOST_INTERVAL_MIN_MS + Math.floor(Math.random() * (delta + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reconcileKhhaoWatchOrders(currentWatchOrders = [], results = [], now = nowIso()) {
|
|
||||||
const nextMap = new Map(normalizeKhhaoWatchOrders(currentWatchOrders).map((item) => [item.platformOrderId, item]))
|
|
||||||
|
|
||||||
for (const item of Array.isArray(results) ? results : []) {
|
|
||||||
const platformOrderId = String(item?.platformOrderId || '').trim()
|
|
||||||
if (!platformOrderId || item?.ignored) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const payStatus = normalizePayStatus(item?.payStatus)
|
|
||||||
if (payStatus === 'paid') {
|
|
||||||
nextMap.delete(platformOrderId)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payStatus !== 'unpaid') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const previous = nextMap.get(platformOrderId)
|
|
||||||
nextMap.set(platformOrderId, {
|
|
||||||
platformOrderId,
|
|
||||||
shopId: String(item?.shopId || previous?.shopId || '').trim(),
|
|
||||||
shopName: String(item?.shopName || previous?.shopName || '').trim(),
|
|
||||||
itemTitle: String(item?.itemTitle || previous?.itemTitle || '').trim(),
|
|
||||||
payStatus,
|
|
||||||
detectedAt: previous?.detectedAt || now,
|
|
||||||
lastSeenAt: now,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...nextMap.values()]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasActiveKhhaoWatchOrders(watchOrders = []) {
|
|
||||||
return normalizeKhhaoWatchOrders(watchOrders).length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeKhhaoWatchOrders(value) {
|
|
||||||
return (Array.isArray(value) ? value : [])
|
|
||||||
.map((item) => ({
|
|
||||||
platformOrderId: String(item?.platformOrderId || '').trim(),
|
|
||||||
shopId: String(item?.shopId || '').trim(),
|
|
||||||
shopName: String(item?.shopName || '').trim(),
|
|
||||||
itemTitle: String(item?.itemTitle || '').trim(),
|
|
||||||
payStatus: normalizePayStatus(item?.payStatus),
|
|
||||||
detectedAt: String(item?.detectedAt || '').trim(),
|
|
||||||
lastSeenAt: String(item?.lastSeenAt || '').trim(),
|
|
||||||
}))
|
|
||||||
.filter((item) => item.platformOrderId && item.payStatus === 'unpaid')
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePayStatus(value) {
|
|
||||||
const normalized = String(value || '').trim().toLowerCase()
|
|
||||||
if (normalized === 'paid') {
|
|
||||||
return 'paid'
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized === 'unpaid' ? 'unpaid' : 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveLatestOrderCreatedAt(results = []) {
|
|
||||||
let latestText = ''
|
|
||||||
let latestTime = 0
|
|
||||||
|
|
||||||
for (const item of Array.isArray(results) ? results : []) {
|
|
||||||
const text = String(item?.rawOrderCreatedAt || item?.orderCreatedAt || '').trim()
|
|
||||||
const timestamp = parseKhhaoOrderCreatedAt(text)
|
|
||||||
if (Number.isFinite(timestamp) && timestamp > latestTime) {
|
|
||||||
latestTime = timestamp
|
|
||||||
latestText = new Date(timestamp).toISOString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return latestText
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseKhhaoOrderCreatedAt(value) {
|
|
||||||
const text = String(value || '').trim()
|
|
||||||
if (!text) {
|
|
||||||
return NaN
|
|
||||||
}
|
|
||||||
|
|
||||||
return Date.parse(`${text.replace(' ', 'T')}+08:00`)
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import test from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
|
||||||
|
|
||||||
import {
|
|
||||||
hasActiveKhhaoWatchOrders,
|
|
||||||
reconcileKhhaoWatchOrders,
|
|
||||||
} from './auto-sync-service.js'
|
|
||||||
|
|
||||||
test('reconcileKhhaoWatchOrders adds new unpaid synced order into watch list', () => {
|
|
||||||
const now = '2026-05-03T12:00:00.000Z'
|
|
||||||
const next = reconcileKhhaoWatchOrders([], [
|
|
||||||
{
|
|
||||||
platformOrderId: '2612300323173226',
|
|
||||||
shopId: '4269276762',
|
|
||||||
shopName: '稚嫩游戏交易店',
|
|
||||||
itemTitle: '暗影哥特',
|
|
||||||
payStatus: 'unpaid',
|
|
||||||
ignored: false,
|
|
||||||
},
|
|
||||||
], now)
|
|
||||||
|
|
||||||
assert.equal(next.length, 1)
|
|
||||||
assert.deepEqual(next[0], {
|
|
||||||
platformOrderId: '2612300323173226',
|
|
||||||
shopId: '4269276762',
|
|
||||||
shopName: '稚嫩游戏交易店',
|
|
||||||
itemTitle: '暗影哥特',
|
|
||||||
payStatus: 'unpaid',
|
|
||||||
detectedAt: now,
|
|
||||||
lastSeenAt: now,
|
|
||||||
})
|
|
||||||
assert.equal(hasActiveKhhaoWatchOrders(next), true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('reconcileKhhaoWatchOrders removes watched order after payment is detected', () => {
|
|
||||||
const current = [
|
|
||||||
{
|
|
||||||
platformOrderId: '2612300323173226',
|
|
||||||
shopId: '4269276762',
|
|
||||||
shopName: '稚嫩游戏交易店',
|
|
||||||
itemTitle: '暗影哥特',
|
|
||||||
payStatus: 'unpaid',
|
|
||||||
detectedAt: '2026-05-03T12:00:00.000Z',
|
|
||||||
lastSeenAt: '2026-05-03T12:00:00.000Z',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const next = reconcileKhhaoWatchOrders(current, [
|
|
||||||
{
|
|
||||||
platformOrderId: '2612300323173226',
|
|
||||||
shopId: '4269276762',
|
|
||||||
shopName: '稚嫩游戏交易店',
|
|
||||||
itemTitle: '暗影哥特',
|
|
||||||
payStatus: 'paid',
|
|
||||||
ignored: false,
|
|
||||||
},
|
|
||||||
], '2026-05-03T12:00:04.000Z')
|
|
||||||
|
|
||||||
assert.deepEqual(next, [])
|
|
||||||
assert.equal(hasActiveKhhaoWatchOrders(next), false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('reconcileKhhaoWatchOrders ignores ignored and unknown-pay-status rows', () => {
|
|
||||||
const now = '2026-05-03T12:00:00.000Z'
|
|
||||||
const next = reconcileKhhaoWatchOrders([], [
|
|
||||||
{
|
|
||||||
platformOrderId: 'ignored-order',
|
|
||||||
payStatus: 'unpaid',
|
|
||||||
ignored: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
platformOrderId: 'unknown-order',
|
|
||||||
payStatus: 'unknown',
|
|
||||||
ignored: false,
|
|
||||||
},
|
|
||||||
], now)
|
|
||||||
|
|
||||||
assert.deepEqual(next, [])
|
|
||||||
})
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { recognizeImageCaptcha } from '../../session/ocr.js'
|
|
||||||
import { buildKhhaoUrl, buildCookieHeader, normalizeKhhaoCookieState, resolveKhhaoConfig } from './shared.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {{
|
|
||||||
* baseUrl?: string
|
|
||||||
* captchaPath?: string
|
|
||||||
* timeoutMs?: number
|
|
||||||
* cookies?: Record<string, string>
|
|
||||||
* requestId?: string
|
|
||||||
* includeImageBase64?: boolean
|
|
||||||
* }} [options]
|
|
||||||
*/
|
|
||||||
export async function fetchKhhaoCaptcha(options = {}) {
|
|
||||||
const config = resolveKhhaoConfig(options)
|
|
||||||
const cookieHeader = buildCookieHeader(options.cookies)
|
|
||||||
const response = await fetchWithTimeout(buildKhhaoUrl(config.baseUrl, config.captchaPath), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
|
||||||
referer: buildKhhaoUrl(config.baseUrl, config.loginPath).toString(),
|
|
||||||
...(cookieHeader ? { cookie: cookieHeader } : {}),
|
|
||||||
},
|
|
||||||
}, config.timeoutMs)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`khhao 验证码请求失败,HTTP ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageBuffer = Buffer.from(await response.arrayBuffer())
|
|
||||||
const imageBase64 = imageBuffer.toString('base64')
|
|
||||||
const ocr = await recognizeImageCaptcha({
|
|
||||||
imageBase64,
|
|
||||||
imageContentType: String(response.headers.get('content-type') || 'image/png').trim() || 'image/png',
|
|
||||||
tag: options.requestId ? `khhao-${options.requestId}` : 'khhao-login-captcha',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (ocr?.code !== 0) {
|
|
||||||
throw new Error(ocr?.msg || 'khhao 验证码 OCR 识别失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
const captchaText = String(ocr?.data?.text || ocr?.data?.recognizedText || '').trim()
|
|
||||||
if (!captchaText) {
|
|
||||||
throw new Error('khhao 验证码 OCR 未识别出内容')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
captchaText,
|
|
||||||
ocr,
|
|
||||||
cookieMap: normalizeKhhaoCookieState(options.cookies, response.headers),
|
|
||||||
contentType: String(response.headers.get('content-type') || 'image/png').trim() || 'image/png',
|
|
||||||
imageBase64: options.includeImageBase64 ? imageBase64 : '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithTimeout(input, init, timeoutMs) {
|
|
||||||
const controller = new AbortController()
|
|
||||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await fetch(input, {
|
|
||||||
...init,
|
|
||||||
signal: controller.signal,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
|
||||||
throw new Error(`khhao 验证码请求超时(${timeoutMs}ms)`)
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { parseAmountToFen } from '../../../utils/money.js'
|
|
||||||
import { resolveKuaishouEticketShopConfig } from '../kuaishou-eticket/source-config-service.js'
|
|
||||||
|
|
||||||
export function mapKhhaoOrderPreviewList(items = []) {
|
|
||||||
return (Array.isArray(items) ? items : []).map((item) => mapKhhaoOrderPreview(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapKhhaoOrderToSourceEvent(item = {}) {
|
|
||||||
const preview = mapKhhaoOrderPreview(item)
|
|
||||||
const orderStatus = resolveKhhaoOrderStatus(preview.status)
|
|
||||||
const payStatus = resolveKhhaoPayStatus(preview.status)
|
|
||||||
const sourceShopId = String(preview.khhaoShopId || preview.shopId || '').trim()
|
|
||||||
|
|
||||||
return {
|
|
||||||
provider: 'khhao',
|
|
||||||
platform: preview.platform || 'unknown',
|
|
||||||
shopId: sourceShopId,
|
|
||||||
shopIdAliases: uniqueNonEmptyValues([sourceShopId, preview.shopId, ...(preview.shopIdAliases || [])]),
|
|
||||||
shopName: preview.shopName,
|
|
||||||
platformOrderId: preview.platformOrderId,
|
|
||||||
orderStatus,
|
|
||||||
payStatus,
|
|
||||||
buyerId: '',
|
|
||||||
buyerName: String(preview.raw.name || '').trim(),
|
|
||||||
receiverContact: '',
|
|
||||||
totalAmount: preview.totalAmountFen,
|
|
||||||
currency: 'CNY',
|
|
||||||
paidAt: payStatus === 'paid' ? normalizePaidAt(preview.orderCreatedAt) : null,
|
|
||||||
rawPayload: preview.raw,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
itemId: preview.itemId,
|
|
||||||
externalItemId: preview.itemId,
|
|
||||||
externalSkuCode: preview.skuCode || preview.itemId,
|
|
||||||
externalSkuName: preview.itemTitle || preview.skuCode || preview.itemId,
|
|
||||||
skuCode: preview.skuCode || preview.itemId,
|
|
||||||
skuName: preview.itemTitle || preview.skuCode || preview.itemId,
|
|
||||||
quantity: preview.quantity,
|
|
||||||
snapshot: preview.raw,
|
|
||||||
spec: preview.raw,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapKhhaoOrderPreview(item = {}) {
|
|
||||||
const raw = isPlainObject(item) ? item : {}
|
|
||||||
const platform = resolveKhhaoPlatform(raw.pingtai)
|
|
||||||
const quantity = normalizeQuantity(raw.num)
|
|
||||||
const internalShopId = String(raw.shopid || '').trim()
|
|
||||||
const shopName = String(raw.shopName || '').trim()
|
|
||||||
const resolvedShopIdentity = resolveKhhaoShopIdentity({
|
|
||||||
platform,
|
|
||||||
internalShopId,
|
|
||||||
shopName,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
provider: 'khhao',
|
|
||||||
platform,
|
|
||||||
platformLabel: String(raw.pingtaiName || '').trim(),
|
|
||||||
platformOrderId: String(raw.ordersn || '').trim(),
|
|
||||||
shopId: resolvedShopIdentity.shopId,
|
|
||||||
kuaishouShopId: resolvedShopIdentity.officialShopId,
|
|
||||||
shopIdAliases: resolvedShopIdentity.shopIdAliases,
|
|
||||||
khhaoShopId: internalShopId,
|
|
||||||
internalShopId,
|
|
||||||
shopName,
|
|
||||||
itemId: String(raw.goodid || '').trim(),
|
|
||||||
itemTitle: String(raw.goodName || '').trim(),
|
|
||||||
skuCode: String(raw.sku || '').trim(),
|
|
||||||
quantity,
|
|
||||||
totalAmountFen: parseAmountToFen(raw.fee),
|
|
||||||
status: String(raw.status || '').trim(),
|
|
||||||
statusLabel: stripHtmlTags(raw.statusName),
|
|
||||||
orderCreatedAt: String(raw.addtime || '').trim(),
|
|
||||||
raw,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveKhhaoShopIdentity({ platform = '', internalShopId = '', shopName = '' } = {}) {
|
|
||||||
const fallbackShopId = String(internalShopId || '').trim()
|
|
||||||
const normalizedShopName = String(shopName || '').trim()
|
|
||||||
|
|
||||||
if (String(platform || '').trim() !== 'kuaishou') {
|
|
||||||
return {
|
|
||||||
shopId: fallbackShopId,
|
|
||||||
officialShopId: '',
|
|
||||||
shopIdAliases: fallbackShopId ? [fallbackShopId] : [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchedShop = resolveKuaishouEticketShopConfig({
|
|
||||||
shopId: fallbackShopId,
|
|
||||||
shopName: normalizedShopName,
|
|
||||||
})
|
|
||||||
const resolvedShopId = String(matchedShop?.shopId || '').trim() || fallbackShopId
|
|
||||||
const shopIdAliases = uniqueNonEmptyValues([resolvedShopId, fallbackShopId])
|
|
||||||
|
|
||||||
return {
|
|
||||||
shopId: resolvedShopId,
|
|
||||||
officialShopId: String(matchedShop?.shopId || '').trim(),
|
|
||||||
shopIdAliases,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveKhhaoPlatform(value) {
|
|
||||||
const normalized = String(value || '').trim()
|
|
||||||
|
|
||||||
if (normalized === '3') {
|
|
||||||
return 'kuaishou'
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized ? 'unknown' : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveKhhaoOrderStatus(value) {
|
|
||||||
const normalized = String(value || '').trim()
|
|
||||||
|
|
||||||
if (normalized === '2') {
|
|
||||||
return 'paid'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalized === '0') {
|
|
||||||
return 'created'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveKhhaoPayStatus(value) {
|
|
||||||
const normalized = String(value || '').trim()
|
|
||||||
|
|
||||||
if (normalized === '2') {
|
|
||||||
return 'paid'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalized === '0') {
|
|
||||||
return 'unpaid'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeQuantity(value) {
|
|
||||||
const parsed = Number(value)
|
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripHtmlTags(value) {
|
|
||||||
return String(value || '').replace(/<[^>]+>/g, '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePaidAt(value) {
|
|
||||||
const text = String(value || '').trim()
|
|
||||||
return text ? text.replace(' ', 'T') : null
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainObject(value) {
|
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniqueNonEmptyValues(values) {
|
|
||||||
return [...new Set((Array.isArray(values) ? values : [])
|
|
||||||
.map((value) => String(value || '').trim())
|
|
||||||
.filter(Boolean))]
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
|
||||||
import { buildKhhaoUrl, resolveKhhaoConfig } from './shared.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {{
|
|
||||||
* session?: {
|
|
||||||
* baseUrl?: string
|
|
||||||
* cookieHeader?: string
|
|
||||||
* }
|
|
||||||
* page?: number | string
|
|
||||||
* limit?: number | string
|
|
||||||
* baseUrl?: string
|
|
||||||
* orderListPath?: string
|
|
||||||
* timeoutMs?: number
|
|
||||||
* }} [payload]
|
|
||||||
*/
|
|
||||||
export async function queryKhhaoOrderList(payload = {}) {
|
|
||||||
const config = resolveKhhaoConfig(payload)
|
|
||||||
const page = normalizePage(payload.page)
|
|
||||||
const limit = normalizeLimit(payload.limit)
|
|
||||||
const session = payload.session || {}
|
|
||||||
const cookieHeader = String(session.cookieHeader || '').trim()
|
|
||||||
|
|
||||||
if (!cookieHeader) {
|
|
||||||
throw createHttpError('khhao 查询订单缺少登录态 Cookie', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'khhao_query_missing_cookie',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetchWithTimeout(buildKhhaoUrl(config.baseUrl, config.orderListPath, { page, limit }), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json, text/javascript, */*; q=0.01',
|
|
||||||
referer: buildKhhaoUrl(config.baseUrl, config.loginPath).toString(),
|
|
||||||
'x-requested-with': 'XMLHttpRequest',
|
|
||||||
cookie: cookieHeader,
|
|
||||||
},
|
|
||||||
}, config.timeoutMs)
|
|
||||||
|
|
||||||
const rawText = await response.text()
|
|
||||||
if (!response.ok) {
|
|
||||||
throw createHttpError(`khhao 订单查询失败,HTTP ${response.status}`, {
|
|
||||||
statusCode: 502,
|
|
||||||
errorCode: 'khhao_query_http_failed',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const payloadJson = tryParseJson(rawText)
|
|
||||||
if (!payloadJson || typeof payloadJson !== 'object') {
|
|
||||||
throw createHttpError('khhao 订单查询返回了无法解析的 JSON', {
|
|
||||||
statusCode: 502,
|
|
||||||
errorCode: 'khhao_query_invalid_json',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = Array.isArray(payloadJson.data) ? payloadJson.data : []
|
|
||||||
|
|
||||||
return {
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
total: Number(payloadJson.count || items.length || 0),
|
|
||||||
items,
|
|
||||||
raw: payloadJson,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePage(value) {
|
|
||||||
const parsed = Number(value)
|
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLimit(value) {
|
|
||||||
const parsed = Number(value)
|
|
||||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
||||||
return 50
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.min(parsed, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithTimeout(input, init, timeoutMs) {
|
|
||||||
const controller = new AbortController()
|
|
||||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await fetch(input, {
|
|
||||||
...init,
|
|
||||||
signal: controller.signal,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
|
||||||
throw createHttpError(`khhao 订单查询超时(${timeoutMs}ms)`, {
|
|
||||||
statusCode: 504,
|
|
||||||
errorCode: 'khhao_query_timeout',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryParseJson(text) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(text)
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { upsertOrderFromSource } from '../../order/order-service.js'
|
|
||||||
import {
|
|
||||||
mapKhhaoOrderPreview,
|
|
||||||
mapKhhaoOrderToSourceEvent,
|
|
||||||
resolveKhhaoOrderStatus,
|
|
||||||
resolveKhhaoPayStatus,
|
|
||||||
} from './order-mapper-service.js'
|
|
||||||
import { queryKhhaoOrderList } from './order-query-service.js'
|
|
||||||
import { loginKhhaoSession } from './session-service.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {{
|
|
||||||
* baseUrl?: string
|
|
||||||
* username?: string
|
|
||||||
* password?: string
|
|
||||||
* page?: number | string
|
|
||||||
* limit?: number | string
|
|
||||||
* pages?: number | string
|
|
||||||
* maxCaptchaAttempts?: number | string
|
|
||||||
* createdAfter?: string
|
|
||||||
* }} [payload]
|
|
||||||
*/
|
|
||||||
export async function syncKhhaoOrders(payload = {}) {
|
|
||||||
const session = await loginKhhaoSession({
|
|
||||||
baseUrl: payload.baseUrl,
|
|
||||||
username: payload.username,
|
|
||||||
password: payload.password,
|
|
||||||
maxCaptchaAttempts: payload.maxCaptchaAttempts,
|
|
||||||
requestId: `admin-khhao-sync-orders:${String(payload.username || '').trim() || 'anonymous'}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
const queryResult = await queryKhhaoOrderList({
|
|
||||||
baseUrl: payload.baseUrl,
|
|
||||||
page: 1,
|
|
||||||
limit: resolvePageSize(payload.limit),
|
|
||||||
session,
|
|
||||||
})
|
|
||||||
|
|
||||||
const createdAfter = String(payload.createdAfter || '').trim()
|
|
||||||
const pages = Math.max(1, Math.min(Number(payload.pages || payload.page || 1) || 1, 5))
|
|
||||||
const results = []
|
|
||||||
const allItems = [...queryResult.items]
|
|
||||||
|
|
||||||
for (let page = 2; page <= pages; page += 1) {
|
|
||||||
const nextPage = await queryKhhaoOrderList({
|
|
||||||
baseUrl: payload.baseUrl,
|
|
||||||
page,
|
|
||||||
limit: resolvePageSize(payload.limit),
|
|
||||||
session,
|
|
||||||
})
|
|
||||||
allItems.push(...nextPage.items)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of allItems) {
|
|
||||||
const preview = mapKhhaoOrderPreview(item)
|
|
||||||
|
|
||||||
if (createdAfter && !isKhhaoOrderAfter(preview.orderCreatedAt, createdAfter)) {
|
|
||||||
results.push({
|
|
||||||
platformOrderId: preview.platformOrderId,
|
|
||||||
provider: 'khhao',
|
|
||||||
platform: preview.platform || 'unknown',
|
|
||||||
shopId: preview.shopId,
|
|
||||||
shopName: preview.shopName,
|
|
||||||
skuCode: preview.skuCode,
|
|
||||||
itemTitle: preview.itemTitle,
|
|
||||||
orderStatus: resolvePreviewOrderStatus(preview),
|
|
||||||
payStatus: resolvePreviewPayStatus(preview),
|
|
||||||
rawOrderCreatedAt: preview.orderCreatedAt,
|
|
||||||
ignored: true,
|
|
||||||
ignoreReason: 'before_sync_baseline',
|
|
||||||
orderId: null,
|
|
||||||
orderItemCount: 0,
|
|
||||||
taskCount: 0,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceEvent = mapKhhaoOrderToSourceEvent(item)
|
|
||||||
const upsertResult = await upsertOrderFromSource(sourceEvent, { sourceLabel: 'khhao-sync' })
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
platformOrderId: preview.platformOrderId,
|
|
||||||
provider: sourceEvent.provider,
|
|
||||||
platform: sourceEvent.platform,
|
|
||||||
shopId: sourceEvent.shopId,
|
|
||||||
shopName: sourceEvent.shopName,
|
|
||||||
skuCode: preview.skuCode,
|
|
||||||
itemTitle: preview.itemTitle,
|
|
||||||
orderStatus: sourceEvent.orderStatus,
|
|
||||||
payStatus: sourceEvent.payStatus,
|
|
||||||
rawOrderCreatedAt: preview.orderCreatedAt,
|
|
||||||
ignored: Boolean(upsertResult.ignored),
|
|
||||||
ignoreReason: String(upsertResult.ignoreReason || '').trim(),
|
|
||||||
orderId: Number(upsertResult.order?.id || 0) || null,
|
|
||||||
orderItemCount: Array.isArray(upsertResult.orderItems) ? upsertResult.orderItems.length : 0,
|
|
||||||
taskCount: Array.isArray(upsertResult.tasks) ? upsertResult.tasks.length : 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: session.baseUrl,
|
|
||||||
page: 1,
|
|
||||||
limit: resolvePageSize(payload.limit),
|
|
||||||
total: queryResult.total,
|
|
||||||
fetchedCount: allItems.length,
|
|
||||||
syncedCount: results.filter((item) => !item.ignored && item.orderId).length,
|
|
||||||
ignoredCount: results.filter((item) => item.ignored).length,
|
|
||||||
results,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvePageSize(value) {
|
|
||||||
const parsed = Number(value)
|
|
||||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
||||||
return 20
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.min(parsed, 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isKhhaoOrderAfter(orderCreatedAt, baselineIso) {
|
|
||||||
const orderTime = parseKhhaoOrderCreatedAt(orderCreatedAt)
|
|
||||||
const baselineTime = Date.parse(String(baselineIso || '').trim())
|
|
||||||
|
|
||||||
if (!Number.isFinite(orderTime) || !Number.isFinite(baselineTime)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return orderTime > baselineTime
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseKhhaoOrderCreatedAt(value) {
|
|
||||||
const text = String(value || '').trim()
|
|
||||||
if (!text) {
|
|
||||||
return NaN
|
|
||||||
}
|
|
||||||
|
|
||||||
const isoText = text.replace(' ', 'T')
|
|
||||||
return Date.parse(`${isoText}+08:00`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvePreviewOrderStatus(preview) {
|
|
||||||
return resolveKhhaoOrderStatus(preview?.status)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvePreviewPayStatus(preview) {
|
|
||||||
return resolveKhhaoPayStatus(preview?.status)
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
|
||||||
import { logInfo, logWarn } from '../../../utils/logger.js'
|
|
||||||
import { fetchKhhaoCaptcha } from './captcha-service.js'
|
|
||||||
import {
|
|
||||||
buildCookieHeader,
|
|
||||||
buildKhhaoUrl,
|
|
||||||
normalizeKhhaoCookieState,
|
|
||||||
resolveKhhaoConfig,
|
|
||||||
} from './shared.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {{
|
|
||||||
* username?: string
|
|
||||||
* password?: string
|
|
||||||
* baseUrl?: string
|
|
||||||
* timeoutMs?: number
|
|
||||||
* maxCaptchaAttempts?: number | string
|
|
||||||
* includeImageBase64?: boolean
|
|
||||||
* requestId?: string
|
|
||||||
* }} [payload]
|
|
||||||
*/
|
|
||||||
export async function loginKhhaoSession(payload = {}) {
|
|
||||||
const username = String(payload.username || '').trim()
|
|
||||||
const password = String(payload.password || '').trim()
|
|
||||||
const requestId = String(payload.requestId || '').trim()
|
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
throw createHttpError('khhao 登录缺少账号', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'khhao_login_missing_username',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password) {
|
|
||||||
throw createHttpError('khhao 登录缺少密码', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'khhao_login_missing_password',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = resolveKhhaoConfig(payload)
|
|
||||||
const maxCaptchaAttempts = normalizeAttempts(payload.maxCaptchaAttempts)
|
|
||||||
/** @type {Record<string, string>} */
|
|
||||||
let cookieMap = {}
|
|
||||||
let lastFailureMessage = ''
|
|
||||||
|
|
||||||
cookieMap = await openKhhaoLoginPage(config, cookieMap)
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxCaptchaAttempts; attempt += 1) {
|
|
||||||
const captcha = await fetchKhhaoCaptcha({
|
|
||||||
...config,
|
|
||||||
cookies: cookieMap,
|
|
||||||
requestId: requestId ? `${requestId}-captcha-${attempt}` : `login-${attempt}`,
|
|
||||||
includeImageBase64: Boolean(payload.includeImageBase64),
|
|
||||||
})
|
|
||||||
cookieMap = captcha.cookieMap
|
|
||||||
|
|
||||||
const result = await submitKhhaoLogin({
|
|
||||||
config,
|
|
||||||
cookieMap,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
captchaText: normalizeKhhaoCaptchaText(captcha.captchaText),
|
|
||||||
requestId,
|
|
||||||
})
|
|
||||||
|
|
||||||
cookieMap = result.cookieMap
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
logInfo('[khhao/session]', 'khhao 登录成功', {
|
|
||||||
requestId,
|
|
||||||
username,
|
|
||||||
attempt,
|
|
||||||
cookieKeys: Object.keys(cookieMap),
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: config.baseUrl,
|
|
||||||
cookieMap,
|
|
||||||
cookieHeader: buildCookieHeader(cookieMap),
|
|
||||||
loggedInAt: new Date().toISOString(),
|
|
||||||
username,
|
|
||||||
attempt,
|
|
||||||
captchaText: captcha.captchaText,
|
|
||||||
captchaImageBase64: captcha.imageBase64,
|
|
||||||
responseMessage: result.message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastFailureMessage = result.message
|
|
||||||
logWarn('[khhao/session]', 'khhao 登录失败', {
|
|
||||||
requestId,
|
|
||||||
username,
|
|
||||||
attempt,
|
|
||||||
message: result.message,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!shouldRetryLogin(result.message) || attempt >= maxCaptchaAttempts) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw createHttpError(lastFailureMessage || 'khhao 登录失败', {
|
|
||||||
statusCode: 401,
|
|
||||||
errorCode: 'khhao_login_failed',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openKhhaoLoginPage(config, currentCookies) {
|
|
||||||
const response = await fetchWithTimeout(buildKhhaoUrl(config.baseUrl, config.loginPath), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
||||||
},
|
|
||||||
}, config.timeoutMs)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw createHttpError(`khhao 登录页打开失败,HTTP ${response.status}`, {
|
|
||||||
statusCode: 502,
|
|
||||||
errorCode: 'khhao_login_page_failed',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizeKhhaoCookieState(currentCookies, response.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitKhhaoLogin({ config, cookieMap, username, password, captchaText, requestId }) {
|
|
||||||
const response = await fetchWithTimeout(buildKhhaoUrl(config.baseUrl, config.loginPath), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json, text/javascript, */*; q=0.01',
|
|
||||||
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
||||||
origin: config.baseUrl,
|
|
||||||
referer: buildKhhaoUrl(config.baseUrl, config.loginPath).toString(),
|
|
||||||
'x-requested-with': 'XMLHttpRequest',
|
|
||||||
...(buildCookieHeader(cookieMap) ? { cookie: buildCookieHeader(cookieMap) } : {}),
|
|
||||||
},
|
|
||||||
body: new URLSearchParams({
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
img_code: captchaText,
|
|
||||||
}).toString(),
|
|
||||||
}, config.timeoutMs)
|
|
||||||
|
|
||||||
const rawText = await response.text()
|
|
||||||
const cookieState = normalizeKhhaoCookieState(cookieMap, response.headers)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw createHttpError(`khhao 登录请求失败,HTTP ${response.status}`, {
|
|
||||||
statusCode: 502,
|
|
||||||
errorCode: 'khhao_login_request_failed',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = tryParseJson(rawText)
|
|
||||||
const errcode = Number(payload?.errcode ?? 1)
|
|
||||||
const message = String(payload?.msg || '').trim() || 'khhao 登录失败'
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: errcode === 0,
|
|
||||||
message,
|
|
||||||
payload,
|
|
||||||
cookieMap: cookieState,
|
|
||||||
requestId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldRetryLogin(message) {
|
|
||||||
const normalized = String(message || '').trim()
|
|
||||||
return normalized.includes('验证码')
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeAttempts(value) {
|
|
||||||
const parsed = Number(value)
|
|
||||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.min(parsed, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeKhhaoCaptchaText(value) {
|
|
||||||
return String(value || '').trim().toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithTimeout(input, init, timeoutMs) {
|
|
||||||
const controller = new AbortController()
|
|
||||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await fetch(input, {
|
|
||||||
...init,
|
|
||||||
signal: controller.signal,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
|
||||||
throw createHttpError(`khhao 请求超时(${timeoutMs}ms)`, {
|
|
||||||
statusCode: 504,
|
|
||||||
errorCode: 'khhao_request_timeout',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryParseJson(text) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(text)
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { runtimeConfig } from '../../../config/runtime.js'
|
|
||||||
|
|
||||||
/** @typedef {import('../../../types/runtime-config.js').RuntimeConfig} RuntimeConfig */
|
|
||||||
|
|
||||||
export function resolveKhhaoConfig(overrides = {}) {
|
|
||||||
/** @type {RuntimeConfig['platforms']['khhao']} */
|
|
||||||
const baseConfig = runtimeConfig.platforms?.khhao || {
|
|
||||||
baseUrl: '',
|
|
||||||
timeoutMs: 5000,
|
|
||||||
loginPath: '/c/login/index.php',
|
|
||||||
captchaPath: '/verify_img.php',
|
|
||||||
orderListPath: '/c/payOrder/get.php',
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: normalizeBaseUrl(overrides.baseUrl || baseConfig.baseUrl),
|
|
||||||
timeoutMs: normalizePositiveInteger(overrides.timeoutMs || baseConfig.timeoutMs, 5000),
|
|
||||||
loginPath: normalizePath(overrides.loginPath || baseConfig.loginPath, '/c/login/index.php'),
|
|
||||||
captchaPath: normalizePath(overrides.captchaPath || baseConfig.captchaPath, '/verify_img.php'),
|
|
||||||
orderListPath: normalizePath(overrides.orderListPath || baseConfig.orderListPath, '/c/payOrder/get.php'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildKhhaoUrl(baseUrl, pathname, searchParams = null) {
|
|
||||||
const url = new URL(normalizePath(pathname, '/'), normalizeBaseUrl(baseUrl) || 'https://admin.khhao.com')
|
|
||||||
|
|
||||||
if (searchParams && typeof searchParams === 'object') {
|
|
||||||
for (const [key, value] of Object.entries(searchParams)) {
|
|
||||||
if (typeof value === 'undefined' || value === null || value === '') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
url.searchParams.set(key, String(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
export function collectSetCookieHeaders(headers) {
|
|
||||||
if (!headers || typeof headers !== 'object') {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof headers.getSetCookie === 'function') {
|
|
||||||
return headers.getSetCookie()
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = headers.get('set-cookie')
|
|
||||||
return raw ? [raw] : []
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergeCookies(currentCookies = {}, setCookieHeaders = []) {
|
|
||||||
const next = { ...currentCookies }
|
|
||||||
|
|
||||||
for (const header of Array.isArray(setCookieHeaders) ? setCookieHeaders : []) {
|
|
||||||
const text = String(header || '').trim()
|
|
||||||
if (!text) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const pair = text.split(';', 1)[0]
|
|
||||||
const separatorIndex = pair.indexOf('=')
|
|
||||||
if (separatorIndex <= 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = pair.slice(0, separatorIndex).trim()
|
|
||||||
const value = pair.slice(separatorIndex + 1).trim()
|
|
||||||
|
|
||||||
if (!key) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
next[key] = value
|
|
||||||
} else {
|
|
||||||
delete next[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildCookieHeader(cookieMap = {}) {
|
|
||||||
return Object.entries(cookieMap)
|
|
||||||
.filter(([key, value]) => String(key || '').trim() && String(value || '').trim())
|
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
|
||||||
.join('; ')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeKhhaoCookieState(currentCookies = {}, headers) {
|
|
||||||
return mergeCookies(currentCookies, collectSetCookieHeaders(headers))
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeBaseUrl(value) {
|
|
||||||
return String(value || '').trim().replace(/\/+$/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePath(value, fallback) {
|
|
||||||
const normalized = String(value || '').trim()
|
|
||||||
if (!normalized) {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized.startsWith('/') ? normalized : `/${normalized}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePositiveInteger(value, fallback) {
|
|
||||||
const parsed = Number(value)
|
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
|
||||||
|
|
||||||
const KHHAO_SOURCES_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'khhao-sources.json')
|
|
||||||
|
|
||||||
export function getKhhaoSourcesFilePath() {
|
|
||||||
return KHHAO_SOURCES_FILE_PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getKhhaoSourceConfig() {
|
|
||||||
return loadKhhaoSourceConfigFromFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveKhhaoSourceConfig(rawValue) {
|
|
||||||
const normalized = normalizeKhhaoSourceConfig(rawValue)
|
|
||||||
fs.mkdirSync(path.dirname(KHHAO_SOURCES_FILE_PATH), { recursive: true })
|
|
||||||
fs.writeFileSync(KHHAO_SOURCES_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadKhhaoSourceConfigFromFile() {
|
|
||||||
if (!fs.existsSync(KHHAO_SOURCES_FILE_PATH)) {
|
|
||||||
return createDefaultKhhaoSourceConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rawText = fs.readFileSync(KHHAO_SOURCES_FILE_PATH, 'utf8')
|
|
||||||
return normalizeKhhaoSourceConfig(JSON.parse(rawText))
|
|
||||||
} catch {
|
|
||||||
return createDefaultKhhaoSourceConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeKhhaoSourceConfig(rawValue) {
|
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
|
|
||||||
baseUrl: String(source.baseUrl || 'https://admin.khhao.com').trim() || 'https://admin.khhao.com',
|
|
||||||
username: String(source.username || '').trim(),
|
|
||||||
password: String(source.password || '').trim(),
|
|
||||||
maxCaptchaAttempts: normalizePositiveInteger(source.maxCaptchaAttempts, 3),
|
|
||||||
autoSync: normalizeAutoSyncConfig(source.autoSync),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDefaultKhhaoSourceConfig() {
|
|
||||||
return {
|
|
||||||
enabled: true,
|
|
||||||
baseUrl: 'https://admin.khhao.com',
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
maxCaptchaAttempts: 3,
|
|
||||||
autoSync: normalizeAutoSyncConfig({}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeAutoSyncConfig(rawValue) {
|
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : false,
|
|
||||||
intervalMinutes: normalizePositiveInteger(source.intervalMinutes, 3),
|
|
||||||
pages: normalizePositiveInteger(source.pages, 2),
|
|
||||||
pageSize: normalizePositiveInteger(source.pageSize, 20),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePositiveInteger(value, fallback) {
|
|
||||||
const parsed = Number(value)
|
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainObject(value) {
|
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
|
||||||
import { nowIso } from '../../../utils/time.js'
|
|
||||||
|
|
||||||
const KHHAO_SYNC_STATE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'khhao-sync-state.json')
|
|
||||||
|
|
||||||
export function getKhhaoSyncStateFilePath() {
|
|
||||||
return KHHAO_SYNC_STATE_FILE_PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getKhhaoSyncState() {
|
|
||||||
return loadKhhaoSyncStateFromFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveKhhaoSyncState(rawValue) {
|
|
||||||
const normalized = normalizeKhhaoSyncState(rawValue)
|
|
||||||
fs.mkdirSync(path.dirname(KHHAO_SYNC_STATE_FILE_PATH), { recursive: true })
|
|
||||||
fs.writeFileSync(KHHAO_SYNC_STATE_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ensureKhhaoSyncBaseline() {
|
|
||||||
const current = getKhhaoSyncState()
|
|
||||||
if (String(current.syncFromCreatedAt || '').trim()) {
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
return saveKhhaoSyncState({
|
|
||||||
...current,
|
|
||||||
syncFromCreatedAt: nowIso(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadKhhaoSyncStateFromFile() {
|
|
||||||
if (!fs.existsSync(KHHAO_SYNC_STATE_FILE_PATH)) {
|
|
||||||
return createDefaultKhhaoSyncState()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return normalizeKhhaoSyncState(JSON.parse(fs.readFileSync(KHHAO_SYNC_STATE_FILE_PATH, 'utf8')))
|
|
||||||
} catch {
|
|
||||||
return createDefaultKhhaoSyncState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDefaultKhhaoSyncState() {
|
|
||||||
return {
|
|
||||||
syncFromCreatedAt: '',
|
|
||||||
lastRunStartedAt: '',
|
|
||||||
lastRunFinishedAt: '',
|
|
||||||
lastRunStatus: 'idle',
|
|
||||||
lastErrorMessage: '',
|
|
||||||
fetchedCount: 0,
|
|
||||||
syncedCount: 0,
|
|
||||||
ignoredCount: 0,
|
|
||||||
lastOrderCreatedAt: '',
|
|
||||||
watchMode: 'normal',
|
|
||||||
watchOrders: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeKhhaoSyncState(rawValue) {
|
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
syncFromCreatedAt: String(source.syncFromCreatedAt || '').trim(),
|
|
||||||
lastRunStartedAt: String(source.lastRunStartedAt || '').trim(),
|
|
||||||
lastRunFinishedAt: String(source.lastRunFinishedAt || '').trim(),
|
|
||||||
lastRunStatus: normalizeStatus(source.lastRunStatus),
|
|
||||||
lastErrorMessage: String(source.lastErrorMessage || '').trim(),
|
|
||||||
fetchedCount: normalizeNonNegativeInteger(source.fetchedCount),
|
|
||||||
syncedCount: normalizeNonNegativeInteger(source.syncedCount),
|
|
||||||
ignoredCount: normalizeNonNegativeInteger(source.ignoredCount),
|
|
||||||
lastOrderCreatedAt: String(source.lastOrderCreatedAt || '').trim(),
|
|
||||||
watchMode: normalizeWatchMode(source.watchMode),
|
|
||||||
watchOrders: normalizeWatchOrders(source.watchOrders),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeStatus(value) {
|
|
||||||
const normalized = String(value || '').trim()
|
|
||||||
return normalized || 'idle'
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeNonNegativeInteger(value) {
|
|
||||||
const parsed = Number(value)
|
|
||||||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeWatchMode(value) {
|
|
||||||
return String(value || '').trim() === 'boosted' ? 'boosted' : 'normal'
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeWatchOrders(value) {
|
|
||||||
return (Array.isArray(value) ? value : [])
|
|
||||||
.map((item) => normalizeWatchOrder(item))
|
|
||||||
.filter((item) => item.platformOrderId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeWatchOrder(value) {
|
|
||||||
const source = isPlainObject(value) ? value : {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
platformOrderId: String(source.platformOrderId || '').trim(),
|
|
||||||
shopId: String(source.shopId || '').trim(),
|
|
||||||
shopName: String(source.shopName || '').trim(),
|
|
||||||
itemTitle: String(source.itemTitle || '').trim(),
|
|
||||||
payStatus: normalizeWatchPayStatus(source.payStatus),
|
|
||||||
detectedAt: String(source.detectedAt || '').trim(),
|
|
||||||
lastSeenAt: String(source.lastSeenAt || '').trim(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeWatchPayStatus(value) {
|
|
||||||
const normalized = String(value || '').trim().toLowerCase()
|
|
||||||
if (normalized === 'paid') {
|
|
||||||
return 'paid'
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized === 'unpaid' ? 'unpaid' : 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainObject(value) {
|
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
|
||||||
}
|
|
||||||
@@ -30,10 +30,6 @@ export {}
|
|||||||
* @typedef {import('./admin-read-inputs.js').AdminWebhookEventListQueryInput} AdminWebhookEventRouteQuery
|
* @typedef {import('./admin-read-inputs.js').AdminWebhookEventListQueryInput} AdminWebhookEventRouteQuery
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('./admin-write-inputs.js').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigRouteBody
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./admin-write-inputs.js').AdminKuaishouEticketSourceConfigInput} AdminKuaishouEticketSourceConfigRouteBody
|
* @typedef {import('./admin-write-inputs.js').AdminKuaishouEticketSourceConfigInput} AdminKuaishouEticketSourceConfigRouteBody
|
||||||
*/
|
*/
|
||||||
@@ -54,14 +50,6 @@ export {}
|
|||||||
* @typedef {import('./admin-write-inputs.js').AdminKuaishouEticketShopInfoInput} AdminKuaishouEticketShopInfoRouteBody
|
* @typedef {import('./admin-write-inputs.js').AdminKuaishouEticketShopInfoInput} AdminKuaishouEticketShopInfoRouteBody
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('./admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginRouteBody
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('./admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryRouteBody
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigRouteBody
|
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigRouteBody
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export {}
|
|||||||
* provider?: string
|
* provider?: string
|
||||||
* platform?: string
|
* platform?: string
|
||||||
* shopId?: string
|
* shopId?: string
|
||||||
* khhaoShopId?: string
|
|
||||||
* internalSkuCode?: string
|
* internalSkuCode?: string
|
||||||
* internalSkuName?: string
|
* internalSkuName?: string
|
||||||
* externalSkuCode?: string
|
* externalSkuCode?: string
|
||||||
@@ -103,22 +102,6 @@ export {}
|
|||||||
* }} AdminKuaishouCloudFulfillmentConfigInput
|
* }} AdminKuaishouCloudFulfillmentConfigInput
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {{
|
|
||||||
* enabled?: boolean
|
|
||||||
* baseUrl?: string
|
|
||||||
* username?: string
|
|
||||||
* password?: string
|
|
||||||
* maxCaptchaAttempts?: number | string
|
|
||||||
* autoSync?: {
|
|
||||||
* enabled?: boolean
|
|
||||||
* intervalMinutes?: number | string
|
|
||||||
* pages?: number | string
|
|
||||||
* pageSize?: number | string
|
|
||||||
* }
|
|
||||||
* }} AdminKhhaoSourceConfigInput
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* shopId?: string
|
* shopId?: string
|
||||||
@@ -167,27 +150,6 @@ export {}
|
|||||||
* }} AdminKuaishouEticketConsumeInput
|
* }} AdminKuaishouEticketConsumeInput
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {{
|
|
||||||
* baseUrl?: string
|
|
||||||
* username?: string
|
|
||||||
* password?: string
|
|
||||||
* maxCaptchaAttempts?: number | string
|
|
||||||
* includeImageBase64?: boolean
|
|
||||||
* }} AdminKhhaoTestLoginInput
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {{
|
|
||||||
* baseUrl?: string
|
|
||||||
* username?: string
|
|
||||||
* password?: string
|
|
||||||
* page?: number | string
|
|
||||||
* limit?: number | string
|
|
||||||
* maxCaptchaAttempts?: number | string
|
|
||||||
* }} AdminKhhaoOrderQueryInput
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* enabled?: boolean
|
* enabled?: boolean
|
||||||
@@ -303,7 +265,6 @@ export {}
|
|||||||
* platform?: string
|
* platform?: string
|
||||||
* shopId?: string
|
* shopId?: string
|
||||||
* shopName?: string
|
* shopName?: string
|
||||||
* khhaoShopId?: string
|
|
||||||
* skuCode?: string
|
* skuCode?: string
|
||||||
* skuName?: string
|
* skuName?: string
|
||||||
* profileKey?: string
|
* profileKey?: string
|
||||||
|
|||||||
@@ -98,13 +98,6 @@ export {}
|
|||||||
* timestampToleranceSeconds: number
|
* timestampToleranceSeconds: number
|
||||||
* cardsEncoding: string
|
* cardsEncoding: string
|
||||||
* }
|
* }
|
||||||
* khhao: {
|
|
||||||
* baseUrl: string
|
|
||||||
* timeoutMs: number
|
|
||||||
* loginPath: string
|
|
||||||
* captchaPath: string
|
|
||||||
* orderListPath: string
|
|
||||||
* }
|
|
||||||
* cloudtentacles: {
|
* cloudtentacles: {
|
||||||
* baseUrl: string
|
* baseUrl: string
|
||||||
* timeoutMs: number
|
* timeoutMs: number
|
||||||
|
|||||||
Vendored
-1
@@ -14,7 +14,6 @@ declare module 'vue' {
|
|||||||
AdminPaginationBar: typeof import('./components/admin/AdminPaginationBar.vue')['default']
|
AdminPaginationBar: typeof import('./components/admin/AdminPaginationBar.vue')['default']
|
||||||
AdminPlatformAgisoSection: typeof import('./components/admin/AdminPlatformAgisoSection.vue')['default']
|
AdminPlatformAgisoSection: typeof import('./components/admin/AdminPlatformAgisoSection.vue')['default']
|
||||||
AdminPlatformCloudtentaclesSection: typeof import('./components/admin/AdminPlatformCloudtentaclesSection.vue')['default']
|
AdminPlatformCloudtentaclesSection: typeof import('./components/admin/AdminPlatformCloudtentaclesSection.vue')['default']
|
||||||
AdminPlatformKhhaoSection: typeof import('./components/admin/AdminPlatformKhhaoSection.vue')['default']
|
|
||||||
AdminPlatformKuaishouEticketSection: typeof import('./components/admin/AdminPlatformKuaishouEticketSection.vue')['default']
|
AdminPlatformKuaishouEticketSection: typeof import('./components/admin/AdminPlatformKuaishouEticketSection.vue')['default']
|
||||||
AdminPlatformNinetyoneSection: typeof import('./components/admin/AdminPlatformNinetyoneSection.vue')['default']
|
AdminPlatformNinetyoneSection: typeof import('./components/admin/AdminPlatformNinetyoneSection.vue')['default']
|
||||||
AdminResultCard: typeof import('./components/admin/AdminResultCard.vue')['default']
|
AdminResultCard: typeof import('./components/admin/AdminResultCard.vue')['default']
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ defineProps<Props>()
|
|||||||
</div>
|
</div>
|
||||||
<div class="meta-line">
|
<div class="meta-line">
|
||||||
<span class="meta-label">职责</span>
|
<span class="meta-label">职责</span>
|
||||||
<span>管理 Agiso 默认消息模板、店铺 AccessToken 以及店铺级模板覆盖。该区域不处理 khhao 登录与拉单。</span>
|
<span>管理 Agiso 默认消息模板、店铺 AccessToken 以及店铺级模板覆盖。</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,264 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import AdminResultCard from '@/components/admin/AdminResultCard.vue'
|
|
||||||
import type {
|
|
||||||
AdminKhhaoLoginTestResult,
|
|
||||||
AdminKhhaoOrderQueryResult,
|
|
||||||
AdminKhhaoOrderSyncResult,
|
|
||||||
AdminKhhaoSyncState,
|
|
||||||
} from '@/types/admin'
|
|
||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
|
||||||
|
|
||||||
type KhhaoForm = {
|
|
||||||
baseUrl: string
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
page: number
|
|
||||||
limit: number
|
|
||||||
maxCaptchaAttempts: number
|
|
||||||
autoSyncEnabled: boolean
|
|
||||||
autoSyncIntervalMinutes: number
|
|
||||||
autoSyncPages: number
|
|
||||||
autoSyncPageSize: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
khhaoFilePath: string
|
|
||||||
khhaoStateFilePath: string
|
|
||||||
khhaoForm: KhhaoForm
|
|
||||||
khhaoSaving: boolean
|
|
||||||
khhaoTesting: boolean
|
|
||||||
khhaoQuerying: boolean
|
|
||||||
khhaoSyncing: boolean
|
|
||||||
khhaoResultError: string
|
|
||||||
khhaoLoginResult: AdminKhhaoLoginTestResult | null
|
|
||||||
khhaoQueryResult: AdminKhhaoOrderQueryResult | null
|
|
||||||
khhaoSyncResult: AdminKhhaoOrderSyncResult | null
|
|
||||||
khhaoAutoSyncState: AdminKhhaoSyncState | null
|
|
||||||
handleKhhaoSaveSource: () => void | Promise<void>
|
|
||||||
handleKhhaoTestLogin: () => void | Promise<void>
|
|
||||||
handleKhhaoQueryOrders: () => void | Promise<void>
|
|
||||||
handleKhhaoSyncOrders: () => void | Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section class="meta-card">
|
|
||||||
<div class="meta-line">
|
|
||||||
<span class="meta-label">配置文件</span>
|
|
||||||
<code>{{ khhaoFilePath || '-' }}</code>
|
|
||||||
</div>
|
|
||||||
<div class="meta-line">
|
|
||||||
<span class="meta-label">状态文件</span>
|
|
||||||
<code>{{ khhaoStateFilePath || '-' }}</code>
|
|
||||||
</div>
|
|
||||||
<div class="meta-line">
|
|
||||||
<span class="meta-label">职责</span>
|
|
||||||
<span>管理 khhao 的来源地址、登录账号、密码与验证码重试次数,并执行测试登录、查询订单与手动同步。</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="table-card">
|
|
||||||
<div class="section-title-row">
|
|
||||||
<div>
|
|
||||||
<h3>khhao 来源配置</h3>
|
|
||||||
<p>先保存 khhao 凭据,再按需执行登录测试、订单查询或同步。履约规则请前往“履约配置”页处理。</p>
|
|
||||||
</div>
|
|
||||||
<div class="section-actions">
|
|
||||||
<el-button :loading="khhaoSaving" round @click="handleKhhaoSaveSource">保存来源配置</el-button>
|
|
||||||
<el-button :loading="khhaoTesting" round @click="handleKhhaoTestLogin">测试登录</el-button>
|
|
||||||
<el-button :loading="khhaoQuerying" round @click="handleKhhaoQueryOrders">查询订单</el-button>
|
|
||||||
<el-button :loading="khhaoSyncing" round type="primary" @click="handleKhhaoSyncOrders">同步订单</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-grid">
|
|
||||||
<label class="field-block field-wide">
|
|
||||||
<span>Base URL</span>
|
|
||||||
<input v-model="khhaoForm.baseUrl" class="text-input" placeholder="https://admin.khhao.com" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>账号</span>
|
|
||||||
<input v-model="khhaoForm.username" class="text-input" placeholder="khhao 登录账号" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>密码</span>
|
|
||||||
<input v-model="khhaoForm.password" class="text-input" type="password" placeholder="khhao 登录密码" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>页码</span>
|
|
||||||
<input v-model.number="khhaoForm.page" class="text-input" type="number" min="1" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>每页条数</span>
|
|
||||||
<input v-model.number="khhaoForm.limit" class="text-input" type="number" min="1" max="50" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>验证码重试次数</span>
|
|
||||||
<input v-model.number="khhaoForm.maxCaptchaAttempts" class="text-input" type="number" min="1" max="5" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>自动同步开关</span>
|
|
||||||
<label class="checkbox-line checkbox-box">
|
|
||||||
<input v-model="khhaoForm.autoSyncEnabled" type="checkbox" />
|
|
||||||
<span>启用自动同步</span>
|
|
||||||
</label>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>同步间隔(分钟)</span>
|
|
||||||
<input v-model.number="khhaoForm.autoSyncIntervalMinutes" class="text-input" type="number" min="1" max="60" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>同步页数</span>
|
|
||||||
<input v-model.number="khhaoForm.autoSyncPages" class="text-input" type="number" min="1" max="5" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>每页条数</span>
|
|
||||||
<input v-model.number="khhaoForm.autoSyncPageSize" class="text-input" type="number" min="1" max="50" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p v-if="khhaoResultError" class="error-copy">{{ khhaoResultError }}</p>
|
|
||||||
|
|
||||||
<div class="result-grid">
|
|
||||||
<AdminResultCard label="登录结果" :value="khhaoLoginResult ? '成功' : '未执行'">
|
|
||||||
<p v-if="khhaoLoginResult">
|
|
||||||
第 {{ khhaoLoginResult.attempt }} 次成功,验证码识别 {{ khhaoLoginResult.captcha.recognizedText || '-' }},
|
|
||||||
{{ formatAdminDateTime(khhaoLoginResult.loggedInAt) }}
|
|
||||||
</p>
|
|
||||||
<p v-else>可先点击“测试登录”验证账号密码与 OCR。</p>
|
|
||||||
</AdminResultCard>
|
|
||||||
|
|
||||||
<AdminResultCard label="查询结果" :value="`${khhaoQueryResult?.itemCount || 0} 条`">
|
|
||||||
<p v-if="khhaoQueryResult">当前页 {{ khhaoQueryResult.page }} / 总计 {{ khhaoQueryResult.total }} 条</p>
|
|
||||||
<p v-else>可先点击“查询订单”验证拉单链路。</p>
|
|
||||||
</AdminResultCard>
|
|
||||||
|
|
||||||
<AdminResultCard label="同步结果" :value="`${khhaoSyncResult?.syncedCount || 0} 条`">
|
|
||||||
<p v-if="khhaoSyncResult">忽略 {{ khhaoSyncResult.ignoredCount }} 条,拉取 {{ khhaoSyncResult.fetchedCount }} 条</p>
|
|
||||||
<p v-else>完成履约配置后可在这里手动同步。</p>
|
|
||||||
</AdminResultCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="khhaoAutoSyncState" class="meta-card compact-meta">
|
|
||||||
<div class="meta-line">
|
|
||||||
<span class="meta-label">自动同步状态</span>
|
|
||||||
<span>{{ khhaoAutoSyncState.lastRunStatus || 'idle' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="meta-line">
|
|
||||||
<span class="meta-label">当前节奏</span>
|
|
||||||
<span>{{ khhaoAutoSyncState.watchMode === 'boosted' ? '提频观察中(3-5 秒)' : '常规同步' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="meta-line">
|
|
||||||
<span class="meta-label">同步基线</span>
|
|
||||||
<span>{{ formatAdminDateTime(khhaoAutoSyncState.syncFromCreatedAt) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="meta-line">
|
|
||||||
<span class="meta-label">最近开始</span>
|
|
||||||
<span>{{ formatAdminDateTime(khhaoAutoSyncState.lastRunStartedAt) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="meta-line">
|
|
||||||
<span class="meta-label">最近完成</span>
|
|
||||||
<span>{{ formatAdminDateTime(khhaoAutoSyncState.lastRunFinishedAt) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="meta-line">
|
|
||||||
<span class="meta-label">最近结果</span>
|
|
||||||
<span>拉取 {{ khhaoAutoSyncState.fetchedCount }} 条,成功 {{ khhaoAutoSyncState.syncedCount }} 条,忽略 {{ khhaoAutoSyncState.ignoredCount }} 条</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="khhaoAutoSyncState.watchOrders.length > 0" class="meta-line">
|
|
||||||
<span class="meta-label">观察订单</span>
|
|
||||||
<span>{{ khhaoAutoSyncState.watchOrders.map((item) => `${item.shopName || item.shopId || '-'} / ${item.platformOrderId}`).join(';') }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="khhaoAutoSyncState.lastErrorMessage" class="meta-line">
|
|
||||||
<span class="meta-label">最近错误</span>
|
|
||||||
<span class="error-copy">{{ khhaoAutoSyncState.lastErrorMessage }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="khhaoQueryResult" class="table-card">
|
|
||||||
<div class="section-title-row">
|
|
||||||
<div>
|
|
||||||
<h3>khhao 查询结果</h3>
|
|
||||||
<p>当前页 {{ khhaoQueryResult.page }},本页 {{ khhaoQueryResult.itemCount }} 条,总数 {{ khhaoQueryResult.total }}。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>订单</th>
|
|
||||||
<th>店铺</th>
|
|
||||||
<th>商品</th>
|
|
||||||
<th>金额</th>
|
|
||||||
<th>状态</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="item in khhaoQueryResult.items" :key="`${item.platformOrderId}:${item.skuCode}:${item.itemId}`">
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ item.platformOrderId }}</strong>
|
|
||||||
<span class="cell-subtle">{{ item.platformLabel || item.platform || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ item.shopName || item.shopId || '-' }}</strong>
|
|
||||||
<span class="cell-subtle">ID: {{ item.shopId || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ item.itemTitle || '-' }}</strong>
|
|
||||||
<span class="cell-subtle">SKU: {{ item.skuCode || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{{ (item.totalAmountFen / 100).toFixed(2) }}</td>
|
|
||||||
<td>{{ item.statusLabel || item.status || '-' }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="khhaoSyncResult" class="table-card">
|
|
||||||
<div class="section-title-row">
|
|
||||||
<div>
|
|
||||||
<h3>khhao 同步结果</h3>
|
|
||||||
<p>拉取 {{ khhaoSyncResult.fetchedCount }} 条,成功 {{ khhaoSyncResult.syncedCount }} 条,忽略 {{ khhaoSyncResult.ignoredCount }} 条。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>订单</th>
|
|
||||||
<th>店铺</th>
|
|
||||||
<th>商品</th>
|
|
||||||
<th>结果</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="item in khhaoSyncResult.results" :key="`${item.platformOrderId}:${item.skuCode}`">
|
|
||||||
<td>{{ item.platformOrderId }}</td>
|
|
||||||
<td>{{ item.shopId || '-' }}</td>
|
|
||||||
<td>{{ item.itemTitle || item.skuCode || '-' }}</td>
|
|
||||||
<td>
|
|
||||||
<span v-if="item.ignored">已忽略:{{ item.ignoreReason || '-' }}</span>
|
|
||||||
<span v-else>已入库,订单 {{ item.orderId || '-' }},任务 {{ item.taskCount }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export type PlatformTab = 'agiso' | 'khhao' | 'ninetyone' | 'kuaishouEticket' | 'cloudtentacles'
|
export type PlatformTab = 'agiso' | 'ninetyone' | 'kuaishouEticket' | 'cloudtentacles'
|
||||||
|
|
||||||
export type EditableDefaults = {
|
export type EditableDefaults = {
|
||||||
messageTemplate: string
|
messageTemplate: string
|
||||||
|
|||||||
@@ -1,215 +0,0 @@
|
|||||||
import { computed, ref } from 'vue'
|
|
||||||
|
|
||||||
import { showError, showSuccess } from '@/lib/feedback'
|
|
||||||
import {
|
|
||||||
queryAdminKhhaoOrders,
|
|
||||||
saveAdminKhhaoSourceConfig,
|
|
||||||
syncAdminKhhaoOrders,
|
|
||||||
testAdminKhhaoLogin,
|
|
||||||
} from '@/services/admin'
|
|
||||||
import type {
|
|
||||||
AdminKhhaoLoginTestResult,
|
|
||||||
AdminKhhaoOrderQueryResult,
|
|
||||||
AdminKhhaoOrderSyncResult,
|
|
||||||
AdminKhhaoSyncState,
|
|
||||||
} from '@/types/admin'
|
|
||||||
|
|
||||||
export function useAdminKhhaoPlatform() {
|
|
||||||
const khhaoFilePath = ref('')
|
|
||||||
const khhaoStateFilePath = ref('')
|
|
||||||
const khhaoForm = ref({
|
|
||||||
baseUrl: 'https://admin.khhao.com',
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
page: 1,
|
|
||||||
limit: 10,
|
|
||||||
maxCaptchaAttempts: 3,
|
|
||||||
autoSyncEnabled: false,
|
|
||||||
autoSyncIntervalMinutes: 3,
|
|
||||||
autoSyncPages: 2,
|
|
||||||
autoSyncPageSize: 20,
|
|
||||||
})
|
|
||||||
const khhaoSaving = ref(false)
|
|
||||||
const khhaoTesting = ref(false)
|
|
||||||
const khhaoQuerying = ref(false)
|
|
||||||
const khhaoSyncing = ref(false)
|
|
||||||
const khhaoResultError = ref('')
|
|
||||||
const khhaoLoginResult = ref<AdminKhhaoLoginTestResult | null>(null)
|
|
||||||
const khhaoQueryResult = ref<AdminKhhaoOrderQueryResult | null>(null)
|
|
||||||
const khhaoSyncResult = ref<AdminKhhaoOrderSyncResult | null>(null)
|
|
||||||
const khhaoAutoSyncState = ref<AdminKhhaoSyncState | null>(null)
|
|
||||||
|
|
||||||
const khhaoStats = computed(() => ({
|
|
||||||
hasCredential: Boolean(khhaoForm.value.username.trim() && khhaoForm.value.password.trim()),
|
|
||||||
queryCount: khhaoQueryResult.value?.itemCount || 0,
|
|
||||||
syncedCount: khhaoAutoSyncState.value?.syncedCount || khhaoSyncResult.value?.syncedCount || 0,
|
|
||||||
ignoredCount: khhaoAutoSyncState.value?.ignoredCount || khhaoSyncResult.value?.ignoredCount || 0,
|
|
||||||
}))
|
|
||||||
|
|
||||||
function hydrateKhhaoConfig(data: {
|
|
||||||
filePath: string
|
|
||||||
stateFilePath: string
|
|
||||||
syncState: AdminKhhaoSyncState
|
|
||||||
source: {
|
|
||||||
baseUrl?: string
|
|
||||||
username?: string
|
|
||||||
password?: string
|
|
||||||
maxCaptchaAttempts?: number
|
|
||||||
autoSync?: {
|
|
||||||
enabled?: boolean
|
|
||||||
intervalMinutes?: number
|
|
||||||
pages?: number
|
|
||||||
pageSize?: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
khhaoFilePath.value = data.filePath
|
|
||||||
khhaoStateFilePath.value = data.stateFilePath
|
|
||||||
khhaoAutoSyncState.value = data.syncState
|
|
||||||
khhaoForm.value = {
|
|
||||||
baseUrl: data.source.baseUrl || 'https://admin.khhao.com',
|
|
||||||
username: data.source.username || '',
|
|
||||||
password: data.source.password || '',
|
|
||||||
page: 1,
|
|
||||||
limit: 10,
|
|
||||||
maxCaptchaAttempts: data.source.maxCaptchaAttempts || 3,
|
|
||||||
autoSyncEnabled: data.source.autoSync?.enabled === true,
|
|
||||||
autoSyncIntervalMinutes: data.source.autoSync?.intervalMinutes || 3,
|
|
||||||
autoSyncPages: data.source.autoSync?.pages || 2,
|
|
||||||
autoSyncPageSize: data.source.autoSync?.pageSize || 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildKhhaoPayload() {
|
|
||||||
return {
|
|
||||||
baseUrl: khhaoForm.value.baseUrl.trim() || 'https://admin.khhao.com',
|
|
||||||
username: khhaoForm.value.username.trim(),
|
|
||||||
password: khhaoForm.value.password.trim(),
|
|
||||||
page: Number(khhaoForm.value.page || 1),
|
|
||||||
limit: Number(khhaoForm.value.limit || 10),
|
|
||||||
maxCaptchaAttempts: Number(khhaoForm.value.maxCaptchaAttempts || 3),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureKhhaoCredentials() {
|
|
||||||
if (!khhaoForm.value.username.trim() || !khhaoForm.value.password.trim()) {
|
|
||||||
khhaoResultError.value = '请先填写 khhao 账号和密码'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleKhhaoSaveSource() {
|
|
||||||
khhaoSaving.value = true
|
|
||||||
khhaoResultError.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await saveAdminKhhaoSourceConfig({
|
|
||||||
enabled: true,
|
|
||||||
baseUrl: khhaoForm.value.baseUrl.trim() || 'https://admin.khhao.com',
|
|
||||||
username: khhaoForm.value.username.trim(),
|
|
||||||
password: khhaoForm.value.password.trim(),
|
|
||||||
maxCaptchaAttempts: Number(khhaoForm.value.maxCaptchaAttempts || 3),
|
|
||||||
autoSync: {
|
|
||||||
enabled: Boolean(khhaoForm.value.autoSyncEnabled),
|
|
||||||
intervalMinutes: Number(khhaoForm.value.autoSyncIntervalMinutes || 3),
|
|
||||||
pages: Number(khhaoForm.value.autoSyncPages || 2),
|
|
||||||
pageSize: Number(khhaoForm.value.autoSyncPageSize || 20),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
khhaoFilePath.value = response.data.filePath
|
|
||||||
khhaoStateFilePath.value = response.data.stateFilePath
|
|
||||||
khhaoAutoSyncState.value = response.data.syncState
|
|
||||||
showSuccess('khhao 来源配置已保存')
|
|
||||||
} catch (error) {
|
|
||||||
khhaoResultError.value = error instanceof Error ? error.message : 'khhao 来源配置保存失败'
|
|
||||||
showError(khhaoResultError.value)
|
|
||||||
} finally {
|
|
||||||
khhaoSaving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleKhhaoTestLogin() {
|
|
||||||
if (!ensureKhhaoCredentials()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
khhaoTesting.value = true
|
|
||||||
khhaoResultError.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await testAdminKhhaoLogin({
|
|
||||||
...buildKhhaoPayload(),
|
|
||||||
includeImageBase64: false,
|
|
||||||
})
|
|
||||||
khhaoLoginResult.value = response.data
|
|
||||||
showSuccess('khhao 登录测试成功')
|
|
||||||
} catch (error) {
|
|
||||||
khhaoResultError.value = error instanceof Error ? error.message : 'khhao 登录测试失败'
|
|
||||||
showError(khhaoResultError.value)
|
|
||||||
} finally {
|
|
||||||
khhaoTesting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleKhhaoQueryOrders() {
|
|
||||||
if (!ensureKhhaoCredentials()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
khhaoQuerying.value = true
|
|
||||||
khhaoResultError.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await queryAdminKhhaoOrders(buildKhhaoPayload())
|
|
||||||
khhaoQueryResult.value = response.data
|
|
||||||
showSuccess(`khhao 订单查询成功,共返回 ${response.data.itemCount} 条`)
|
|
||||||
} catch (error) {
|
|
||||||
khhaoResultError.value = error instanceof Error ? error.message : 'khhao 订单查询失败'
|
|
||||||
showError(khhaoResultError.value)
|
|
||||||
} finally {
|
|
||||||
khhaoQuerying.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleKhhaoSyncOrders() {
|
|
||||||
if (!ensureKhhaoCredentials()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
khhaoSyncing.value = true
|
|
||||||
khhaoResultError.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await syncAdminKhhaoOrders(buildKhhaoPayload())
|
|
||||||
khhaoSyncResult.value = response.data
|
|
||||||
showSuccess(`khhao 同步完成:拉取 ${response.data.fetchedCount} 条,成功 ${response.data.syncedCount} 条`)
|
|
||||||
} catch (error) {
|
|
||||||
khhaoResultError.value = error instanceof Error ? error.message : 'khhao 订单同步失败'
|
|
||||||
showError(khhaoResultError.value)
|
|
||||||
} finally {
|
|
||||||
khhaoSyncing.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
khhaoFilePath,
|
|
||||||
khhaoStateFilePath,
|
|
||||||
khhaoForm,
|
|
||||||
khhaoSaving,
|
|
||||||
khhaoTesting,
|
|
||||||
khhaoQuerying,
|
|
||||||
khhaoSyncing,
|
|
||||||
khhaoResultError,
|
|
||||||
khhaoLoginResult,
|
|
||||||
khhaoQueryResult,
|
|
||||||
khhaoSyncResult,
|
|
||||||
khhaoAutoSyncState,
|
|
||||||
khhaoStats,
|
|
||||||
hydrateKhhaoConfig,
|
|
||||||
handleKhhaoSaveSource,
|
|
||||||
handleKhhaoTestLogin,
|
|
||||||
handleKhhaoQueryOrders,
|
|
||||||
handleKhhaoSyncOrders,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,11 +11,6 @@ import type {
|
|||||||
AdminCloudtentaclesValidateSessionResult,
|
AdminCloudtentaclesValidateSessionResult,
|
||||||
AdminFulfillmentBindingConfigItem,
|
AdminFulfillmentBindingConfigItem,
|
||||||
AdminFulfillmentLookupResult,
|
AdminFulfillmentLookupResult,
|
||||||
AdminKhhaoLoginTestResult,
|
|
||||||
AdminKhhaoOrderQueryResult,
|
|
||||||
AdminKhhaoOrderSyncResult,
|
|
||||||
AdminKhhaoSourceConfig,
|
|
||||||
AdminKhhaoSyncState,
|
|
||||||
AdminNinetyoneOrderActionResult,
|
AdminNinetyoneOrderActionResult,
|
||||||
AdminNinetyoneOrderListResult,
|
AdminNinetyoneOrderListResult,
|
||||||
AdminKuaishouCloudFulfillmentConfig,
|
AdminKuaishouCloudFulfillmentConfig,
|
||||||
@@ -46,56 +41,6 @@ export function saveAdminAgisoShopConfigs(payload: {
|
|||||||
}>('/api/v1/admin/platform-config/agiso-shops', payload)
|
}>('/api/v1/admin/platform-config/agiso-shops', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchAdminKhhaoSourceConfig() {
|
|
||||||
return apiGet<{
|
|
||||||
filePath: string
|
|
||||||
source: AdminKhhaoSourceConfig
|
|
||||||
stateFilePath: string
|
|
||||||
syncState: AdminKhhaoSyncState
|
|
||||||
}>('/api/v1/admin/platform-config/khhao-source')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveAdminKhhaoSourceConfig(payload: AdminKhhaoSourceConfig) {
|
|
||||||
return apiPost<{
|
|
||||||
filePath: string
|
|
||||||
source: AdminKhhaoSourceConfig
|
|
||||||
stateFilePath: string
|
|
||||||
syncState: AdminKhhaoSyncState
|
|
||||||
}>('/api/v1/admin/platform-config/khhao-source', payload as unknown as Record<string, unknown>)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function testAdminKhhaoLogin(payload: {
|
|
||||||
baseUrl?: string
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
maxCaptchaAttempts?: number
|
|
||||||
includeImageBase64?: boolean
|
|
||||||
}) {
|
|
||||||
return apiPost<AdminKhhaoLoginTestResult>('/api/v1/admin/platform-config/khhao/test-login', payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queryAdminKhhaoOrders(payload: {
|
|
||||||
baseUrl?: string
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
page?: number
|
|
||||||
limit?: number
|
|
||||||
maxCaptchaAttempts?: number
|
|
||||||
}) {
|
|
||||||
return apiPost<AdminKhhaoOrderQueryResult>('/api/v1/admin/platform-config/khhao/query-orders', payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function syncAdminKhhaoOrders(payload: {
|
|
||||||
baseUrl?: string
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
page?: number
|
|
||||||
limit?: number
|
|
||||||
maxCaptchaAttempts?: number
|
|
||||||
}) {
|
|
||||||
return apiPost<AdminKhhaoOrderSyncResult>('/api/v1/admin/platform-config/khhao/sync-orders', payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchAdminNinetyoneOrders(params: {
|
export function fetchAdminNinetyoneOrders(params: {
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
|
|||||||
@@ -80,37 +80,6 @@ export interface AdminAgisoObservedShopItem {
|
|||||||
configured: boolean
|
configured: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminKhhaoLoginTestResult {
|
|
||||||
baseUrl: string
|
|
||||||
username: string
|
|
||||||
loggedInAt: string
|
|
||||||
attempt: number
|
|
||||||
responseMessage: string
|
|
||||||
captcha: {
|
|
||||||
recognizedText: string
|
|
||||||
imageBase64: string
|
|
||||||
}
|
|
||||||
session: {
|
|
||||||
cookieKeys: string[]
|
|
||||||
cookieCount: number
|
|
||||||
cookieHeaderMasked: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminKhhaoSourceConfig {
|
|
||||||
enabled: boolean
|
|
||||||
baseUrl: string
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
maxCaptchaAttempts: number
|
|
||||||
autoSync: {
|
|
||||||
enabled: boolean
|
|
||||||
intervalMinutes: number
|
|
||||||
pages: number
|
|
||||||
pageSize: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminKuaishouEticketSourceConfig {
|
export interface AdminKuaishouEticketSourceConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
@@ -195,82 +164,6 @@ export interface AdminKuaishouEticketConsumeResult {
|
|||||||
raw: Record<string, unknown>
|
raw: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminKhhaoSyncState {
|
|
||||||
syncFromCreatedAt: string
|
|
||||||
lastRunStartedAt: string
|
|
||||||
lastRunFinishedAt: string
|
|
||||||
lastRunStatus: string
|
|
||||||
lastErrorMessage: string
|
|
||||||
fetchedCount: number
|
|
||||||
syncedCount: number
|
|
||||||
ignoredCount: number
|
|
||||||
watchMode: string
|
|
||||||
watchOrders: Array<{
|
|
||||||
platformOrderId: string
|
|
||||||
shopId: string
|
|
||||||
shopName: string
|
|
||||||
itemTitle: string
|
|
||||||
payStatus: string
|
|
||||||
detectedAt: string
|
|
||||||
lastSeenAt: string
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminKhhaoOrderPreview {
|
|
||||||
provider: string
|
|
||||||
platform: string
|
|
||||||
platformLabel: string
|
|
||||||
platformOrderId: string
|
|
||||||
shopId: string
|
|
||||||
kuaishouShopId: string
|
|
||||||
khhaoShopId: string
|
|
||||||
shopName: string
|
|
||||||
itemId: string
|
|
||||||
itemTitle: string
|
|
||||||
skuCode: string
|
|
||||||
quantity: number
|
|
||||||
totalAmountFen: number
|
|
||||||
status: string
|
|
||||||
statusLabel: string
|
|
||||||
orderCreatedAt: string
|
|
||||||
raw: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminKhhaoOrderQueryResult {
|
|
||||||
baseUrl: string
|
|
||||||
page: number
|
|
||||||
limit: number
|
|
||||||
total: number
|
|
||||||
itemCount: number
|
|
||||||
items: AdminKhhaoOrderPreview[]
|
|
||||||
rawItems: Record<string, unknown>[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminKhhaoOrderSyncItem {
|
|
||||||
platformOrderId: string
|
|
||||||
provider: string
|
|
||||||
platform: string
|
|
||||||
shopId: string
|
|
||||||
skuCode: string
|
|
||||||
itemTitle: string
|
|
||||||
ignored: boolean
|
|
||||||
ignoreReason: string
|
|
||||||
orderId: number | null
|
|
||||||
orderItemCount: number
|
|
||||||
taskCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminKhhaoOrderSyncResult {
|
|
||||||
baseUrl: string
|
|
||||||
page: number
|
|
||||||
limit: number
|
|
||||||
total: number
|
|
||||||
fetchedCount: number
|
|
||||||
syncedCount: number
|
|
||||||
ignoredCount: number
|
|
||||||
results: AdminKhhaoOrderSyncItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminNinetyoneOrderItem {
|
export interface AdminNinetyoneOrderItem {
|
||||||
orderId: number
|
orderId: number
|
||||||
orderNo: string
|
orderNo: string
|
||||||
@@ -389,7 +282,6 @@ export interface AdminFulfillmentBindingConfigItem {
|
|||||||
platform: string
|
platform: string
|
||||||
shopId: string
|
shopId: string
|
||||||
shopName: string
|
shopName: string
|
||||||
khhaoShopId: string
|
|
||||||
skuCode: string
|
skuCode: string
|
||||||
skuName: string
|
skuName: string
|
||||||
profileKey: string
|
profileKey: string
|
||||||
@@ -411,7 +303,6 @@ export interface AdminKuaishouCloudFulfillmentItem {
|
|||||||
provider: string
|
provider: string
|
||||||
platform: string
|
platform: string
|
||||||
shopId: string
|
shopId: string
|
||||||
khhaoShopId: string
|
|
||||||
internalSkuCode: string
|
internalSkuCode: string
|
||||||
internalSkuName: string
|
internalSkuName: string
|
||||||
externalSkuCode: string
|
externalSkuCode: string
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|||||||
import { showSuccess } from '@/lib/feedback'
|
import { showSuccess } from '@/lib/feedback'
|
||||||
import {
|
import {
|
||||||
fetchAdminFulfillmentBindingConfigs,
|
fetchAdminFulfillmentBindingConfigs,
|
||||||
fetchAdminKuaishouEticketSourceConfig,
|
|
||||||
lookupAdminFulfillmentBindingOrder,
|
lookupAdminFulfillmentBindingOrder,
|
||||||
saveAdminFulfillmentBindingConfigs,
|
saveAdminFulfillmentBindingConfigs,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
@@ -12,7 +11,6 @@ import type {
|
|||||||
AdminFulfillmentBindingConfigItem,
|
AdminFulfillmentBindingConfigItem,
|
||||||
AdminFulfillmentLookupItem,
|
AdminFulfillmentLookupItem,
|
||||||
AdminFulfillmentLookupResult,
|
AdminFulfillmentLookupResult,
|
||||||
AdminKuaishouEticketShopConfigItem,
|
|
||||||
AdminObservedProductItem,
|
AdminObservedProductItem,
|
||||||
} from '@/types/admin'
|
} from '@/types/admin'
|
||||||
import { hasAdminRole } from '@/utils/admin-auth'
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
@@ -24,7 +22,6 @@ type EditableBinding = {
|
|||||||
platform: string
|
platform: string
|
||||||
shopId: string
|
shopId: string
|
||||||
shopName: string
|
shopName: string
|
||||||
khhaoShopId: string
|
|
||||||
skuCode: string
|
skuCode: string
|
||||||
skuName: string
|
skuName: string
|
||||||
profileKey: string
|
profileKey: string
|
||||||
@@ -41,7 +38,6 @@ type SaveBindingPayload = {
|
|||||||
platform: string
|
platform: string
|
||||||
shopId: string
|
shopId: string
|
||||||
shopName: string
|
shopName: string
|
||||||
khhaoShopId: string
|
|
||||||
skuCode: string
|
skuCode: string
|
||||||
skuName: string
|
skuName: string
|
||||||
profileKey: string
|
profileKey: string
|
||||||
@@ -57,7 +53,7 @@ type SaveBindingPayload = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ValidationField = 'skuCode' | 'match' | 'profileKey' | 'shopId' | 'khhaoShopId'
|
type ValidationField = 'skuCode' | 'match' | 'profileKey' | 'shopId'
|
||||||
|
|
||||||
type ValidationState = {
|
type ValidationState = {
|
||||||
bindingId: string
|
bindingId: string
|
||||||
@@ -78,7 +74,6 @@ const errorMessage = ref('')
|
|||||||
const filePath = ref('')
|
const filePath = ref('')
|
||||||
const bindings = ref<EditableBinding[]>([])
|
const bindings = ref<EditableBinding[]>([])
|
||||||
const observedProducts = ref<AdminObservedProductItem[]>([])
|
const observedProducts = ref<AdminObservedProductItem[]>([])
|
||||||
const kuaishouShopOptions = ref<AdminKuaishouEticketShopConfigItem[]>([])
|
|
||||||
const lookupLoading = ref(false)
|
const lookupLoading = ref(false)
|
||||||
const lookupErrorMessage = ref('')
|
const lookupErrorMessage = ref('')
|
||||||
const lookupResult = ref<AdminFulfillmentLookupResult | null>(null)
|
const lookupResult = ref<AdminFulfillmentLookupResult | null>(null)
|
||||||
@@ -113,7 +108,6 @@ type ImportableProductCandidate = Pick<
|
|||||||
AdminObservedProductItem,
|
AdminObservedProductItem,
|
||||||
'provider' | 'platform' | 'shopId' | 'shopName' | 'externalSkuCode' | 'externalItemId' | 'externalSkuName'
|
'provider' | 'platform' | 'shopId' | 'shopName' | 'externalSkuCode' | 'externalItemId' | 'externalSkuName'
|
||||||
> & {
|
> & {
|
||||||
khhaoShopId?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyBinding(): EditableBinding {
|
function createEmptyBinding(): EditableBinding {
|
||||||
@@ -123,7 +117,6 @@ function createEmptyBinding(): EditableBinding {
|
|||||||
platform: '',
|
platform: '',
|
||||||
shopId: '',
|
shopId: '',
|
||||||
shopName: '',
|
shopName: '',
|
||||||
khhaoShopId: '',
|
|
||||||
skuCode: '',
|
skuCode: '',
|
||||||
skuName: '',
|
skuName: '',
|
||||||
profileKey: 'manual_review',
|
profileKey: 'manual_review',
|
||||||
@@ -137,14 +130,12 @@ function createEmptyBinding(): EditableBinding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapEditableBinding(item: AdminFulfillmentBindingConfigItem): EditableBinding {
|
function mapEditableBinding(item: AdminFulfillmentBindingConfigItem): EditableBinding {
|
||||||
const matchedShop = resolveKuaishouShopById(item.shopId) || resolveKuaishouShopByName(item.shopName || '')
|
return {
|
||||||
const binding = {
|
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
provider: item.provider || 'agiso',
|
provider: item.provider || 'agiso',
|
||||||
platform: item.platform,
|
platform: item.platform,
|
||||||
shopId: matchedShop?.shopId || item.shopId,
|
shopId: item.shopId,
|
||||||
shopName: item.shopName || matchedShop?.kshopName || '',
|
shopName: item.shopName || '',
|
||||||
khhaoShopId: item.khhaoShopId || '',
|
|
||||||
skuCode: item.skuCode,
|
skuCode: item.skuCode,
|
||||||
skuName: item.skuName,
|
skuName: item.skuName,
|
||||||
profileKey: item.profileKey || 'manual_review',
|
profileKey: item.profileKey || 'manual_review',
|
||||||
@@ -155,9 +146,6 @@ function mapEditableBinding(item: AdminFulfillmentBindingConfigItem): EditableBi
|
|||||||
externalSkuName: item.match.externalSkuName,
|
externalSkuName: item.match.externalSkuName,
|
||||||
resolvedSkuName: String(item.match.config?.resolvedSkuName || ''),
|
resolvedSkuName: String(item.match.config?.resolvedSkuName || ''),
|
||||||
}
|
}
|
||||||
|
|
||||||
syncBindingShopSelection(binding)
|
|
||||||
return binding
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMatchCondition(item: Pick<EditableBinding, 'externalSkuCode' | 'externalItemId' | 'externalSkuName'>) {
|
function hasMatchCondition(item: Pick<EditableBinding, 'externalSkuCode' | 'externalItemId' | 'externalSkuName'>) {
|
||||||
@@ -205,64 +193,16 @@ function getBindingStatusLabel(binding: EditableBinding) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getBindingSummary(binding: EditableBinding) {
|
function getBindingSummary(binding: EditableBinding) {
|
||||||
const shopLabel = isKhhaoKuaishouBinding(binding)
|
|
||||||
? [
|
|
||||||
binding.shopName.trim() || binding.shopId.trim() || '未选快手店铺',
|
|
||||||
binding.khhaoShopId.trim() ? `khhao#${binding.khhaoShopId.trim()}` : '',
|
|
||||||
].filter(Boolean).join(' / ')
|
|
||||||
: (binding.shopName.trim() || binding.shopId.trim() || '跨店铺')
|
|
||||||
const parts = [
|
const parts = [
|
||||||
binding.provider.trim() || 'agiso',
|
binding.provider.trim() || 'agiso',
|
||||||
binding.platform.trim() || '未选平台',
|
binding.platform.trim() || '未选平台',
|
||||||
shopLabel,
|
binding.shopName.trim() || binding.shopId.trim() || '跨店铺',
|
||||||
binding.profileKey.trim() || 'manual_review',
|
binding.profileKey.trim() || 'manual_review',
|
||||||
]
|
]
|
||||||
|
|
||||||
return parts.join(' / ')
|
return parts.join(' / ')
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKhhaoKuaishouBinding(binding: Pick<EditableBinding, 'provider' | 'platform'>) {
|
|
||||||
return binding.provider.trim() === 'khhao' && binding.platform.trim() === 'kuaishou'
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveKuaishouShopById(shopId: string) {
|
|
||||||
const normalizedShopId = shopId.trim()
|
|
||||||
return kuaishouShopOptions.value.find((item) => item.shopId === normalizedShopId) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveKuaishouShopByName(shopName: string) {
|
|
||||||
const normalizedShopName = shopName.trim()
|
|
||||||
return kuaishouShopOptions.value.find((item) => item.kshopName === normalizedShopName) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getKuaishouShopOptionLabel(item: AdminKuaishouEticketShopConfigItem) {
|
|
||||||
return item.kshopName.trim()
|
|
||||||
? `${item.kshopName} (${item.shopId})`
|
|
||||||
: item.shopId
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncBindingShopSelection(binding: EditableBinding) {
|
|
||||||
if (!isKhhaoKuaishouBinding(binding)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchedById = resolveKuaishouShopById(binding.shopId)
|
|
||||||
if (matchedById) {
|
|
||||||
binding.shopId = matchedById.shopId
|
|
||||||
binding.shopName = matchedById.kshopName
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchedByName = resolveKuaishouShopByName(binding.shopName)
|
|
||||||
if (matchedByName) {
|
|
||||||
binding.shopId = matchedByName.shopId
|
|
||||||
binding.shopName = matchedByName.kshopName
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.shopId = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadConfigs() {
|
async function loadConfigs() {
|
||||||
if (!hasAdminRole('admin')) {
|
if (!hasAdminRole('admin')) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -273,12 +213,8 @@ async function loadConfigs() {
|
|||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [response, kuaishouSourceResponse] = await Promise.all([
|
const response = await fetchAdminFulfillmentBindingConfigs()
|
||||||
fetchAdminFulfillmentBindingConfigs(),
|
|
||||||
fetchAdminKuaishouEticketSourceConfig(),
|
|
||||||
])
|
|
||||||
filePath.value = response.data.filePath
|
filePath.value = response.data.filePath
|
||||||
kuaishouShopOptions.value = kuaishouSourceResponse.data.source.shops.filter((item) => item.enabled !== false)
|
|
||||||
const nextBindings = response.data.bindings.map(mapEditableBinding)
|
const nextBindings = response.data.bindings.map(mapEditableBinding)
|
||||||
bindings.value = nextBindings
|
bindings.value = nextBindings
|
||||||
collapsedBindingIds.value = buildCollapsedIds(nextBindings)
|
collapsedBindingIds.value = buildCollapsedIds(nextBindings)
|
||||||
@@ -312,7 +248,6 @@ function createBindingFromProductCandidate(item: ImportableProductCandidate): Ed
|
|||||||
platform: item.platform,
|
platform: item.platform,
|
||||||
shopId: item.shopId,
|
shopId: item.shopId,
|
||||||
shopName: item.shopName,
|
shopName: item.shopName,
|
||||||
khhaoShopId: item.khhaoShopId || '',
|
|
||||||
skuCode: '',
|
skuCode: '',
|
||||||
skuName: item.externalSkuName,
|
skuName: item.externalSkuName,
|
||||||
profileKey: 'manual_review',
|
profileKey: 'manual_review',
|
||||||
@@ -324,7 +259,6 @@ function createBindingFromProductCandidate(item: ImportableProductCandidate): Ed
|
|||||||
resolvedSkuName: item.externalSkuName,
|
resolvedSkuName: item.externalSkuName,
|
||||||
}
|
}
|
||||||
|
|
||||||
syncBindingShopSelection(binding)
|
|
||||||
return binding
|
return binding
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,14 +281,11 @@ function isLookupProductImported(lineId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBindingForSave(item: EditableBinding): SaveBindingPayload {
|
function normalizeBindingForSave(item: EditableBinding): SaveBindingPayload {
|
||||||
syncBindingShopSelection(item)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provider: item.provider.trim() || 'agiso',
|
provider: item.provider.trim() || 'agiso',
|
||||||
platform: item.platform.trim(),
|
platform: item.platform.trim(),
|
||||||
shopId: item.shopId.trim(),
|
shopId: item.shopId.trim(),
|
||||||
shopName: item.shopName.trim(),
|
shopName: item.shopName.trim(),
|
||||||
khhaoShopId: item.khhaoShopId.trim(),
|
|
||||||
skuCode: item.skuCode.trim(),
|
skuCode: item.skuCode.trim(),
|
||||||
skuName: item.skuName.trim(),
|
skuName: item.skuName.trim(),
|
||||||
profileKey: item.profileKey.trim() || 'manual_review',
|
profileKey: item.profileKey.trim() || 'manual_review',
|
||||||
@@ -376,7 +307,6 @@ function hasBindingContent(item: SaveBindingPayload) {
|
|||||||
item.platform
|
item.platform
|
||||||
|| item.shopId
|
|| item.shopId
|
||||||
|| item.shopName
|
|| item.shopName
|
||||||
|| item.khhaoShopId
|
|
||||||
|| item.skuCode
|
|| item.skuCode
|
||||||
|| item.skuName
|
|| item.skuName
|
||||||
|| item.match.externalSkuCode
|
|| item.match.externalSkuCode
|
||||||
@@ -392,24 +322,6 @@ function hasBindingContent(item: SaveBindingPayload) {
|
|||||||
|
|
||||||
function resolveBindingValidationState(item: SaveBindingPayload, index: number, bindingId: string): ValidationState {
|
function resolveBindingValidationState(item: SaveBindingPayload, index: number, bindingId: string): ValidationState {
|
||||||
const label = `第 ${index + 1} 条规则`
|
const label = `第 ${index + 1} 条规则`
|
||||||
const provider = item.provider.trim() || 'agiso'
|
|
||||||
const platform = item.platform.trim()
|
|
||||||
|
|
||||||
if (provider === 'khhao' && platform === 'kuaishou' && (!item.shopId || !item.shopName)) {
|
|
||||||
return {
|
|
||||||
bindingId,
|
|
||||||
field: 'shopId',
|
|
||||||
message: `${label} 需要选择已配置的快手官方店铺`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === 'khhao' && platform === 'kuaishou' && !item.khhaoShopId) {
|
|
||||||
return {
|
|
||||||
bindingId,
|
|
||||||
field: 'khhaoShopId',
|
|
||||||
message: `${label} 需要填写 khhao 店铺 ID`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.skuCode) {
|
if (!item.skuCode) {
|
||||||
return {
|
return {
|
||||||
@@ -454,7 +366,6 @@ async function focusValidationTarget(state: ValidationState) {
|
|||||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
card.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
|
||||||
const selectors: Record<ValidationField, string> = {
|
const selectors: Record<ValidationField, string> = {
|
||||||
khhaoShopId: '[data-field="khhaoShopId"]',
|
|
||||||
shopId: '[data-field="shopId"]',
|
shopId: '[data-field="shopId"]',
|
||||||
skuCode: '[data-field="skuCode"]',
|
skuCode: '[data-field="skuCode"]',
|
||||||
match: '[data-field="externalSkuCode"]',
|
match: '[data-field="externalSkuCode"]',
|
||||||
@@ -485,16 +396,6 @@ function isFieldInvalid(bindingId: string, field: ValidationField | 'externalSku
|
|||||||
return validationState.value.field === field
|
return validationState.value.field === field
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBindingProviderPlatformChange(binding: EditableBinding) {
|
|
||||||
syncBindingShopSelection(binding)
|
|
||||||
clearValidationState()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKuaishouShopChange(binding: EditableBinding) {
|
|
||||||
syncBindingShopSelection(binding)
|
|
||||||
clearValidationState()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveConfigs() {
|
async function saveConfigs() {
|
||||||
const normalizedBindings = bindings.value.map(normalizeBindingForSave)
|
const normalizedBindings = bindings.value.map(normalizeBindingForSave)
|
||||||
const nonEmptyBindings = normalizedBindings.filter(hasBindingContent)
|
const nonEmptyBindings = normalizedBindings.filter(hasBindingContent)
|
||||||
@@ -694,7 +595,7 @@ onMounted(loadConfigs)
|
|||||||
v-model="binding.provider"
|
v-model="binding.provider"
|
||||||
class="text-input"
|
class="text-input"
|
||||||
placeholder="agiso"
|
placeholder="agiso"
|
||||||
@input="handleBindingProviderPlatformChange(binding)"
|
@input="clearValidationState"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -704,76 +605,30 @@ onMounted(loadConfigs)
|
|||||||
v-model="binding.platform"
|
v-model="binding.platform"
|
||||||
class="text-input"
|
class="text-input"
|
||||||
placeholder="xianyu / taobao / pdd"
|
placeholder="xianyu / taobao / pdd"
|
||||||
@input="handleBindingProviderPlatformChange(binding)"
|
@input="clearValidationState"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<template v-if="isKhhaoKuaishouBinding(binding)">
|
<label class="field-block">
|
||||||
<label class="field-block">
|
<span>店铺 ID</span>
|
||||||
<span>khhao 店铺 ID</span>
|
<input
|
||||||
<input
|
v-model="binding.shopId"
|
||||||
v-model="binding.khhaoShopId"
|
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'shopId') }]"
|
||||||
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'khhaoShopId') }]"
|
data-field="shopId"
|
||||||
data-field="khhaoShopId"
|
placeholder="留空表示跨店铺通用"
|
||||||
placeholder="例如 12 / 13 / 14"
|
@input="clearValidationState"
|
||||||
@input="clearValidationState"
|
/>
|
||||||
/>
|
</label>
|
||||||
<small class="field-help">这里填 khhao 系统里的店铺编号,专门用于同步订单命中规则。</small>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
<label class="field-block">
|
||||||
<span>快手店铺</span>
|
<span>店铺名称</span>
|
||||||
<select
|
<input
|
||||||
v-model="binding.shopId"
|
v-model="binding.shopName"
|
||||||
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'shopId') }]"
|
class="text-input"
|
||||||
data-field="shopId"
|
placeholder="用于保存和识别店铺"
|
||||||
@change="handleKuaishouShopChange(binding)"
|
@input="clearValidationState"
|
||||||
>
|
/>
|
||||||
<option value="">请选择已配置的快手官方店铺</option>
|
</label>
|
||||||
<option
|
|
||||||
v-for="shop in kuaishouShopOptions"
|
|
||||||
:key="shop.shopId"
|
|
||||||
:value="shop.shopId"
|
|
||||||
>
|
|
||||||
{{ getKuaishouShopOptionLabel(shop) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<small class="field-help">这里会直接写入快手官方店铺 ID,不再使用 khhao 内部店铺编号。</small>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>快手店铺名</span>
|
|
||||||
<input
|
|
||||||
:value="binding.shopName || ''"
|
|
||||||
class="text-input text-input--readonly"
|
|
||||||
placeholder="选择店铺后自动带出"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<label class="field-block">
|
|
||||||
<span>店铺 ID</span>
|
|
||||||
<input
|
|
||||||
v-model="binding.shopId"
|
|
||||||
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'shopId') }]"
|
|
||||||
data-field="shopId"
|
|
||||||
placeholder="留空表示跨店铺通用"
|
|
||||||
@input="clearValidationState"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>店铺名称</span>
|
|
||||||
<input
|
|
||||||
v-model="binding.shopName"
|
|
||||||
class="text-input"
|
|
||||||
placeholder="用于保存和识别店铺"
|
|
||||||
@input="clearValidationState"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<label class="field-block">
|
<label class="field-block">
|
||||||
<span>优先级</span>
|
<span>优先级</span>
|
||||||
@@ -1213,7 +1068,6 @@ onMounted(loadConfigs)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lookup-toolbar,
|
.lookup-toolbar,
|
||||||
.khhao-toolbar,
|
|
||||||
.lookup-summary {
|
.lookup-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -1224,11 +1078,6 @@ onMounted(loadConfigs)
|
|||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.khhao-toolbar {
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-block--action :deep(.el-button) {
|
.field-block--action :deep(.el-button) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,12 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|||||||
import { showError, showSuccess } from '@/lib/feedback'
|
import { showError, showSuccess } from '@/lib/feedback'
|
||||||
import {
|
import {
|
||||||
fetchAdminCloudtentaclesSkuList,
|
fetchAdminCloudtentaclesSkuList,
|
||||||
fetchAdminKhhaoSourceConfig,
|
|
||||||
fetchAdminKuaishouCloudFulfillmentConfig,
|
fetchAdminKuaishouCloudFulfillmentConfig,
|
||||||
fetchAdminNinetyoneOrders,
|
fetchAdminNinetyoneOrders,
|
||||||
queryAdminKhhaoOrders,
|
|
||||||
saveAdminKuaishouCloudFulfillmentConfig,
|
saveAdminKuaishouCloudFulfillmentConfig,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
import type {
|
import type {
|
||||||
AdminCloudtentaclesSkuItem,
|
AdminCloudtentaclesSkuItem,
|
||||||
AdminKhhaoOrderPreview,
|
|
||||||
AdminKuaishouCloudFulfillmentConfig,
|
AdminKuaishouCloudFulfillmentConfig,
|
||||||
AdminKuaishouCloudFulfillmentItem,
|
AdminKuaishouCloudFulfillmentItem,
|
||||||
AdminNinetyoneOrderItem,
|
AdminNinetyoneOrderItem,
|
||||||
@@ -39,10 +36,6 @@ const enabled = ref(true)
|
|||||||
const items = ref<EditableItem[]>([])
|
const items = ref<EditableItem[]>([])
|
||||||
const collapsedIds = ref<string[]>([])
|
const collapsedIds = ref<string[]>([])
|
||||||
const ruleFilter = ref<RuleFilter>('all')
|
const ruleFilter = ref<RuleFilter>('all')
|
||||||
const khhaoLookupLoading = ref(false)
|
|
||||||
const khhaoLookupErrorMessage = ref('')
|
|
||||||
const khhaoLookupResults = ref<AdminKhhaoOrderPreview[]>([])
|
|
||||||
const importedKhhaoOrderKeys = ref<string[]>([])
|
|
||||||
const ninetyoneLookupLoading = ref(false)
|
const ninetyoneLookupLoading = ref(false)
|
||||||
const ninetyoneLookupErrorMessage = ref('')
|
const ninetyoneLookupErrorMessage = ref('')
|
||||||
const ninetyoneLookupResults = ref<AdminNinetyoneOrderItem[]>([])
|
const ninetyoneLookupResults = ref<AdminNinetyoneOrderItem[]>([])
|
||||||
@@ -50,14 +43,6 @@ const importedNinetyoneOrderKeys = ref<string[]>([])
|
|||||||
const cloudSkuCatalogLoading = ref(false)
|
const cloudSkuCatalogLoading = ref(false)
|
||||||
const cloudSkuCatalogErrorMessage = ref('')
|
const cloudSkuCatalogErrorMessage = ref('')
|
||||||
const cloudSkuCatalog = ref<AdminCloudtentaclesSkuItem[]>([])
|
const cloudSkuCatalog = ref<AdminCloudtentaclesSkuItem[]>([])
|
||||||
const khhaoLookupForm = reactive({
|
|
||||||
baseUrl: 'https://admin.khhao.com',
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
page: 1,
|
|
||||||
limit: 10,
|
|
||||||
maxCaptchaAttempts: 3,
|
|
||||||
})
|
|
||||||
const ninetyoneLookupForm = reactive({
|
const ninetyoneLookupForm = reactive({
|
||||||
status: 'pending_config' as 'pending_config' | 'all' | 'manual_failed',
|
status: 'pending_config' as 'pending_config' | 'all' | 'manual_failed',
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -86,21 +71,6 @@ const ruleFilterOptions = computed<Array<{ value: RuleFilter; label: string; cou
|
|||||||
{ value: 'disabled', label: '已停用', count: metrics.value.disabledCount },
|
{ value: 'disabled', label: '已停用', count: metrics.value.disabledCount },
|
||||||
])
|
])
|
||||||
|
|
||||||
const khhaoLookupMetrics = computed(() => {
|
|
||||||
const importedCount = khhaoLookupResults.value.filter((item) => isKhhaoProductImported(item)).length
|
|
||||||
const shopCount = new Set(
|
|
||||||
khhaoLookupResults.value
|
|
||||||
.map((item) => String(item.shopName || item.kuaishouShopId || item.shopId || '').trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
).size
|
|
||||||
return {
|
|
||||||
total: khhaoLookupResults.value.length,
|
|
||||||
importedCount,
|
|
||||||
availableCount: Math.max(khhaoLookupResults.value.length - importedCount, 0),
|
|
||||||
shopCount,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const ninetyoneLookupMetrics = computed(() => {
|
const ninetyoneLookupMetrics = computed(() => {
|
||||||
const importedCount = ninetyoneLookupResults.value.filter((item) => isNinetyoneProductImported(item)).length
|
const importedCount = ninetyoneLookupResults.value.filter((item) => isNinetyoneProductImported(item)).length
|
||||||
const pendingCount = ninetyoneLookupResults.value.filter((item) => item.orderStatus === 'pending_config').length
|
const pendingCount = ninetyoneLookupResults.value.filter((item) => item.orderStatus === 'pending_config').length
|
||||||
@@ -118,10 +88,9 @@ function createEmptyItem(): EditableItem {
|
|||||||
id: '',
|
id: '',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
provider: 'khhao',
|
provider: '91kaquan',
|
||||||
platform: 'kuaishou',
|
platform: 'kuaishou',
|
||||||
shopId: '',
|
shopId: '',
|
||||||
khhaoShopId: '',
|
|
||||||
internalSkuCode: '',
|
internalSkuCode: '',
|
||||||
internalSkuName: '',
|
internalSkuName: '',
|
||||||
externalSkuCode: '',
|
externalSkuCode: '',
|
||||||
@@ -148,10 +117,9 @@ function mapEditableItem(item: AdminKuaishouCloudFulfillmentItem): EditableItem
|
|||||||
id: item.id || crypto.randomUUID(),
|
id: item.id || crypto.randomUUID(),
|
||||||
enabled: item.enabled !== false,
|
enabled: item.enabled !== false,
|
||||||
priority: item.priority || 100,
|
priority: item.priority || 100,
|
||||||
provider: item.provider || 'khhao',
|
provider: item.provider || '91kaquan',
|
||||||
platform: item.platform || 'kuaishou',
|
platform: item.platform || 'kuaishou',
|
||||||
shopId: item.shopId || '',
|
shopId: item.shopId || '',
|
||||||
khhaoShopId: item.khhaoShopId || '',
|
|
||||||
internalSkuCode: item.internalSkuCode || '',
|
internalSkuCode: item.internalSkuCode || '',
|
||||||
internalSkuName: item.internalSkuName || '',
|
internalSkuName: item.internalSkuName || '',
|
||||||
externalSkuCode: item.externalSkuCode || '',
|
externalSkuCode: item.externalSkuCode || '',
|
||||||
@@ -177,10 +145,9 @@ function mapSaveItem(item: EditableItem): AdminKuaishouCloudFulfillmentItem {
|
|||||||
id: item.id.trim() || item.internalSkuCode.trim() || item.localId,
|
id: item.id.trim() || item.internalSkuCode.trim() || item.localId,
|
||||||
enabled: item.enabled,
|
enabled: item.enabled,
|
||||||
priority: Number(item.priority || 100) || 100,
|
priority: Number(item.priority || 100) || 100,
|
||||||
provider: item.provider.trim() || 'khhao',
|
provider: item.provider.trim() || '91kaquan',
|
||||||
platform: item.platform.trim() || 'kuaishou',
|
platform: item.platform.trim() || 'kuaishou',
|
||||||
shopId: item.shopId.trim(),
|
shopId: item.shopId.trim(),
|
||||||
khhaoShopId: item.khhaoShopId.trim(),
|
|
||||||
internalSkuCode: item.internalSkuCode.trim(),
|
internalSkuCode: item.internalSkuCode.trim(),
|
||||||
internalSkuName: item.internalSkuName.trim(),
|
internalSkuName: item.internalSkuName.trim(),
|
||||||
externalSkuCode: item.externalSkuCode.trim(),
|
externalSkuCode: item.externalSkuCode.trim(),
|
||||||
@@ -218,9 +185,8 @@ function hasMeaningfulContent(item: EditableItem) {
|
|||||||
|| item.cloudSkuName.trim()
|
|| item.cloudSkuName.trim()
|
||||||
|| item.vnKey.trim()
|
|| item.vnKey.trim()
|
||||||
|| item.shopId.trim()
|
|| item.shopId.trim()
|
||||||
|| item.khhaoShopId.trim()
|
|
||||||
|| item.notes.trim()
|
|| item.notes.trim()
|
||||||
|| item.provider.trim() !== 'khhao'
|
|| item.provider.trim() !== '91kaquan'
|
||||||
|| item.platform.trim() !== 'kuaishou'
|
|| item.platform.trim() !== 'kuaishou'
|
||||||
|| item.priority !== 100
|
|| item.priority !== 100
|
||||||
|| item.autoBuyEnabled !== true
|
|| item.autoBuyEnabled !== true
|
||||||
@@ -268,7 +234,7 @@ function getCardTitle(item: EditableItem, index: number) {
|
|||||||
|
|
||||||
function getCardSummary(item: EditableItem) {
|
function getCardSummary(item: EditableItem) {
|
||||||
const parts = [
|
const parts = [
|
||||||
item.provider.trim() || 'khhao',
|
item.provider.trim() || '91kaquan',
|
||||||
item.platform.trim() || 'kuaishou',
|
item.platform.trim() || 'kuaishou',
|
||||||
item.shopId.trim() || '跨店铺',
|
item.shopId.trim() || '跨店铺',
|
||||||
item.cloudSkuId > 0 ? `cloud#${item.cloudSkuId}` : '待填 cloud SKU',
|
item.cloudSkuId > 0 ? `cloud#${item.cloudSkuId}` : '待填 cloud SKU',
|
||||||
@@ -337,10 +303,6 @@ function getCloudSkuSummary(item: EditableItem) {
|
|||||||
return '待选择 cloud SKU'
|
return '待选择 cloud SKU'
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAmountFen(value: number) {
|
|
||||||
return (Number(value || 0) / 100).toFixed(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCollapsed(localId: string) {
|
function isCollapsed(localId: string) {
|
||||||
return collapsedIds.value.includes(localId)
|
return collapsedIds.value.includes(localId)
|
||||||
}
|
}
|
||||||
@@ -381,18 +343,11 @@ async function loadConfigs() {
|
|||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [response, khhaoSourceResponse] = await Promise.all([
|
const response = await fetchAdminKuaishouCloudFulfillmentConfig()
|
||||||
fetchAdminKuaishouCloudFulfillmentConfig(),
|
|
||||||
fetchAdminKhhaoSourceConfig(),
|
|
||||||
])
|
|
||||||
filePath.value = response.data.filePath
|
filePath.value = response.data.filePath
|
||||||
enabled.value = response.data.source.enabled !== false
|
enabled.value = response.data.source.enabled !== false
|
||||||
items.value = (response.data.source.items || []).map(mapEditableItem)
|
items.value = (response.data.source.items || []).map(mapEditableItem)
|
||||||
rebuildCollapsedState()
|
rebuildCollapsedState()
|
||||||
khhaoLookupForm.baseUrl = khhaoSourceResponse.data.source.baseUrl || 'https://admin.khhao.com'
|
|
||||||
khhaoLookupForm.username = khhaoSourceResponse.data.source.username || ''
|
|
||||||
khhaoLookupForm.password = khhaoSourceResponse.data.source.password || ''
|
|
||||||
khhaoLookupForm.maxCaptchaAttempts = khhaoSourceResponse.data.source.maxCaptchaAttempts || 3
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error instanceof Error ? error.message : '读取新履约配置失败'
|
errorMessage.value = error instanceof Error ? error.message : '读取新履约配置失败'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -415,36 +370,6 @@ function removeItem(localId: string) {
|
|||||||
setCollapsed(localId, false)
|
setCollapsed(localId, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createItemFromKhhaoOrder(item: AdminKhhaoOrderPreview): EditableItem {
|
|
||||||
return {
|
|
||||||
localId: crypto.randomUUID(),
|
|
||||||
id: '',
|
|
||||||
enabled: true,
|
|
||||||
priority: 100,
|
|
||||||
provider: item.provider || 'khhao',
|
|
||||||
platform: item.platform || 'kuaishou',
|
|
||||||
shopId: item.kuaishouShopId || item.shopId || '',
|
|
||||||
khhaoShopId: item.khhaoShopId || item.shopId || '',
|
|
||||||
internalSkuCode: '',
|
|
||||||
internalSkuName: item.itemTitle || '',
|
|
||||||
externalSkuCode: item.skuCode || '',
|
|
||||||
externalItemId: item.itemId || '',
|
|
||||||
externalSkuName: item.itemTitle || '',
|
|
||||||
resolvedSkuName: item.itemTitle || '',
|
|
||||||
cloudSourceKey: 'default',
|
|
||||||
cloudSkuId: 0,
|
|
||||||
cloudSkuName: '',
|
|
||||||
vnKey: '1',
|
|
||||||
autoBuyEnabled: true,
|
|
||||||
minAssetReserve: 0,
|
|
||||||
autoReturnNumberAfterDispatch: false,
|
|
||||||
autoConsumeAfterDispatch: false,
|
|
||||||
kuaishouConsumeShopId: item.kuaishouShopId || item.shopId || '',
|
|
||||||
kuaishouConsumeShopName: item.shopName || '',
|
|
||||||
notes: `从 khhao 订单 ${item.platformOrderId} 导入`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createItemFromNinetyoneOrder(item: AdminNinetyoneOrderItem): EditableItem {
|
function createItemFromNinetyoneOrder(item: AdminNinetyoneOrderItem): EditableItem {
|
||||||
const productNo = String(item.productNo || '').trim()
|
const productNo = String(item.productNo || '').trim()
|
||||||
const productName = String(item.productName || productNo).trim()
|
const productName = String(item.productName || productNo).trim()
|
||||||
@@ -456,7 +381,6 @@ function createItemFromNinetyoneOrder(item: AdminNinetyoneOrderItem): EditableIt
|
|||||||
provider: '91kaquan',
|
provider: '91kaquan',
|
||||||
platform: 'kuaishou',
|
platform: 'kuaishou',
|
||||||
shopId: item.shopId || '91kaquan',
|
shopId: item.shopId || '91kaquan',
|
||||||
khhaoShopId: '',
|
|
||||||
internalSkuCode: '',
|
internalSkuCode: '',
|
||||||
internalSkuName: productName,
|
internalSkuName: productName,
|
||||||
externalSkuCode: productNo,
|
externalSkuCode: productNo,
|
||||||
@@ -477,24 +401,6 @@ function createItemFromNinetyoneOrder(item: AdminNinetyoneOrderItem): EditableIt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findExistingItemFromKhhaoOrder(order: AdminKhhaoOrderPreview) {
|
|
||||||
const orderShopId = String(order.kuaishouShopId || order.shopId || '').trim()
|
|
||||||
const orderItemId = String(order.itemId || '').trim()
|
|
||||||
const orderSkuCode = String(order.skuCode || '').trim()
|
|
||||||
const orderTitle = String(order.itemTitle || '').trim()
|
|
||||||
|
|
||||||
return items.value.find((item) => {
|
|
||||||
const sameShop = !orderShopId
|
|
||||||
|| item.shopId.trim() === orderShopId
|
|
||||||
|| item.khhaoShopId.trim() === String(order.khhaoShopId || '').trim()
|
|
||||||
|| item.kuaishouConsumeShopId.trim() === orderShopId
|
|
||||||
const sameItemId = orderItemId && item.externalItemId.trim() === orderItemId
|
|
||||||
const sameSkuCode = orderSkuCode && item.externalSkuCode.trim() === orderSkuCode
|
|
||||||
const sameTitle = orderTitle && [item.externalSkuName, item.internalSkuName, item.resolvedSkuName].some((value) => value.trim() === orderTitle)
|
|
||||||
return sameShop && (sameItemId || sameSkuCode || sameTitle)
|
|
||||||
}) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function findExistingItemFromNinetyoneOrder(order: AdminNinetyoneOrderItem) {
|
function findExistingItemFromNinetyoneOrder(order: AdminNinetyoneOrderItem) {
|
||||||
const productNo = String(order.productNo || '').trim()
|
const productNo = String(order.productNo || '').trim()
|
||||||
const productName = String(order.productName || '').trim()
|
const productName = String(order.productName || '').trim()
|
||||||
@@ -512,31 +418,6 @@ function findExistingItemFromNinetyoneOrder(order: AdminNinetyoneOrderItem) {
|
|||||||
}) || null
|
}) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importKhhaoProduct(item: AdminKhhaoOrderPreview) {
|
|
||||||
validationState.value = null
|
|
||||||
const importKey = getKhhaoOrderImportKey(item)
|
|
||||||
const existing = findExistingItemFromKhhaoOrder(item)
|
|
||||||
if (existing) {
|
|
||||||
setCollapsed(existing.localId, false)
|
|
||||||
if (!importedKhhaoOrderKeys.value.includes(importKey)) {
|
|
||||||
importedKhhaoOrderKeys.value = [...importedKhhaoOrderKeys.value, importKey]
|
|
||||||
}
|
|
||||||
showSuccess('已定位到现有规则草稿,直接继续完善即可')
|
|
||||||
await focusValidationTarget(existing.localId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = createItemFromKhhaoOrder(item)
|
|
||||||
items.value.unshift(next)
|
|
||||||
setCollapsed(next.localId, false)
|
|
||||||
|
|
||||||
if (!importedKhhaoOrderKeys.value.includes(importKey)) {
|
|
||||||
importedKhhaoOrderKeys.value = [...importedKhhaoOrderKeys.value, importKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
await focusValidationTarget(next.localId)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function importNinetyoneProduct(item: AdminNinetyoneOrderItem) {
|
async function importNinetyoneProduct(item: AdminNinetyoneOrderItem) {
|
||||||
validationState.value = null
|
validationState.value = null
|
||||||
const importKey = getNinetyoneOrderImportKey(item)
|
const importKey = getNinetyoneOrderImportKey(item)
|
||||||
@@ -562,21 +443,6 @@ async function importNinetyoneProduct(item: AdminNinetyoneOrderItem) {
|
|||||||
await focusValidationTarget(next.localId)
|
await focusValidationTarget(next.localId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKhhaoOrderImportKey(item: AdminKhhaoOrderPreview) {
|
|
||||||
return [
|
|
||||||
item.platformOrderId,
|
|
||||||
item.itemId,
|
|
||||||
item.skuCode,
|
|
||||||
item.kuaishouShopId || item.shopId,
|
|
||||||
]
|
|
||||||
.map((value) => String(value || '').trim())
|
|
||||||
.join(':')
|
|
||||||
}
|
|
||||||
|
|
||||||
function isKhhaoProductImported(item: AdminKhhaoOrderPreview) {
|
|
||||||
return importedKhhaoOrderKeys.value.includes(getKhhaoOrderImportKey(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNinetyoneOrderImportKey(item: AdminNinetyoneOrderItem) {
|
function getNinetyoneOrderImportKey(item: AdminNinetyoneOrderItem) {
|
||||||
return [
|
return [
|
||||||
item.orderNo,
|
item.orderNo,
|
||||||
@@ -768,34 +634,6 @@ async function saveConfigs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function lookupKhhaoProducts() {
|
|
||||||
if (!khhaoLookupForm.username.trim() || !khhaoLookupForm.password.trim()) {
|
|
||||||
khhaoLookupErrorMessage.value = '请先填写 khhao 账号和密码'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
khhaoLookupLoading.value = true
|
|
||||||
khhaoLookupErrorMessage.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await queryAdminKhhaoOrders({
|
|
||||||
baseUrl: khhaoLookupForm.baseUrl.trim() || 'https://admin.khhao.com',
|
|
||||||
username: khhaoLookupForm.username.trim(),
|
|
||||||
password: khhaoLookupForm.password.trim(),
|
|
||||||
page: Number(khhaoLookupForm.page || 1),
|
|
||||||
limit: Number(khhaoLookupForm.limit || 10),
|
|
||||||
maxCaptchaAttempts: Number(khhaoLookupForm.maxCaptchaAttempts || 3),
|
|
||||||
})
|
|
||||||
khhaoLookupResults.value = response.data.items
|
|
||||||
importedKhhaoOrderKeys.value = []
|
|
||||||
} catch (error) {
|
|
||||||
khhaoLookupResults.value = []
|
|
||||||
khhaoLookupErrorMessage.value = error instanceof Error ? error.message : 'khhao 订单查询失败'
|
|
||||||
} finally {
|
|
||||||
khhaoLookupLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function lookupNinetyoneProducts() {
|
async function lookupNinetyoneProducts() {
|
||||||
ninetyoneLookupLoading.value = true
|
ninetyoneLookupLoading.value = true
|
||||||
ninetyoneLookupErrorMessage.value = ''
|
ninetyoneLookupErrorMessage.value = ''
|
||||||
@@ -824,7 +662,7 @@ onMounted(loadConfigs)
|
|||||||
<header class="panel-header">
|
<header class="panel-header">
|
||||||
<div class="panel-header-copy">
|
<div class="panel-header-copy">
|
||||||
<h1>快手 Cloud 新履约</h1>
|
<h1>快手 Cloud 新履约</h1>
|
||||||
<p>维护 khhao 快手订单到内部 SKU、cloud SKU、虚拟号与后续动作的映射规则。</p>
|
<p>维护 91卡券快手订单到内部 SKU、cloud SKU、虚拟号与后续动作的映射规则。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-tags">
|
<div class="header-tags">
|
||||||
<span class="header-tag">独立新流程</span>
|
<span class="header-tag">独立新流程</span>
|
||||||
@@ -874,7 +712,6 @@ onMounted(loadConfigs)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overview-notes">
|
<div class="overview-notes">
|
||||||
<span class="overview-note">khhao 订单只用于取样导入,不写入订单库</span>
|
|
||||||
<span class="overview-note">91卡券订单来自已接收的待补全队列</span>
|
<span class="overview-note">91卡券订单来自已接收的待补全队列</span>
|
||||||
<span class="overview-note">内部 SKU 决定最终任务与库存绑定</span>
|
<span class="overview-note">内部 SKU 决定最终任务与库存绑定</span>
|
||||||
<span class="overview-note">cloud SKU 决定自动购买、发货与退号资源</span>
|
<span class="overview-note">cloud SKU 决定自动购买、发货与退号资源</span>
|
||||||
@@ -885,112 +722,6 @@ onMounted(loadConfigs)
|
|||||||
<div v-if="loading" class="empty-block">新履约配置加载中</div>
|
<div v-if="loading" class="empty-block">新履约配置加载中</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<section class="table-card table-card--dense">
|
|
||||||
<div class="section-title-row section-title-row--tight">
|
|
||||||
<div>
|
|
||||||
<h3>khhao 订单取样导入</h3>
|
|
||||||
<p>先查快手订单样本,再导入到下面的规则草稿里继续补齐内部 SKU 和 cloud 资源。</p>
|
|
||||||
</div>
|
|
||||||
<div class="section-mini-stats">
|
|
||||||
<span class="mini-stat-chip">结果 {{ khhaoLookupMetrics.total }}</span>
|
|
||||||
<span class="mini-stat-chip">店铺 {{ khhaoLookupMetrics.shopCount }}</span>
|
|
||||||
<span class="mini-stat-chip">待导入 {{ khhaoLookupMetrics.availableCount }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="khhao-toolbar">
|
|
||||||
<label class="field-block field-wide">
|
|
||||||
<span>Base URL</span>
|
|
||||||
<input v-model="khhaoLookupForm.baseUrl" class="text-input" placeholder="https://admin.khhao.com" />
|
|
||||||
</label>
|
|
||||||
<label class="field-block">
|
|
||||||
<span>账号</span>
|
|
||||||
<input v-model="khhaoLookupForm.username" class="text-input" placeholder="khhao 登录账号" />
|
|
||||||
</label>
|
|
||||||
<label class="field-block">
|
|
||||||
<span>密码</span>
|
|
||||||
<input v-model="khhaoLookupForm.password" class="text-input" placeholder="khhao 登录密码" type="password" />
|
|
||||||
</label>
|
|
||||||
<label class="field-block">
|
|
||||||
<span>页码</span>
|
|
||||||
<input v-model.number="khhaoLookupForm.page" class="text-input" min="1" type="number" />
|
|
||||||
</label>
|
|
||||||
<label class="field-block">
|
|
||||||
<span>每页条数</span>
|
|
||||||
<input v-model.number="khhaoLookupForm.limit" class="text-input" max="50" min="1" type="number" />
|
|
||||||
</label>
|
|
||||||
<label class="field-block">
|
|
||||||
<span>验证码重试</span>
|
|
||||||
<input v-model.number="khhaoLookupForm.maxCaptchaAttempts" class="text-input" max="5" min="1" type="number" />
|
|
||||||
</label>
|
|
||||||
<div class="field-block field-block--action">
|
|
||||||
<span>操作</span>
|
|
||||||
<el-button :loading="khhaoLookupLoading" round type="primary" @click="lookupKhhaoProducts">查询 khhao 订单</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="toolbar-hint">查询结果只用于补配置,不会写入订单库,也不会影响现有订单数据。</p>
|
|
||||||
|
|
||||||
<p v-if="khhaoLookupErrorMessage" class="error-copy lookup-error">{{ khhaoLookupErrorMessage }}</p>
|
|
||||||
<div v-else-if="khhaoLookupLoading" class="empty-inline">正在查询 khhao 订单…</div>
|
|
||||||
|
|
||||||
<table v-else class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>订单</th>
|
|
||||||
<th>店铺</th>
|
|
||||||
<th>商品</th>
|
|
||||||
<th>金额 / 状态</th>
|
|
||||||
<th>操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="item in khhaoLookupResults" :key="getKhhaoOrderImportKey(item)">
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ item.platformOrderId || '-' }}</strong>
|
|
||||||
<span class="cell-subtle">{{ item.platformLabel || item.platform || 'kuaishou' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ item.shopName || item.shopId || '-' }}</strong>
|
|
||||||
<span class="cell-subtle">khhao ID: {{ item.khhaoShopId || item.shopId || '-' }}</span>
|
|
||||||
<span class="cell-subtle">快手 ID: {{ item.kuaishouShopId || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ item.itemTitle || '-' }}</strong>
|
|
||||||
<span class="cell-subtle">SKU: {{ item.skuCode || '-' }}</span>
|
|
||||||
<span class="cell-subtle">ItemId: {{ item.itemId || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ formatAmountFen(item.totalAmountFen) }}</strong>
|
|
||||||
<span class="cell-subtle">{{ item.statusLabel || item.status || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<el-button
|
|
||||||
v-if="!isKhhaoProductImported(item)"
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
@click="importKhhaoProduct(item)"
|
|
||||||
>
|
|
||||||
导入到规则
|
|
||||||
</el-button>
|
|
||||||
<span v-else class="cell-subtle">已导入草稿</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="khhaoLookupResults.length === 0">
|
|
||||||
<td colspan="5" class="empty-inline">还没有 khhao 查询结果。</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="table-card table-card--dense">
|
<section class="table-card table-card--dense">
|
||||||
<div class="section-title-row section-title-row--tight">
|
<div class="section-title-row section-title-row--tight">
|
||||||
<div>
|
<div>
|
||||||
@@ -1004,7 +735,7 @@ onMounted(loadConfigs)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="khhao-toolbar">
|
<div class="lookup-toolbar">
|
||||||
<label class="field-block">
|
<label class="field-block">
|
||||||
<span>订单状态</span>
|
<span>订单状态</span>
|
||||||
<select v-model="ninetyoneLookupForm.status" class="text-input">
|
<select v-model="ninetyoneLookupForm.status" class="text-input">
|
||||||
@@ -1159,7 +890,7 @@ onMounted(loadConfigs)
|
|||||||
<div class="mapping-grid">
|
<div class="mapping-grid">
|
||||||
<label class="field-block">
|
<label class="field-block">
|
||||||
<span>来源 provider</span>
|
<span>来源 provider</span>
|
||||||
<input v-model="item.provider" class="text-input" maxlength="32" placeholder="khhao / 91kaquan" />
|
<input v-model="item.provider" class="text-input" maxlength="32" placeholder="默认 91kaquan" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field-block">
|
<label class="field-block">
|
||||||
<span>来源 platform</span>
|
<span>来源 platform</span>
|
||||||
@@ -1169,13 +900,9 @@ onMounted(loadConfigs)
|
|||||||
<span>店铺 ID</span>
|
<span>店铺 ID</span>
|
||||||
<input v-model="item.shopId" class="text-input" maxlength="80" placeholder="留空表示跨店铺共用" />
|
<input v-model="item.shopId" class="text-input" maxlength="80" placeholder="留空表示跨店铺共用" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field-block">
|
|
||||||
<span>khhao 店铺 ID</span>
|
|
||||||
<input v-model="item.khhaoShopId" class="text-input" maxlength="80" placeholder="仅 khhao 使用,例如 10" />
|
|
||||||
</label>
|
|
||||||
<label class="field-block">
|
<label class="field-block">
|
||||||
<span>外部 SKU</span>
|
<span>外部 SKU</span>
|
||||||
<input v-model="item.externalSkuCode" class="text-input" maxlength="120" placeholder="例如 khhao 的 skuCode" />
|
<input v-model="item.externalSkuCode" class="text-input" maxlength="120" placeholder="例如 91卡券 productNo" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field-block">
|
<label class="field-block">
|
||||||
<span>外部商品 ID</span>
|
<span>外部商品 ID</span>
|
||||||
@@ -2102,9 +1829,9 @@ onMounted(loadConfigs)
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.khhao-toolbar {
|
.lookup-toolbar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(220px, 1.6fr) minmax(140px, 1fr) minmax(140px, 1fr) 90px 110px 120px 150px;
|
grid-template-columns: minmax(180px, 1fr) 110px 130px 180px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
@@ -2407,7 +2134,7 @@ onMounted(loadConfigs)
|
|||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.overview-stats,
|
.overview-stats,
|
||||||
.khhao-toolbar,
|
.lookup-toolbar,
|
||||||
.binding-grid,
|
.binding-grid,
|
||||||
.mapping-editor {
|
.mapping-editor {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -2433,7 +2160,7 @@ onMounted(loadConfigs)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.overview-stats,
|
.overview-stats,
|
||||||
.khhao-toolbar,
|
.lookup-toolbar,
|
||||||
.binding-grid,
|
.binding-grid,
|
||||||
.mapping-editor,
|
.mapping-editor,
|
||||||
.mapping-grid,
|
.mapping-grid,
|
||||||
|
|||||||
@@ -3,19 +3,16 @@ import { onMounted, ref } from 'vue'
|
|||||||
|
|
||||||
import AdminPlatformAgisoSection from '@/components/admin/AdminPlatformAgisoSection.vue'
|
import AdminPlatformAgisoSection from '@/components/admin/AdminPlatformAgisoSection.vue'
|
||||||
import AdminPlatformCloudtentaclesSection from '@/components/admin/AdminPlatformCloudtentaclesSection.vue'
|
import AdminPlatformCloudtentaclesSection from '@/components/admin/AdminPlatformCloudtentaclesSection.vue'
|
||||||
import AdminPlatformKhhaoSection from '@/components/admin/AdminPlatformKhhaoSection.vue'
|
|
||||||
import AdminPlatformNinetyoneSection from '@/components/admin/AdminPlatformNinetyoneSection.vue'
|
import AdminPlatformNinetyoneSection from '@/components/admin/AdminPlatformNinetyoneSection.vue'
|
||||||
import AdminPlatformKuaishouEticketSection from '@/components/admin/AdminPlatformKuaishouEticketSection.vue'
|
import AdminPlatformKuaishouEticketSection from '@/components/admin/AdminPlatformKuaishouEticketSection.vue'
|
||||||
import { useAdminAgisoPlatform } from '@/composables/admin/platform-shops/useAdminAgisoPlatform'
|
import { useAdminAgisoPlatform } from '@/composables/admin/platform-shops/useAdminAgisoPlatform'
|
||||||
import { useAdminCloudtentaclesPlatform } from '@/composables/admin/platform-shops/useAdminCloudtentaclesPlatform'
|
import { useAdminCloudtentaclesPlatform } from '@/composables/admin/platform-shops/useAdminCloudtentaclesPlatform'
|
||||||
import { useAdminKhhaoPlatform } from '@/composables/admin/platform-shops/useAdminKhhaoPlatform'
|
|
||||||
import { useAdminNinetyonePlatform } from '@/composables/admin/platform-shops/useAdminNinetyonePlatform'
|
import { useAdminNinetyonePlatform } from '@/composables/admin/platform-shops/useAdminNinetyonePlatform'
|
||||||
import { useAdminKuaishouEticketPlatform } from '@/composables/admin/platform-shops/useAdminKuaishouEticketPlatform'
|
import { useAdminKuaishouEticketPlatform } from '@/composables/admin/platform-shops/useAdminKuaishouEticketPlatform'
|
||||||
import type { PlatformTab } from '@/composables/admin/platform-shops/types'
|
import type { PlatformTab } from '@/composables/admin/platform-shops/types'
|
||||||
import {
|
import {
|
||||||
fetchAdminAgisoShopConfigs,
|
fetchAdminAgisoShopConfigs,
|
||||||
fetchAdminCloudtentaclesSourceConfig,
|
fetchAdminCloudtentaclesSourceConfig,
|
||||||
fetchAdminKhhaoSourceConfig,
|
|
||||||
fetchAdminKuaishouEticketSourceConfig,
|
fetchAdminKuaishouEticketSourceConfig,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
import { hasAdminRole } from '@/utils/admin-auth'
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
@@ -45,27 +42,6 @@ const {
|
|||||||
saveAgisoConfigs: persistAgisoConfigs,
|
saveAgisoConfigs: persistAgisoConfigs,
|
||||||
} = useAdminAgisoPlatform()
|
} = useAdminAgisoPlatform()
|
||||||
|
|
||||||
const {
|
|
||||||
khhaoFilePath,
|
|
||||||
khhaoStateFilePath,
|
|
||||||
khhaoForm,
|
|
||||||
khhaoSaving,
|
|
||||||
khhaoTesting,
|
|
||||||
khhaoQuerying,
|
|
||||||
khhaoSyncing,
|
|
||||||
khhaoResultError,
|
|
||||||
khhaoLoginResult,
|
|
||||||
khhaoQueryResult,
|
|
||||||
khhaoSyncResult,
|
|
||||||
khhaoAutoSyncState,
|
|
||||||
khhaoStats,
|
|
||||||
hydrateKhhaoConfig,
|
|
||||||
handleKhhaoSaveSource,
|
|
||||||
handleKhhaoTestLogin,
|
|
||||||
handleKhhaoQueryOrders,
|
|
||||||
handleKhhaoSyncOrders,
|
|
||||||
} = useAdminKhhaoPlatform()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ninetyoneLoading,
|
ninetyoneLoading,
|
||||||
ninetyoneActionLoadingId,
|
ninetyoneActionLoadingId,
|
||||||
@@ -156,15 +132,13 @@ async function loadConfigs() {
|
|||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [agisoResponse, khhaoResponse, kuaishouEticketResponse, cloudtentaclesResponse] = await Promise.all([
|
const [agisoResponse, kuaishouEticketResponse, cloudtentaclesResponse] = await Promise.all([
|
||||||
fetchAdminAgisoShopConfigs(),
|
fetchAdminAgisoShopConfigs(),
|
||||||
fetchAdminKhhaoSourceConfig(),
|
|
||||||
fetchAdminKuaishouEticketSourceConfig(),
|
fetchAdminKuaishouEticketSourceConfig(),
|
||||||
fetchAdminCloudtentaclesSourceConfig(),
|
fetchAdminCloudtentaclesSourceConfig(),
|
||||||
])
|
])
|
||||||
|
|
||||||
hydrateAgisoConfig(agisoResponse.data)
|
hydrateAgisoConfig(agisoResponse.data)
|
||||||
hydrateKhhaoConfig(khhaoResponse.data)
|
|
||||||
hydrateKuaishouEticketConfig(kuaishouEticketResponse.data)
|
hydrateKuaishouEticketConfig(kuaishouEticketResponse.data)
|
||||||
hydrateCloudtentaclesConfig(cloudtentaclesResponse.data)
|
hydrateCloudtentaclesConfig(cloudtentaclesResponse.data)
|
||||||
await loadNinetyoneOrders(1)
|
await loadNinetyoneOrders(1)
|
||||||
@@ -191,7 +165,7 @@ onMounted(loadConfigs)
|
|||||||
<header class="hero-card">
|
<header class="hero-card">
|
||||||
<div class="hero-copy">
|
<div class="hero-copy">
|
||||||
<h1>平台配置</h1>
|
<h1>平台配置</h1>
|
||||||
<p>按平台分别管理来源接入、凭据与店铺能力。Agiso 负责 webhook / 店铺消息配置,khhao 负责来源拉单,快手小店核销负责独立券核销调试,cloudtentacles 负责外部履约平台登录。</p>
|
<p>按平台分别管理来源接入、凭据与店铺能力。Agiso 负责 webhook / 店铺消息配置,91卡券负责新订单来源,快手小店核销负责独立券核销调试,cloudtentacles 负责外部履约平台登录。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<el-button round @click="loadConfigs">刷新全部配置</el-button>
|
<el-button round @click="loadConfigs">刷新全部配置</el-button>
|
||||||
@@ -241,23 +215,6 @@ onMounted(loadConfigs)
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="['platform-overview-card', { 'is-active': activePlatform === 'khhao' }]"
|
|
||||||
@click="switchPlatform('khhao')"
|
|
||||||
>
|
|
||||||
<div class="platform-head">
|
|
||||||
<span class="platform-badge">khhao</span>
|
|
||||||
<span class="platform-tag">账号 / 拉单</span>
|
|
||||||
</div>
|
|
||||||
<strong>{{ khhaoStats.hasCredential ? '已配置' : '未配置' }}</strong>
|
|
||||||
<span>来源凭据</span>
|
|
||||||
<div class="platform-metrics">
|
|
||||||
<span>最近查询 {{ khhaoStats.queryCount }} 条</span>
|
|
||||||
<span>最近同步成功 {{ khhaoStats.syncedCount }} 条</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:class="['platform-overview-card', { 'is-active': activePlatform === 'kuaishouEticket' }]"
|
:class="['platform-overview-card', { 'is-active': activePlatform === 'kuaishouEticket' }]"
|
||||||
@@ -302,13 +259,6 @@ onMounted(loadConfigs)
|
|||||||
>
|
>
|
||||||
Agiso 店铺与消息
|
Agiso 店铺与消息
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="['switch-chip', { 'is-active': activePlatform === 'khhao' }]"
|
|
||||||
@click="switchPlatform('khhao')"
|
|
||||||
>
|
|
||||||
khhao 来源与同步
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:class="['switch-chip', { 'is-active': activePlatform === 'ninetyone' }]"
|
:class="['switch-chip', { 'is-active': activePlatform === 'ninetyone' }]"
|
||||||
@@ -350,26 +300,6 @@ onMounted(loadConfigs)
|
|||||||
:save-agiso-configs="saveAgisoConfigs"
|
:save-agiso-configs="saveAgisoConfigs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminPlatformKhhaoSection
|
|
||||||
v-else-if="activePlatform === 'khhao'"
|
|
||||||
:khhao-file-path="khhaoFilePath"
|
|
||||||
:khhao-state-file-path="khhaoStateFilePath"
|
|
||||||
:khhao-form="khhaoForm"
|
|
||||||
:khhao-saving="khhaoSaving"
|
|
||||||
:khhao-testing="khhaoTesting"
|
|
||||||
:khhao-querying="khhaoQuerying"
|
|
||||||
:khhao-syncing="khhaoSyncing"
|
|
||||||
:khhao-result-error="khhaoResultError"
|
|
||||||
:khhao-login-result="khhaoLoginResult"
|
|
||||||
:khhao-query-result="khhaoQueryResult"
|
|
||||||
:khhao-sync-result="khhaoSyncResult"
|
|
||||||
:khhao-auto-sync-state="khhaoAutoSyncState"
|
|
||||||
:handle-khhao-save-source="handleKhhaoSaveSource"
|
|
||||||
:handle-khhao-test-login="handleKhhaoTestLogin"
|
|
||||||
:handle-khhao-query-orders="handleKhhaoQueryOrders"
|
|
||||||
:handle-khhao-sync-orders="handleKhhaoSyncOrders"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AdminPlatformNinetyoneSection
|
<AdminPlatformNinetyoneSection
|
||||||
v-else-if="activePlatform === 'ninetyone'"
|
v-else-if="activePlatform === 'ninetyone'"
|
||||||
:ninetyone-loading="ninetyoneLoading"
|
:ninetyone-loading="ninetyoneLoading"
|
||||||
|
|||||||
+2
-2
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
## 2. 设计原则
|
## 2. 设计原则
|
||||||
|
|
||||||
- `91卡券` 作为**新的订单来源**,不复用 `agiso`、`khhao`。
|
- `91卡券` 作为**新的订单来源**,不复用 `agiso`。
|
||||||
- `91卡券` 不是新的履约执行器,而是外部售卖、订单输入和自动发货通道。
|
- `91卡券` 不是新的履约执行器,而是外部售卖、订单输入和自动发货通道。
|
||||||
- 当前项目真正的交付物仍然是 `claimUrl`。
|
- 当前项目真正的交付物仍然是 `claimUrl`。
|
||||||
- 查询接口返回 `20` 的判断标准是“领取链接已生成并可发”,不是“快手最终兑换完成”。
|
- 查询接口返回 `20` 的判断标准是“领取链接已生成并可发”,不是“快手最终兑换完成”。
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
原因:
|
原因:
|
||||||
|
|
||||||
- 避免与 `agiso` webhook 语义混淆;
|
- 避免与 `agiso` webhook 语义混淆;
|
||||||
- 避免复用 `khhao` 的后台拉单来源;
|
- 避免复用历史后台拉单来源;
|
||||||
- 便于后续单独做日志、排障和绑定配置。
|
- 便于后续单独做日志、排障和绑定配置。
|
||||||
|
|
||||||
## 4. 接口设计
|
## 4. 接口设计
|
||||||
|
|||||||
+1
-2
@@ -72,7 +72,7 @@ https://5cc8-193-176-84-38.ngrok-free.app/api/v1/webhooks/agiso/trade
|
|||||||
https://221329.cc.cd/api/v1/webhooks/agiso/trade
|
https://221329.cc.cd/api/v1/webhooks/agiso/trade
|
||||||
|
|
||||||
----
|
----
|
||||||
服务器 https://order.khhao.com/api/v1/webhooks/agiso/trade
|
服务器 生产域名见部署配置
|
||||||
|
|
||||||
调试 https://926d-193-176-84-20.ngrok-free.app/api/v1/webhooks/agiso/trade
|
调试 https://926d-193-176-84-20.ngrok-free.app/api/v1/webhooks/agiso/trade
|
||||||
|
|
||||||
@@ -81,4 +81,3 @@ https://221329.cc.cd/api/v1/webhooks/agiso/trade
|
|||||||
==========
|
==========
|
||||||
https://123.207.217.176/#/home 发货
|
https://123.207.217.176/#/home 发货
|
||||||
账号 17665234375 密码 yaochao11
|
账号 17665234375 密码 yaochao11
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
|
||||||
|
最快解决(强制使用 HTTP/2 协议)防止clash干扰
|
||||||
|
cloudflared tunnel --protocol http2 run --url http://localhost:80 mac-mini-tunnel
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
cloudflared tunnel create mac-mini-tunnel
|
cloudflared tunnel create mac-mini-tunnel
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
- **订单来源层**
|
- **订单来源层**
|
||||||
- `agiso`:Webhook 推送型;
|
- `agiso`:Webhook 推送型;
|
||||||
- `khhao`:账号密码登录 + 验证码识别 + Cookie 会话拉取型。
|
- `91卡券`:开放接口下单型。
|
||||||
- **履约执行层**
|
- **履约执行层**
|
||||||
- 当前以内置人工发货、腾讯领取兑换等执行器为主。
|
- 当前以内置人工发货、腾讯领取兑换等执行器为主。
|
||||||
|
|
||||||
@@ -17,19 +17,19 @@
|
|||||||
- 前端代码里根实例名显示为 `cloudtentacles`
|
- 前端代码里根实例名显示为 `cloudtentacles`
|
||||||
|
|
||||||
这个平台**不是订单数据源**,而是**订单创建后用于执行发货/兑换的外部履约平台**。
|
这个平台**不是订单数据源**,而是**订单创建后用于执行发货/兑换的外部履约平台**。
|
||||||
它和 `khhao` 的相同点只是“都需要后台登录”,但系统角色完全不同:
|
它和订单来源平台的系统角色完全不同:
|
||||||
|
|
||||||
- `khhao`:订单来源 / 上游平台;
|
- `91卡券`:订单来源 / 上游平台;
|
||||||
- `cloudtentacles`:履约执行 / 下游发货平台。
|
- `cloudtentacles`:履约执行 / 下游发货平台。
|
||||||
|
|
||||||
它的登录链路与 `khhao` 明显不同:
|
它的登录链路独立于订单来源:
|
||||||
|
|
||||||
- 不是图片验证码识别;
|
- 不是图片验证码识别;
|
||||||
- 是“账号 + 密码 + 手机号 + 短信验证码”登录;
|
- 是“账号 + 密码 + 手机号 + 短信验证码”登录;
|
||||||
- 登录参数不是明文提交,而是前端先做 `MD5 + RSA-OAEP(SHA-256)` 加密;
|
- 登录参数不是明文提交,而是前端先做 `MD5 + RSA-OAEP(SHA-256)` 加密;
|
||||||
- 登录成功后不是 Cookie 会话,而是返回 `token`,后续通过 `Authorization` 请求头访问接口。
|
- 登录成功后不是 Cookie 会话,而是返回 `token`,后续通过 `Authorization` 请求头访问接口。
|
||||||
|
|
||||||
因此它应当作为**履约执行器能力**接入,复用 `khhao` 的“HTTP 登录平台”经验,但不要按“来源平台”建模。
|
因此它应当作为**履约执行器能力**接入,不要按“来源平台”建模。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ MD5(password).toString(Hex)
|
|||||||
|
|
||||||
更合理的定位是:
|
更合理的定位是:
|
||||||
|
|
||||||
- 订单来源 `provider/platform/shopId` 仍然来自 `agiso` / `khhao`
|
- 订单来源 `provider/platform/shopId` 仍然来自 `agiso` / `91卡券`
|
||||||
- `cloudtentacles` 属于**履约执行器 / 外部发货平台**
|
- `cloudtentacles` 属于**履约执行器 / 外部发货平台**
|
||||||
|
|
||||||
因此它更适合进入这条链路:
|
因此它更适合进入这条链路:
|
||||||
@@ -207,9 +207,9 @@ MD5(password).toString(Hex)
|
|||||||
- 发货业务服务
|
- 发货业务服务
|
||||||
- 履约执行适配层
|
- 履约执行适配层
|
||||||
|
|
||||||
## 4.3 不建议直接照抄 khhao 的原因
|
## 4.3 不建议照搬来源平台的原因
|
||||||
|
|
||||||
`khhao` 当前链路核心是:
|
历史拉单平台链路核心是:
|
||||||
|
|
||||||
- 打开登录页
|
- 打开登录页
|
||||||
- 拉验证码图片
|
- 拉验证码图片
|
||||||
@@ -441,7 +441,7 @@ platforms: {
|
|||||||
|
|
||||||
## 6.2 持久化来源配置
|
## 6.2 持久化来源配置
|
||||||
|
|
||||||
建议和 `khhao` 一样,先用数据文件落地,不急着进数据库。
|
建议先用数据文件落地,不急着进数据库。
|
||||||
|
|
||||||
建议文件:
|
建议文件:
|
||||||
|
|
||||||
@@ -563,7 +563,7 @@ POST /api/v1/admin/platforms/cloudtentacles/validate-session
|
|||||||
|
|
||||||
## 8. 后台 UI 设计建议
|
## 8. 后台 UI 设计建议
|
||||||
|
|
||||||
第一版 UI 不需要太复杂,但应与 `khhao` 分开,避免把“订单来源配置”和“履约平台配置”混在一起。
|
第一版 UI 不需要太复杂,应避免把“订单来源配置”和“履约平台配置”混在一起。
|
||||||
|
|
||||||
建议在“平台配置”页新增独立区块,归类到“履约平台”分组:
|
建议在“平台配置”页新增独立区块,归类到“履约平台”分组:
|
||||||
|
|
||||||
@@ -606,7 +606,7 @@ POST /api/v1/admin/platforms/cloudtentacles/validate-session
|
|||||||
|
|
||||||
注意:
|
注意:
|
||||||
|
|
||||||
- 不要把这个平台和 `khhao` 放在同一张混合表单里;
|
- 不要把这个平台和订单来源放在同一张混合表单里;
|
||||||
- 页面上要明确区分:
|
- 页面上要明确区分:
|
||||||
- 订单来源平台
|
- 订单来源平台
|
||||||
- 履约执行平台
|
- 履约执行平台
|
||||||
@@ -656,7 +656,7 @@ POST /api/v1/admin/platforms/cloudtentacles/validate-session
|
|||||||
|
|
||||||
也就是说:
|
也就是说:
|
||||||
|
|
||||||
- `agiso/khhao` 决定“订单从哪里来”
|
- `agiso/91卡券` 决定“订单从哪里来”
|
||||||
- `cloudtentacles` 决定“订单怎么发出去”
|
- `cloudtentacles` 决定“订单怎么发出去”
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -717,12 +717,12 @@ POST /api/v1/admin/platforms/cloudtentacles/validate-session
|
|||||||
|
|
||||||
最重要的不是把接口先堆进去,而是先把结构定对:
|
最重要的不是把接口先堆进去,而是先把结构定对:
|
||||||
|
|
||||||
- `khhao` 型平台:验证码图片 + Cookie
|
- 历史拉单型平台:验证码图片 + Cookie
|
||||||
- `cloudtentacles` 型平台:短信码 + RSA 加密 + token
|
- `cloudtentacles` 型平台:短信码 + RSA 加密 + token
|
||||||
|
|
||||||
二者都属于“后台登录型平台”,但在系统分层上不同:
|
二者都属于“后台登录型平台”,但在系统分层上不同:
|
||||||
|
|
||||||
- `khhao`:来源层
|
- `91卡券`:来源层
|
||||||
- `cloudtentacles`:履约层
|
- `cloudtentacles`:履约层
|
||||||
|
|
||||||
因此第一版推荐结论是:
|
因此第一版推荐结论是:
|
||||||
|
|||||||
@@ -1,403 +0,0 @@
|
|||||||
# khhao 新数据来源接入设计
|
|
||||||
|
|
||||||
## 1. 背景
|
|
||||||
|
|
||||||
当前系统的订单来源只有 `agiso`,核心模式是:
|
|
||||||
|
|
||||||
- 上游通过 webhook 推送订单事件;
|
|
||||||
- 后端按 `provider + platform + shopId + platformOrderId` 幂等落库;
|
|
||||||
- 命中履约绑定后生成任务、领取链接、自动发货等后续流程。
|
|
||||||
|
|
||||||
现在需要新增一个来源:`admin.khhao.com` 这一套已开发好的平台后台。
|
|
||||||
|
|
||||||
和 `agiso` 的差异是:
|
|
||||||
|
|
||||||
- **不是 webhook 推送**,而是**账号密码登录 + 验证码识别 + Cookie 会话查询**;
|
|
||||||
- 订单数据来自平台接口查询;
|
|
||||||
- 初期目标是先完成**登录能力**与**订单查询能力**,再决定是否做定时拉取/增量同步。
|
|
||||||
|
|
||||||
## 2. 已确认事实(来自 `2026-05-02` 的 HAR)
|
|
||||||
|
|
||||||
HAR 文件:`/Users/yml/Desktop/抓包/登陆.har`
|
|
||||||
|
|
||||||
已确认接口链路:
|
|
||||||
|
|
||||||
1. 打开登录页
|
|
||||||
`GET https://admin.khhao.com/c/login/index.php`
|
|
||||||
|
|
||||||
2. 获取验证码图片
|
|
||||||
`GET https://admin.khhao.com/verify_img.php`
|
|
||||||
|
|
||||||
3. 提交登录表单
|
|
||||||
`POST https://admin.khhao.com/c/login/index.php`
|
|
||||||
表单字段:
|
|
||||||
- `username`
|
|
||||||
- `password`
|
|
||||||
- `img_code`
|
|
||||||
|
|
||||||
4. 登录成功响应
|
|
||||||
```json
|
|
||||||
{"errcode":0,"msg":"登陆成功","url":"/c/payOrder/index.php"}
|
|
||||||
```
|
|
||||||
|
|
||||||
5. 查询订单列表
|
|
||||||
`GET https://admin.khhao.com/c/payOrder/get.php?page=1&limit=50`
|
|
||||||
|
|
||||||
6. 当前 HAR 中,登录依赖 `PHPSESSID` Cookie 维持会话。
|
|
||||||
|
|
||||||
订单列表响应里已看到这些关键字段:
|
|
||||||
|
|
||||||
- `ordersn`:外部订单号
|
|
||||||
- `pingtai` / `pingtaiName`:平台标识/平台名
|
|
||||||
- `shopid` / `shopName`:店铺
|
|
||||||
- `goodid` / `goodName`:商品
|
|
||||||
- `sku`:SKU
|
|
||||||
- `fee`:成交金额
|
|
||||||
- `chengben` / `lirun`:成本/利润
|
|
||||||
- `num`:数量
|
|
||||||
- `status` / `statusName`:订单状态
|
|
||||||
- `addtime`:下单/录入时间
|
|
||||||
|
|
||||||
## 3. 设计目标
|
|
||||||
|
|
||||||
### 3.1 MVP 目标
|
|
||||||
|
|
||||||
先做最小可用能力:
|
|
||||||
|
|
||||||
1. 后端可通过账号密码自动登录 khhao;
|
|
||||||
2. 自动拉取验证码图片,并用现有 `ddddocr` 识别;
|
|
||||||
3. 登录成功后带 Cookie 调用订单查询接口;
|
|
||||||
4. 能按统一结构把 khhao 订单转换为内部订单模型;
|
|
||||||
5. 提供一个**手动查询 / 手动同步**入口,先不直接接入自动履约主流程。
|
|
||||||
|
|
||||||
### 3.2 第二阶段目标
|
|
||||||
|
|
||||||
1. 支持按时间窗口或页码批量拉取订单;
|
|
||||||
2. 增加定时轮询同步;
|
|
||||||
3. 将 khhao 订单正式纳入现有订单履约流程;
|
|
||||||
4. 视需要增加远端订单详情接口接入。
|
|
||||||
|
|
||||||
## 4. 关键设计决策
|
|
||||||
|
|
||||||
## 4.1 新来源标识
|
|
||||||
|
|
||||||
建议:
|
|
||||||
|
|
||||||
- `provider = 'khhao'`
|
|
||||||
- `platform = 由返回字段映射`
|
|
||||||
|
|
||||||
原因:
|
|
||||||
|
|
||||||
- 现有系统已经以 `provider/platform` 作为来源分层;
|
|
||||||
- khhao 本身更像“聚合后台/运营后台”,其下可能有多个平台;
|
|
||||||
- 后续如果 `pingtai=3` 代表快手、还有其它平台,也可以继续扩展。
|
|
||||||
|
|
||||||
建议先做一个映射表,例如:
|
|
||||||
|
|
||||||
- `3 -> kuaishou`
|
|
||||||
- 未识别值 -> `unknown`
|
|
||||||
|
|
||||||
同时把原始 `pingtai`、`pingtaiName` 放进 `raw_payload_json` 保留证据。
|
|
||||||
|
|
||||||
## 4.2 接入模式
|
|
||||||
|
|
||||||
`agiso` 是 **push**;`khhao` 应设计成 **pull**。
|
|
||||||
|
|
||||||
因此不要强行塞进 `webhook-service`,而是单独建立:
|
|
||||||
|
|
||||||
- 登录会话服务
|
|
||||||
- 验证码识别服务
|
|
||||||
- 订单查询服务
|
|
||||||
- 订单转换/同步服务
|
|
||||||
|
|
||||||
## 4.3 会话管理
|
|
||||||
|
|
||||||
建议优先采用 **纯 HTTP + Cookie Jar**,不引入浏览器自动化。
|
|
||||||
|
|
||||||
原因:
|
|
||||||
|
|
||||||
- HAR 显示登录流程很简单,没有前端加密、签名或复杂跳转;
|
|
||||||
- 验证码是普通图片;
|
|
||||||
- 登录后查询接口是标准 XHR;
|
|
||||||
- 用 HTTP 实现更稳定,也更适合服务端定时任务。
|
|
||||||
|
|
||||||
只有在后续发现以下情况时,再退回浏览器方案:
|
|
||||||
|
|
||||||
- 服务端校验动态前端字段;
|
|
||||||
- 有 JS 生成签名;
|
|
||||||
- Cookie/会话依赖浏览器行为;
|
|
||||||
- 验证码识别成功但仍反复登录失败。
|
|
||||||
|
|
||||||
## 4.4 OCR 复用
|
|
||||||
|
|
||||||
现有 OCR 子服务已经使用:
|
|
||||||
|
|
||||||
- `apps/backend/subservices/ocr-worker`
|
|
||||||
- `ddddocr>=1.6.1`
|
|
||||||
|
|
||||||
因此不需要新增 OCR 技术栈,只需要把 khhao 验证码图片作为图片字节传给现有 OCR 能力即可。
|
|
||||||
|
|
||||||
建议新增一个更通用的方法,而不是继续绑定在 “Tencent” 语义上:
|
|
||||||
|
|
||||||
- 现状:`recognizeTencentCaptcha`
|
|
||||||
- 建议新增:`recognizeImageCaptcha`
|
|
||||||
|
|
||||||
然后:
|
|
||||||
|
|
||||||
- 腾讯验证码继续走原接口或复用通用接口;
|
|
||||||
- khhao 登录验证码直接走通用接口。
|
|
||||||
|
|
||||||
## 5. 推荐模块拆分
|
|
||||||
|
|
||||||
建议新增目录:
|
|
||||||
|
|
||||||
`apps/backend/src/services/platforms/khhao/`
|
|
||||||
|
|
||||||
建议模块:
|
|
||||||
|
|
||||||
### 5.1 `captcha-service.js`
|
|
||||||
|
|
||||||
职责:
|
|
||||||
|
|
||||||
- 请求 `/verify_img.php`
|
|
||||||
- 读取图片字节
|
|
||||||
- 调 OCR
|
|
||||||
- 返回识别结果与调试信息
|
|
||||||
|
|
||||||
建议返回:
|
|
||||||
|
|
||||||
- `captchaText`
|
|
||||||
- `imageBase64`(仅调试阶段可选)
|
|
||||||
- `cookieHeaders`
|
|
||||||
|
|
||||||
### 5.2 `session-service.js`
|
|
||||||
|
|
||||||
职责:
|
|
||||||
|
|
||||||
- 初始化 Cookie 容器;
|
|
||||||
- 访问登录页,拿到初始 `PHPSESSID`;
|
|
||||||
- 获取验证码并识别;
|
|
||||||
- 提交登录表单;
|
|
||||||
- 校验登录响应;
|
|
||||||
- 返回已登录会话对象。
|
|
||||||
|
|
||||||
建议会话对象:
|
|
||||||
|
|
||||||
- `baseUrl`
|
|
||||||
- `cookieHeader`
|
|
||||||
- `cookieMap`
|
|
||||||
- `loggedInAt`
|
|
||||||
|
|
||||||
### 5.3 `order-query-service.js`
|
|
||||||
|
|
||||||
职责:
|
|
||||||
|
|
||||||
- 调用 `/c/payOrder/get.php`
|
|
||||||
- 支持分页参数:`page`、`limit`
|
|
||||||
- 后续支持筛选参数(如果继续抓到更多接口)
|
|
||||||
- 返回原始平台订单列表
|
|
||||||
|
|
||||||
### 5.4 `order-mapper-service.js`
|
|
||||||
|
|
||||||
职责:
|
|
||||||
|
|
||||||
- 将 khhao 原始订单映射成内部统一订单结构;
|
|
||||||
- 归一化金额、数量、状态、平台、店铺、SKU;
|
|
||||||
- 产出可供 `order-service` 或新同步服务消费的对象。
|
|
||||||
|
|
||||||
建议映射:
|
|
||||||
|
|
||||||
- `platformOrderId <- ordersn`
|
|
||||||
- `shopId <- shopid`
|
|
||||||
- `shopName <- shopName`
|
|
||||||
- `totalAmount <- fee`(转分)
|
|
||||||
- `items[0].skuCode <- sku`
|
|
||||||
- `items[0].skuName <- goodName`
|
|
||||||
- `items[0].quantity <- num`
|
|
||||||
- `rawPayload <- 原始整条记录`
|
|
||||||
|
|
||||||
注意:
|
|
||||||
|
|
||||||
- 当前列表接口一条记录看起来像“一单一商品”,但不能假设永远如此;
|
|
||||||
- 第一版可以先按单商品映射;
|
|
||||||
- 如果后续发现有多商品订单,再调整内部映射。
|
|
||||||
|
|
||||||
### 5.5 `order-sync-service.js`
|
|
||||||
|
|
||||||
职责:
|
|
||||||
|
|
||||||
- 批量查询 khhao 订单;
|
|
||||||
- 调 `order-mapper-service` 转换;
|
|
||||||
- 调内部订单 upsert;
|
|
||||||
- 记录同步结果、失败原因、游标信息。
|
|
||||||
|
|
||||||
这里建议不要直接复用 `upsertOrderFromWebhook(event)`,因为它的命名和语义绑定 webhook。
|
|
||||||
|
|
||||||
更合理的演进方向:
|
|
||||||
|
|
||||||
- 抽一个来源无关的方法,例如 `upsertOrderFromSource(event)`;
|
|
||||||
- `webhook-service` 继续只负责 webhook 解析;
|
|
||||||
- `khhao` 同步服务直接调用来源无关的 upsert。
|
|
||||||
|
|
||||||
## 6. 配置设计
|
|
||||||
|
|
||||||
## 6.1 运行时配置
|
|
||||||
|
|
||||||
建议在:
|
|
||||||
|
|
||||||
- `apps/backend/config/default.cjs`
|
|
||||||
- `apps/backend/src/types/runtime-config.js`
|
|
||||||
- `apps/backend/src/config/runtime.js`
|
|
||||||
|
|
||||||
增加:
|
|
||||||
|
|
||||||
```js
|
|
||||||
platforms: {
|
|
||||||
khhao: {
|
|
||||||
baseUrl: 'https://admin.khhao.com',
|
|
||||||
timeoutMs: 5000,
|
|
||||||
loginPath: '/c/login/index.php',
|
|
||||||
captchaPath: '/verify_img.php',
|
|
||||||
orderListPath: '/c/payOrder/get.php',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6.2 凭据配置
|
|
||||||
|
|
||||||
不建议把账号密码直接写进仓库文档或默认配置。
|
|
||||||
|
|
||||||
建议通过以下方式之一管理:
|
|
||||||
|
|
||||||
1. 环境变量;
|
|
||||||
2. `apps/backend/data/khhao-sources.json`;
|
|
||||||
3. 后台平台配置页维护。
|
|
||||||
|
|
||||||
建议结构:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"sourceKey": "khhao-main",
|
|
||||||
"enabled": true,
|
|
||||||
"baseUrl": "https://admin.khhao.com",
|
|
||||||
"username": "******",
|
|
||||||
"password": "******",
|
|
||||||
"defaultPlatform": "kuaishou"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
第一阶段最省成本的方案:**本地文件 + 环境变量覆盖**。
|
|
||||||
|
|
||||||
## 7. 与现有系统的衔接方式
|
|
||||||
|
|
||||||
## 7.1 第一阶段:只做手动查询/同步
|
|
||||||
|
|
||||||
建议新增后台接口,例如:
|
|
||||||
|
|
||||||
- `POST /api/v1/admin/platform-config/khhao/test-login`
|
|
||||||
- `POST /api/v1/admin/platform-config/khhao/query-orders`
|
|
||||||
- `POST /api/v1/admin/platform-config/khhao/sync-orders`
|
|
||||||
|
|
||||||
这样好处是:
|
|
||||||
|
|
||||||
- 不会影响现有 `agiso` 主链路;
|
|
||||||
- 可以先验证 OCR、登录、Cookie、字段映射;
|
|
||||||
- 便于比对真实订单数据。
|
|
||||||
|
|
||||||
## 7.2 第二阶段:纳入统一订单流程
|
|
||||||
|
|
||||||
当字段映射稳定后,再把 khhao 同步数据接到统一订单主链路:
|
|
||||||
|
|
||||||
- 来源查询 -> 订单映射 -> 通用 upsert -> 履约绑定 -> 任务生成
|
|
||||||
|
|
||||||
这一步需要补充:
|
|
||||||
|
|
||||||
- khhao 订单状态到内部 `orderStatus/payStatus` 的映射;
|
|
||||||
- 去重与增量同步策略;
|
|
||||||
- 同一来源重复登录的频率控制;
|
|
||||||
- 同步审计日志。
|
|
||||||
|
|
||||||
## 8. 状态映射建议
|
|
||||||
|
|
||||||
当前 HAR 仅看到:
|
|
||||||
|
|
||||||
- `status = 2` => `已发货`
|
|
||||||
- `status = 0` => `未打款`
|
|
||||||
|
|
||||||
第一阶段不要写死太多业务含义,建议:
|
|
||||||
|
|
||||||
### 内部订单状态
|
|
||||||
|
|
||||||
- `status=0` -> `created`
|
|
||||||
- `status=2` -> `delivered`
|
|
||||||
- 未知值 -> `unknown`
|
|
||||||
|
|
||||||
### 内部支付状态
|
|
||||||
|
|
||||||
- `status=0` -> `unpaid`
|
|
||||||
- `status=2` -> `paid`
|
|
||||||
- 未知值 -> `unknown`
|
|
||||||
|
|
||||||
但这只是 **初版推断**,最终要以更多样本验证。
|
|
||||||
|
|
||||||
## 9. 风险点
|
|
||||||
|
|
||||||
1. **验证码识别率**
|
|
||||||
- `ddddocr` 大概率可用,但识别失败需要自动重试;
|
|
||||||
- 建议单次登录允许 `2~3` 次验证码重试。
|
|
||||||
|
|
||||||
2. **登录态过期**
|
|
||||||
- 需要判断何时复用 Cookie,何时重新登录;
|
|
||||||
- 第一阶段可以每次查询都重新登录,先求稳定。
|
|
||||||
|
|
||||||
3. **订单列表不是详情接口**
|
|
||||||
- 当前拿到的是列表接口,不排除存在订单详情页/详情接口;
|
|
||||||
- 第一版先以列表字段落库;
|
|
||||||
- 如果后续履约需要更多字段,再补抓详情接口。
|
|
||||||
|
|
||||||
4. **平台字段含义未完全确认**
|
|
||||||
- `pingtai=3` 目前只从样本推断是快手;
|
|
||||||
- 需要再抓几个平台样本确认映射表。
|
|
||||||
|
|
||||||
5. **同步边界**
|
|
||||||
- khhao 是拉取式来源,必须明确增量条件;
|
|
||||||
- 第一版建议按 `page/limit` 手动同步,先不做自动游标。
|
|
||||||
|
|
||||||
## 10. 推荐实施顺序
|
|
||||||
|
|
||||||
### 阶段 A:打通基础能力
|
|
||||||
|
|
||||||
1. 增加 khhao 配置结构;
|
|
||||||
2. 增加通用图片 OCR 能力;
|
|
||||||
3. 实现登录 + Cookie 会话;
|
|
||||||
4. 实现订单列表查询;
|
|
||||||
5. 增加后台“测试登录 / 查询订单”接口。
|
|
||||||
|
|
||||||
### 阶段 B:打通内部模型
|
|
||||||
|
|
||||||
1. 实现 khhao -> 内部订单映射;
|
|
||||||
2. 提炼 webhook 无关的通用 order upsert;
|
|
||||||
3. 增加手动同步接口;
|
|
||||||
4. 在后台展示同步结果。
|
|
||||||
|
|
||||||
### 阶段 C:进入自动履约
|
|
||||||
|
|
||||||
1. 增量同步;
|
|
||||||
2. 定时任务;
|
|
||||||
3. 状态回补;
|
|
||||||
4. 接入履约绑定、任务生成、消息通知。
|
|
||||||
|
|
||||||
## 11. 本次设计结论
|
|
||||||
|
|
||||||
结论很明确:
|
|
||||||
|
|
||||||
- khhao 不应该按 `agiso webhook` 思路接;
|
|
||||||
- 应该作为一个新的 **pull 型来源** 单独建接入层;
|
|
||||||
- OCR 直接复用现有 `ddddocr` 子服务;
|
|
||||||
- 第一阶段先做**手动查询/同步**最稳;
|
|
||||||
- 等字段、状态、平台映射稳定后,再接入统一履约主流程。
|
|
||||||
|
|
||||||
+5
-6
@@ -28,7 +28,7 @@
|
|||||||
- `apps/frontend/src/views/admin/AdminPlatformShopsView.vue` 的平台状态和行为已经拆到 composable
|
- `apps/frontend/src/views/admin/AdminPlatformShopsView.vue` 的平台状态和行为已经拆到 composable
|
||||||
- 已有:
|
- 已有:
|
||||||
- `useAdminAgisoPlatform.ts`
|
- `useAdminAgisoPlatform.ts`
|
||||||
- `useAdminKhhaoPlatform.ts`
|
- `useAdminNinetyonePlatform.ts`
|
||||||
- `useAdminKuaishouEticketPlatform.ts`
|
- `useAdminKuaishouEticketPlatform.ts`
|
||||||
- `useAdminCloudtentaclesPlatform.ts`
|
- `useAdminCloudtentaclesPlatform.ts`
|
||||||
|
|
||||||
@@ -38,13 +38,13 @@
|
|||||||
- `60c2d91`、`254a031`、`8f6a72f` 完成平台区块拆分
|
- `60c2d91`、`254a031`、`8f6a72f` 完成平台区块拆分
|
||||||
- 四个平台区块已经分别下沉为独立组件:
|
- 四个平台区块已经分别下沉为独立组件:
|
||||||
- `AdminPlatformAgisoSection.vue`
|
- `AdminPlatformAgisoSection.vue`
|
||||||
- `AdminPlatformKhhaoSection.vue`
|
- `AdminPlatformNinetyoneSection.vue`
|
||||||
- `AdminPlatformKuaishouEticketSection.vue`
|
- `AdminPlatformKuaishouEticketSection.vue`
|
||||||
- `AdminPlatformCloudtentaclesSection.vue`
|
- `AdminPlatformCloudtentaclesSection.vue`
|
||||||
|
|
||||||
- `58f25fa` 抽离平台配置结果信息卡片
|
- `58f25fa` 抽离平台配置结果信息卡片
|
||||||
- 新增 `AdminResultCard.vue`
|
- 新增 `AdminResultCard.vue`
|
||||||
- `khhao` / `快手核销` / `cloudtentacles` 的重复结果卡片已统一
|
- `快手核销` / `cloudtentacles` 的重复结果卡片已统一
|
||||||
|
|
||||||
### 2. `session-proof.js` 已完成第一轮拆分
|
### 2. `session-proof.js` 已完成第一轮拆分
|
||||||
|
|
||||||
@@ -72,7 +72,6 @@
|
|||||||
- `d11879f` 下沉后台履约规则校验编排逻辑
|
- `d11879f` 下沉后台履约规则校验编排逻辑
|
||||||
- `8c9d487` 下沉后台云触手会话结果组装逻辑
|
- `8c9d487` 下沉后台云触手会话结果组装逻辑
|
||||||
- `dad646b` 抽离后台平台配置写入归一化逻辑
|
- `dad646b` 抽离后台平台配置写入归一化逻辑
|
||||||
- `60b84b2` 下沉后台 khhao 平台辅助组装逻辑
|
|
||||||
- `3a7c311` 按平台拆分后台平台配置服务
|
- `3a7c311` 按平台拆分后台平台配置服务
|
||||||
|
|
||||||
当前 `apps/backend/src/services/admin/platform-config/` 已形成按职责拆分的目录:
|
当前 `apps/backend/src/services/admin/platform-config/` 已形成按职责拆分的目录:
|
||||||
@@ -80,7 +79,7 @@
|
|||||||
- `service.js`
|
- `service.js`
|
||||||
- 只保留统一导出入口
|
- 只保留统一导出入口
|
||||||
- `agiso-service.js`
|
- `agiso-service.js`
|
||||||
- `khhao-service.js`
|
- `ninetyone-service.js`
|
||||||
- `kuaishou-eticket-service.js`
|
- `kuaishou-eticket-service.js`
|
||||||
- `cloudtentacles-service.js`
|
- `cloudtentacles-service.js`
|
||||||
- `fulfillment-bindings-service.js`
|
- `fulfillment-bindings-service.js`
|
||||||
@@ -296,7 +295,7 @@
|
|||||||
1. 拆分前端后台管理 API 客户端
|
1. 拆分前端后台管理 API 客户端
|
||||||
2. 拆分平台配置页脚本逻辑
|
2. 拆分平台配置页脚本逻辑
|
||||||
3. 抽离平台配置页共享样式
|
3. 抽离平台配置页共享样式
|
||||||
4. 拆分 Agiso / khhao / 快手核销 / cloudtentacles 四个平台区块
|
4. 拆分 Agiso / 91卡券 / 快手核销 / cloudtentacles 四个平台区块
|
||||||
5. 抽离结果信息卡片
|
5. 抽离结果信息卡片
|
||||||
6. 拆分 `session-proof.js`
|
6. 拆分 `session-proof.js`
|
||||||
7. 重构后台 `platform-config` 目录并完成按平台拆分
|
7. 重构后台 `platform-config` 目录并完成按平台拆分
|
||||||
|
|||||||
Reference in New Issue
Block a user