diff --git a/11.txt b/11.txt index b09e489f..842c0757 100644 --- a/11.txt +++ b/11.txt @@ -1,16 +1,22 @@ +https://cdk.linux.do/receive/069af7bc-7915-48b6-834b-cf52eb00b798 -这条“半自动+人工”链路已经落下第一版了。 -后端现在新增了 support 角色,并把半自动档案 tencent_claim_assisted 接进了履约目录。用户侧领取页现在只允许“创建登录会话 + 扫码登录”,如果任务是半自动客服模式,用户自己再点“确认角色/开始兑换”会被后端直接拦住;客服则可以在任务详情里拿领取链接、看登录后的复核截图、确认角色、再触发后端自动兑换。关键入口在 admin-service.js、claim-session-service.js、session.js、fulfillment-bootstrap-service.js。 +cloudflared tunnel create mac-mini-tunnel -权限也一起收紧了。support 看不到库存凭据、原始 claim token、库存管理、Webhook、配置、审计、用户管理;任务详情接口本身已经做了脱敏,不只是前端隐藏。前端这边补了客服角色导航、路由限制和任务操作按钮,配置页也新增了“腾讯领取兑换(半自动+人工)”选项,相关界面在 AdminTaskDetailView.vue、AdminLayout.vue、AdminFulfillmentBindingsView.vue、AdminUsersView.vue。 +yml@ymlMacBook-Air ~ % cloudflared tunnel create mac-mini-tunnel +Tunnel credentials written to /Users/yml/.cloudflared/562f9639-71f2-44e0-a5ef-b34bd4e215e5.json. cloudflared chose this file based on where your origin certificate was found. Keep this file secret. To revoke these credentials, delete the tunnel. -我做了后端文件的语法检查,node --check 通过;前端没法做完整 typecheck,因为当前环境里 vue-tsc 没装,npm run typecheck 会直接报 vue-tsc: command not found。 -你现在可以直接这样试: +Created tunnel mac-mini-tunnel with id 562f9639-71f2-44e0-a5ef-b34bd4e215e5 +yml@ymlMacBook-Air ~ % -后台用户里新增一个 support 账号。 -在履约配置里把目标商品改成 tencent_claim_assisted。 -下一个测试单后,让用户打开领取链接并扫码登录。 -客服进入任务详情,确认能看到领取链接和复核截图,但看不到兑换码。 -客服点“客服确认角色”再点“客服开始兑换”。 -如果你愿意,我下一步可以继续把“任务列表页也做客服专用态优化”以及“客服复核截图/角色状态做成更明显的状态块”。 \ No newline at end of file + +221329.cc.cd + +4. # 例子:把 web.yourdomain.com 指向刚才创建的隧道 +cloudflared tunnel route dns mac-mini-tunnel 221329.cc.cd + + +5. 启动隧道(临时/测试) +如果你只是临时跑一个服务(比如你 OrbStack 里的容器映射到了本地 80 端口): + +cloudflared tunnel run --url http://localhost:80 mac-mini-tunnel \ No newline at end of file diff --git a/apps/backend/src/routes/admin/platform-config.js b/apps/backend/src/routes/admin/platform-config.js index ab30fa2d..41f2b767 100644 --- a/apps/backend/src/routes/admin/platform-config.js +++ b/apps/backend/src/routes/admin/platform-config.js @@ -3,6 +3,7 @@ import { Router } from 'express' import { getAdminAgisoShopConfigs, getAdminFulfillmentBindingConfigs, + lookupAdminFulfillmentBindingOrder, updateAdminAgisoShopConfigs, updateAdminFulfillmentBindingConfigs, } from '../../services/admin/admin-service.js' @@ -48,6 +49,30 @@ router.get('/platform-config/fulfillment-bindings', createJsonHandler( }, )) +router.post('/platform-config/fulfillment-bindings/lookup-order', createJsonHandler( + (req) => lookupAdminFulfillmentBindingOrder(req.body), + { + successMessage: '订单商品查询成功', + errorMessage: '手动查询订单商品失败', + scope: '[admin/platform-config/fulfillment-bindings/lookup-order]', + audit: (req, data) => ({ + action: 'platform_fulfillment_order_lookup', + targetType: 'platform_config', + targetId: [ + data.order?.provider || 'unknown', + data.order?.platform || 'unknown', + data.order?.shopId || 'unknown', + data.order?.platformOrderId || 'unknown', + ].join(':'), + data: { + itemCount: Array.isArray(data.items) ? data.items.length : 0, + shopId: data.order?.shopId || String(req.body?.shopId || '').trim(), + platformOrderId: data.order?.platformOrderId || String(req.body?.platformOrderId || '').trim(), + }, + }), + }, +)) + router.post('/platform-config/fulfillment-bindings', createJsonHandler( (req) => updateAdminFulfillmentBindingConfigs(req.body), { diff --git a/apps/backend/src/services/admin/admin-service.js b/apps/backend/src/services/admin/admin-service.js index 54ffc5cd..6cac2a45 100644 --- a/apps/backend/src/services/admin/admin-service.js +++ b/apps/backend/src/services/admin/admin-service.js @@ -8,6 +8,7 @@ import { getAgisoShopsFilePath, saveAgisoShopConfigMap, } from '../platforms/agiso/shop-config-service.js' +import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js' import { getOrderFulfillmentBindingConfigs, getOrderFulfillmentBindingsFilePath, @@ -688,22 +689,138 @@ export async function getAdminFulfillmentBindingConfigs() { return { filePath: getOrderFulfillmentBindingsFilePath(), bindings: bindings.map(mapAdminFulfillmentBindingConfigItem), - observedProducts: rowsResult.rows.map((row) => { - const item = { - provider: String(row.provider || '').trim(), - platform: String(row.platform || '').trim(), - shopId: String(row.shop_id || '').trim(), - shopName: String(row.shop_name || '').trim(), - externalItemId: String(row.external_item_id || '').trim(), - externalSkuCode: String(row.external_sku_code || '').trim(), - externalSkuName: String(row.external_sku_name || '').trim(), - latestSeenAt: row.latest_seen_at || null, - orderItemCount: Number(row.order_item_count || 0), + observedProducts: rowsResult.rows.map((row) => mapAdminObservedProductItem({ + provider: String(row.provider || '').trim(), + platform: String(row.platform || '').trim(), + shopId: String(row.shop_id || '').trim(), + shopName: String(row.shop_name || '').trim(), + externalItemId: String(row.external_item_id || '').trim(), + externalSkuCode: String(row.external_sku_code || '').trim(), + externalSkuName: String(row.external_sku_name || '').trim(), + latestSeenAt: row.latest_seen_at || null, + orderItemCount: Number(row.order_item_count || 0), + }, bindings)), + } +} + +export async function lookupAdminFulfillmentBindingOrder(payload = {}) { + const provider = String(payload.provider || 'agiso').trim() || 'agiso' + const platform = String(payload.platform || 'xianyu').trim() || 'xianyu' + const shopId = String(payload.shopId || '').trim() + const platformOrderId = String(payload.platformOrderId || '').trim() + + if (!shopId) { + throw createHttpError('请先填写店铺 ID', { + statusCode: 400, + errorCode: 'admin_fulfillment_lookup_missing_shop_id', + }) + } + + if (!platformOrderId) { + throw createHttpError('请先填写平台订单号', { + statusCode: 400, + errorCode: 'admin_fulfillment_lookup_missing_platform_order_id', + }) + } + + if (provider !== 'agiso' || platform !== 'xianyu') { + throw createHttpError('目前仅支持 Agiso 咸鱼订单手动查询', { + statusCode: 400, + errorCode: 'admin_fulfillment_lookup_platform_not_supported', + }) + } + + const detailResult = await enrichAgisoXianyuTradeOrder({ + provider, + platform, + shopId, + shopName: '', + platformOrderId, + orderStatus: 'created', + payStatus: 'unpaid', + buyerId: '', + buyerName: '', + receiverContact: '', + totalAmount: 0, + currency: 'CNY', + paidAt: null, + rawPayload: {}, + items: [], + }, { + requestId: `admin-fulfillment-lookup:${shopId}:${platformOrderId}`, + }) + + const detail = isPlainObject(detailResult?.parsed) ? detailResult.parsed : {} + const items = Array.isArray(detail.items) ? detail.items : [] + + if (items.length === 0) { + const detailReason = String(detailResult?.reason || '').trim() + const detailMessage = String(detailResult?.errorMessage || '').trim() + let message = detailMessage + + if (!message && detailReason === 'missing_config') { + message = '当前店铺缺少订单详情查询配置,请先检查 accessToken、appSecret 和详情接口地址' + } + + if (!message) { + message = `未查询到订单 ${platformOrderId} 的商品明细` + } + + throw createHttpError(message, { + statusCode: 404, + errorCode: 'admin_fulfillment_lookup_order_items_not_found', + }) + } + + const bindings = getOrderFulfillmentBindingConfigs() + const resolvedShopName = pickFirstNonEmpty([ + detail.shopName, + getAgisoShopConfig(shopId)?.shopName, + ]) + + return { + order: { + provider, + platform, + shopId, + shopName: resolvedShopName, + platformOrderId, + buyerName: String(detail.buyerName || '').trim(), + totalAmountFen: Number(detail.totalAmount || 0), + totalAmount: formatFenToAmount(detail.totalAmount), + paidAt: detail.paidAt || null, + enriched: Boolean(detailResult?.enriched), + enrichReason: String(detailResult?.reason || '').trim(), + errorMessage: String(detailResult?.errorMessage || '').trim(), + }, + items: items.map((item, index) => { + const observed = { + provider, + platform, + shopId, + shopName: resolvedShopName, + externalItemId: pickFirstNonEmpty([item?.externalItemId, item?.itemId]), + externalSkuCode: pickFirstNonEmpty([item?.externalSkuCode, item?.skuCode, item?.externalItemId, item?.itemId]), + externalSkuName: pickFirstNonEmpty([item?.externalSkuName, item?.skuName]), + latestSeenAt: null, + orderItemCount: Math.max(1, Number(item?.quantity || 0) || 1), } return { - ...item, - configured: bindings.some((binding) => matchesObservedProduct(binding, item)), + lineId: [ + platformOrderId, + index + 1, + observed.externalSkuCode || 'na', + observed.externalItemId || 'na', + ].join(':'), + itemTitle: pickFirstNonEmpty([ + item?.skuName, + item?.externalSkuName, + item?.externalSkuCode, + item?.externalItemId, + ]), + quantity: observed.orderItemCount, + ...mapAdminObservedProductItem(observed, bindings), } }), } @@ -1253,6 +1370,34 @@ function matchesObservedProduct(binding, observed) { ) } +function mapAdminObservedProductItem(item, bindings = []) { + const matchedBinding = findMatchingObservedBinding(bindings, item) + + return { + provider: String(item?.provider || '').trim(), + platform: String(item?.platform || '').trim(), + shopId: String(item?.shopId || '').trim(), + shopName: String(item?.shopName || '').trim(), + externalItemId: String(item?.externalItemId || '').trim(), + externalSkuCode: String(item?.externalSkuCode || '').trim(), + externalSkuName: String(item?.externalSkuName || '').trim(), + latestSeenAt: item?.latestSeenAt || null, + orderItemCount: Number(item?.orderItemCount || 0), + configured: Boolean(matchedBinding), + matchedBinding: matchedBinding + ? { + skuCode: String(matchedBinding.skuCode || '').trim(), + skuName: String(matchedBinding.skuName || '').trim(), + profileKey: String(matchedBinding.profileKey || '').trim(), + } + : null, + } +} + +function findMatchingObservedBinding(bindings, observed) { + return (Array.isArray(bindings) ? bindings : []).find((binding) => matchesObservedProduct(binding, observed)) || null +} + async function mapAdminWebhookEvent(item, { includeRaw = false } = {}) { const headers = normalizeRecord(safeParseJson(item.headers_json)) const query = normalizeRecord(safeParseJson(item.query_json)) diff --git a/apps/frontend/src/services/admin.ts b/apps/frontend/src/services/admin.ts index 713d2251..be1b0ab6 100644 --- a/apps/frontend/src/services/admin.ts +++ b/apps/frontend/src/services/admin.ts @@ -3,6 +3,7 @@ import type { AdminAgisoObservedShopItem, AdminAgisoShopConfigItem, AdminAuditLogItem, + AdminFulfillmentLookupResult, AdminFulfillmentBindingConfigItem, AdminInventoryItemListItem, AdminMessageDeliveryListItem, @@ -85,6 +86,15 @@ export function fetchAdminFulfillmentBindingConfigs() { }>('/api/v1/admin/platform-config/fulfillment-bindings') } +export function lookupAdminFulfillmentBindingOrder(payload: { + provider?: string + platform?: string + shopId: string + platformOrderId: string +}) { + return apiPost('/api/v1/admin/platform-config/fulfillment-bindings/lookup-order', payload) +} + export function saveAdminFulfillmentBindingConfigs(payload: { bindings: Array> }) { return apiPost<{ filePath: string diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts index 8ae01624..1d0a720f 100644 --- a/apps/frontend/src/types/admin.ts +++ b/apps/frontend/src/types/admin.ts @@ -98,6 +98,37 @@ export interface AdminObservedProductItem { latestSeenAt: string | null orderItemCount: number configured: boolean + matchedBinding: null | { + skuCode: string + skuName: string + profileKey: string + } +} + +export interface AdminFulfillmentLookupOrder { + provider: string + platform: string + shopId: string + shopName: string + platformOrderId: string + buyerName: string + totalAmount: string + totalAmountFen: number + paidAt: string | null + enriched: boolean + enrichReason: string + errorMessage: string +} + +export interface AdminFulfillmentLookupItem extends AdminObservedProductItem { + lineId: string + itemTitle: string + quantity: number +} + +export interface AdminFulfillmentLookupResult { + order: AdminFulfillmentLookupOrder + items: AdminFulfillmentLookupItem[] } export interface AdminDashboardSummary { diff --git a/apps/frontend/src/views/admin/AdminFulfillmentBindingsView.vue b/apps/frontend/src/views/admin/AdminFulfillmentBindingsView.vue index 4dbf3121..9b15a983 100644 --- a/apps/frontend/src/views/admin/AdminFulfillmentBindingsView.vue +++ b/apps/frontend/src/views/admin/AdminFulfillmentBindingsView.vue @@ -1,12 +1,18 @@ @@ -443,6 +506,119 @@ onMounted(loadConfigs)
履约配置加载中