Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24d25aa77a |
@@ -1,4 +0,0 @@
|
|||||||
# 容器内 /bin/sh 直接执行的脚本必须是 LF。
|
|
||||||
# Windows 下 core.autocrlf=true 会把 checkout 出来的 .sh 转成 CRLF,
|
|
||||||
# 导致 `set: Illegal option -`,backend 容器反复重启。
|
|
||||||
*.sh text eol=lf
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
-- 168_semi_auto_binding_role_id.sql
|
|
||||||
--
|
|
||||||
-- 客户页角色绑定需要保存平台侧角色 ID:
|
|
||||||
-- 1. 虎牙 checkUserBindGameAccount 的 gameRole.roleId 即平台侧角色 ID;
|
|
||||||
-- 2. 客户工单资料里的游戏 ID(gameId / characterId)作为客户填写角色 ID;
|
|
||||||
-- 3. 两侧角色 ID 不一致时禁止确认绑定,只允许重新扫码或联系客服。
|
|
||||||
--
|
|
||||||
-- 该列只保存平台识别结果,不参与订单、资金等 A 级事实。
|
|
||||||
|
|
||||||
ALTER TABLE semi_auto_tasks
|
|
||||||
ADD COLUMN IF NOT EXISTS bind_game_role_id TEXT NOT NULL DEFAULT '';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN semi_auto_tasks.bind_game_role_id IS
|
|
||||||
'平台侧游戏角色 ID(虎牙 gameRole.roleId),用于与客户填写角色比对';
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
-- 169_semi_auto_customer_profile.sql
|
|
||||||
--
|
|
||||||
-- 半自动订单不会生成 work_orders,客户资料页 /c/<订单号> 是唯一入口:
|
|
||||||
-- 1. 客户在授权页填写本次需要绑定的角色名称 / 游戏编号(角色 ID)/ 区服;
|
|
||||||
-- 2. 平台识别到角色后,用这里填写的角色 ID 与 bind_game_role_id 比对;
|
|
||||||
-- 3. 两侧不一致时禁止确认绑定,只能重新扫码或联系客服。
|
|
||||||
|
|
||||||
ALTER TABLE semi_auto_tasks
|
|
||||||
ADD COLUMN IF NOT EXISTS customer_game_role_name TEXT NOT NULL DEFAULT '',
|
|
||||||
ADD COLUMN IF NOT EXISTS customer_game_role_id TEXT NOT NULL DEFAULT '',
|
|
||||||
ADD COLUMN IF NOT EXISTS customer_game_server_zone TEXT NOT NULL DEFAULT '';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN semi_auto_tasks.customer_game_role_name IS
|
|
||||||
'客户在授权页填写的游戏角色名称';
|
|
||||||
COMMENT ON COLUMN semi_auto_tasks.customer_game_role_id IS
|
|
||||||
'客户在授权页填写的游戏编号 / 角色 ID,用于与平台识别角色比对';
|
|
||||||
COMMENT ON COLUMN semi_auto_tasks.customer_game_server_zone IS
|
|
||||||
'客户在授权页填写的区服';
|
|
||||||
@@ -254,6 +254,64 @@ export async function restoreConsignmentOriginalRewardWithClient(
|
|||||||
return Boolean(result.rows[0])
|
return Boolean(result.rows[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「撤销寄售履约」释放被该工单占用的现货。
|
||||||
|
*
|
||||||
|
* 占用模型下 remaining_quantity 与卖家端「进行中」都以 allocation 为准:撤销以前只改
|
||||||
|
* consignment_inventories(status/bound_*),占用行停在 reserved、余量停在 0,
|
||||||
|
* 现货回库却永远匹配不到,卖家端还一直显示占用(工单 217368 / 库存 1584)。
|
||||||
|
*
|
||||||
|
* 回库分支复用 releaseAllocationRows(回补余量、consumed→in_stock、解绑/改绑);
|
||||||
|
* 报废分支只关闭占用行、不回补余量,避免作废货重新进池被二次出售。
|
||||||
|
*/
|
||||||
|
export async function releaseConsignmentAllocationsForRevokedFulfillmentWithClient(
|
||||||
|
client: PoolClient,
|
||||||
|
input: {
|
||||||
|
workOrderId: number
|
||||||
|
inventoryId: number
|
||||||
|
/** true = 运营选择「报废该现货」 */
|
||||||
|
archive: boolean
|
||||||
|
now: string
|
||||||
|
},
|
||||||
|
): Promise<number> {
|
||||||
|
const result = await client.query<ConsignmentAllocationRow>(
|
||||||
|
`SELECT * FROM consignment_inventory_allocations
|
||||||
|
WHERE work_order_id = $1 AND inventory_id = $2 AND status IN ('reserved', 'consumed')
|
||||||
|
FOR UPDATE`,
|
||||||
|
[input.workOrderId, input.inventoryId],
|
||||||
|
)
|
||||||
|
if (result.rows.length === 0) return 0
|
||||||
|
|
||||||
|
if (!input.archive) {
|
||||||
|
await releaseAllocationRows(client, result.rows, input.now)
|
||||||
|
return result.rows.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// 报废:占用必须关掉(否则卖家端一直显示进行中),但余量不能回补到作废货上。
|
||||||
|
await client.query(
|
||||||
|
`UPDATE consignment_inventory_allocations
|
||||||
|
SET status = 'released', updated_at = $1
|
||||||
|
WHERE work_order_id = $2 AND inventory_id = $3 AND status IN ('reserved', 'consumed')`,
|
||||||
|
[input.now, input.workOrderId, input.inventoryId],
|
||||||
|
)
|
||||||
|
// 共享库存还有别的工单在履约时不动余量,只释放本单。
|
||||||
|
const active = await client.query<{ count: number }>(
|
||||||
|
`SELECT COUNT(*)::integer AS count
|
||||||
|
FROM consignment_inventory_allocations
|
||||||
|
WHERE inventory_id = $1 AND status IN ('reserved', 'consumed')`,
|
||||||
|
[input.inventoryId],
|
||||||
|
)
|
||||||
|
if (Number(active.rows[0]?.count || 0) === 0) {
|
||||||
|
await client.query(
|
||||||
|
`UPDATE consignment_inventories
|
||||||
|
SET remaining_quantity = 0, allocation_status = '待安排', updated_at = $1
|
||||||
|
WHERE id = $2`,
|
||||||
|
[input.now, input.inventoryId],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result.rows.length
|
||||||
|
}
|
||||||
|
|
||||||
/** 释放整个工单占用的全部库存(整单取消 / 退回大厅 / 撤销履约)。 */
|
/** 释放整个工单占用的全部库存(整单取消 / 退回大厅 / 撤销履约)。 */
|
||||||
export async function releaseConsignmentAllocationsForWorkOrderWithClient(
|
export async function releaseConsignmentAllocationsForWorkOrderWithClient(
|
||||||
client: PoolClient,
|
client: PoolClient,
|
||||||
|
|||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
import type { PoolClient } from 'pg'
|
||||||
|
|
||||||
|
import { releaseConsignmentAllocationsForRevokedFulfillmentWithClient } from './consignment-allocation-repo.js'
|
||||||
|
|
||||||
|
type Statement = { sql: string; values: unknown[] }
|
||||||
|
|
||||||
|
function fakeClient(options: {
|
||||||
|
allocations: Array<Record<string, unknown>>
|
||||||
|
activeAfterRelease?: number
|
||||||
|
}) {
|
||||||
|
const statements: Statement[] = []
|
||||||
|
const client = {
|
||||||
|
query: async (sql: string, values?: unknown[]) => {
|
||||||
|
statements.push({ sql, values: values || [] })
|
||||||
|
if (sql.includes('FROM consignment_inventory_allocations') && sql.includes('FOR UPDATE')) {
|
||||||
|
return { rows: options.allocations }
|
||||||
|
}
|
||||||
|
if (sql.includes('COUNT(*)::integer AS count')) {
|
||||||
|
return { rows: [{ count: options.activeAfterRelease ?? 0 }] }
|
||||||
|
}
|
||||||
|
return { rows: [] }
|
||||||
|
},
|
||||||
|
} as unknown as PoolClient
|
||||||
|
return { client, statements }
|
||||||
|
}
|
||||||
|
|
||||||
|
const allocationRow = {
|
||||||
|
id: 38,
|
||||||
|
inventory_id: 1584,
|
||||||
|
work_order_id: 217368,
|
||||||
|
work_order_share_id: null,
|
||||||
|
quantity: 1,
|
||||||
|
unit_price_fen: 19500,
|
||||||
|
status: 'reserved',
|
||||||
|
created_at: '2026-11-15T06:39:16.805Z',
|
||||||
|
updated_at: '2026-11-15T06:39:16.805Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
test('撤销回库:按占用行回补余量并释放占用,避免现货永久占库', async () => {
|
||||||
|
const { client, statements } = fakeClient({ allocations: [allocationRow] })
|
||||||
|
|
||||||
|
const released = await releaseConsignmentAllocationsForRevokedFulfillmentWithClient(client, {
|
||||||
|
workOrderId: 217368,
|
||||||
|
inventoryId: 1584,
|
||||||
|
archive: false,
|
||||||
|
now: '2026-11-15T08:32:44.000Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(released, 1)
|
||||||
|
|
||||||
|
// 真实故障:撤销只改库存状态,余量停在 0,匹配一律被 remaining_quantity > 0 挡掉。
|
||||||
|
const restore = statements.find((item) =>
|
||||||
|
item.sql.includes('remaining_quantity = remaining_quantity + $1'),
|
||||||
|
)
|
||||||
|
assert.ok(restore, '回库必须回补 remaining_quantity,否则现货仍是 0 件可匹配')
|
||||||
|
assert.deepEqual(restore!.values, [1, '2026-11-15T08:32:44.000Z', 1584])
|
||||||
|
|
||||||
|
const releaseRow = statements.find((item) => item.sql.includes("SET status = 'released'"))
|
||||||
|
assert.ok(releaseRow, '占用行必须置 released,否则卖家端一直显示进行中')
|
||||||
|
assert.deepEqual(releaseRow!.values, ['2026-11-15T08:32:44.000Z', 38])
|
||||||
|
|
||||||
|
const clearBinding = statements.find((item) => item.sql.includes('bound_order_id = NULL'))
|
||||||
|
assert.ok(clearBinding, '没有其它在途占用时必须清空卖家端绑定')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('撤销回库:还有其它在途占用时只释放本单,绑定改指向剩下的工单', async () => {
|
||||||
|
const { client, statements } = fakeClient({ allocations: [allocationRow], activeAfterRelease: 1 })
|
||||||
|
|
||||||
|
await releaseConsignmentAllocationsForRevokedFulfillmentWithClient(client, {
|
||||||
|
workOrderId: 217368,
|
||||||
|
inventoryId: 1584,
|
||||||
|
archive: false,
|
||||||
|
now: '2026-11-15T08:32:44.000Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!statements.some((item) => item.sql.includes('bound_order_id = NULL')),
|
||||||
|
'还有其它在途占用时不能把卖家端绑定清空',
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
statements.some((item) => item.sql.includes('FROM work_orders wo')),
|
||||||
|
'还有其它在途占用时必须把绑定改指向剩下的最新工单',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('撤销报废:只关闭占用,不回补余量,避免作废货被二次出售', async () => {
|
||||||
|
const { client, statements } = fakeClient({ allocations: [allocationRow] })
|
||||||
|
|
||||||
|
const released = await releaseConsignmentAllocationsForRevokedFulfillmentWithClient(client, {
|
||||||
|
workOrderId: 217368,
|
||||||
|
inventoryId: 1584,
|
||||||
|
archive: true,
|
||||||
|
now: '2026-11-15T08:32:44.000Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(released, 1)
|
||||||
|
assert.ok(
|
||||||
|
!statements.some((item) => item.sql.includes('remaining_quantity = remaining_quantity + $1')),
|
||||||
|
'报废现货不能回补余量',
|
||||||
|
)
|
||||||
|
const zero = statements.find((item) => item.sql.includes('SET remaining_quantity = 0'))
|
||||||
|
assert.ok(zero, '报废后余量必须归零,避免作废货显示可匹配数量')
|
||||||
|
assert.ok(statements.some((item) => item.sql.includes("SET status = 'released'")))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('没有可释放的占用行时保持幂等,不写库存', async () => {
|
||||||
|
const { client, statements } = fakeClient({ allocations: [] })
|
||||||
|
|
||||||
|
const released = await releaseConsignmentAllocationsForRevokedFulfillmentWithClient(client, {
|
||||||
|
workOrderId: 217368,
|
||||||
|
inventoryId: 1584,
|
||||||
|
archive: false,
|
||||||
|
now: '2026-11-15T08:32:44.000Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(released, 0)
|
||||||
|
assert.equal(statements.length, 1)
|
||||||
|
})
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
confirmPublicBinding,
|
|
||||||
getPublicWeiboCountryCodes,
|
getPublicWeiboCountryCodes,
|
||||||
listPublicSemiAutoTasks,
|
listPublicSemiAutoTasks,
|
||||||
loginPublicHuyaSms,
|
loginPublicHuyaSms,
|
||||||
loginPublicWeiboSms,
|
loginPublicWeiboSms,
|
||||||
markCustomerLinkOpened,
|
markCustomerLinkOpened,
|
||||||
pollPublicBinding,
|
|
||||||
pollPublicLogin,
|
pollPublicLogin,
|
||||||
pollPublicWeiboQrLogin,
|
pollPublicWeiboQrLogin,
|
||||||
savePublicTaskProfile,
|
|
||||||
sendPublicHuyaSmsCode,
|
sendPublicHuyaSmsCode,
|
||||||
sendPublicWeiboSmsCode,
|
sendPublicWeiboSmsCode,
|
||||||
startPublicBinding,
|
|
||||||
startPublicLogin,
|
startPublicLogin,
|
||||||
startPublicWeiboQrLogin,
|
startPublicWeiboQrLogin,
|
||||||
verifyPublicHuyaSmsChallenge,
|
verifyPublicHuyaSmsChallenge,
|
||||||
@@ -74,48 +70,6 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/:orderNo/tasks/:taskId/profile',
|
|
||||||
writeLimit,
|
|
||||||
createRouteHandler(
|
|
||||||
(req) => savePublicTaskProfile(req.params.orderNo, req.params.taskId, req.body || {}),
|
|
||||||
{
|
|
||||||
successMessage: '角色资料已保存',
|
|
||||||
errorMessage: '保存角色资料失败',
|
|
||||||
scope: '[customer-fulfillment/:orderNo/tasks/:taskId/profile]',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/:orderNo/tasks/:taskId/binding/start',
|
|
||||||
writeLimit,
|
|
||||||
createRouteHandler((req) => startPublicBinding(req.params.orderNo, req.params.taskId, req.body), {
|
|
||||||
successMessage: '绑定会话已创建',
|
|
||||||
errorMessage: '开始账号绑定失败',
|
|
||||||
scope: '[customer-fulfillment/:orderNo/tasks/:taskId/binding/start]',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/:orderNo/tasks/:taskId/binding/status',
|
|
||||||
readLimit,
|
|
||||||
createRouteHandler((req) => pollPublicBinding(req.params.orderNo, req.params.taskId), {
|
|
||||||
errorMessage: '刷新账号绑定状态失败',
|
|
||||||
scope: '[customer-fulfillment/:orderNo/tasks/:taskId/binding/status]',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/:orderNo/tasks/:taskId/binding/confirm',
|
|
||||||
writeLimit,
|
|
||||||
createRouteHandler((req) => confirmPublicBinding(req.params.orderNo, req.params.taskId), {
|
|
||||||
successMessage: '绑定已确认',
|
|
||||||
errorMessage: '确认账号绑定失败',
|
|
||||||
scope: '[customer-fulfillment/:orderNo/tasks/:taskId/binding/confirm]',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:orderNo/tasks/:taskId/weibo/config',
|
'/:orderNo/tasks/:taskId/weibo/config',
|
||||||
readLimit,
|
readLimit,
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ type MockSession = {
|
|||||||
bindingQrExpiresAtMs: number
|
bindingQrExpiresAtMs: number
|
||||||
bindGameAccount: string
|
bindGameAccount: string
|
||||||
bindGameRole: string
|
bindGameRole: string
|
||||||
bindGameRoleId: string
|
|
||||||
bindGameChannel: string
|
bindGameChannel: string
|
||||||
message: string
|
message: string
|
||||||
tickets: Map<string, MockTicket>
|
tickets: Map<string, MockTicket>
|
||||||
@@ -137,10 +136,8 @@ function bindingView(session: MockSession): ConnectorBindingSession {
|
|||||||
status: session.bindingStatus,
|
status: session.bindingStatus,
|
||||||
qrImage: session.bindingStatus === 'not_started' ? '' : buildMockBindingQr(session.sessionId),
|
qrImage: session.bindingStatus === 'not_started' ? '' : buildMockBindingQr(session.sessionId),
|
||||||
qrExpiresAt: new Date(session.bindingQrExpiresAtMs).toISOString(),
|
qrExpiresAt: new Date(session.bindingQrExpiresAtMs).toISOString(),
|
||||||
qrUrl: '',
|
|
||||||
bindGameAccount: session.bindGameAccount,
|
bindGameAccount: session.bindGameAccount,
|
||||||
bindGameRole: session.bindGameRole,
|
bindGameRole: session.bindGameRole,
|
||||||
bindGameRoleId: session.bindGameRoleId,
|
|
||||||
bindGameChannel: session.bindGameChannel,
|
bindGameChannel: session.bindGameChannel,
|
||||||
message: session.message,
|
message: session.message,
|
||||||
}
|
}
|
||||||
@@ -161,7 +158,6 @@ function markRoleReady(session: MockSession) {
|
|||||||
session.bindingStatus = 'role_ready'
|
session.bindingStatus = 'role_ready'
|
||||||
session.bindGameAccount = `mock-account-${session.sessionId.slice(-6)}`
|
session.bindGameAccount = `mock-account-${session.sessionId.slice(-6)}`
|
||||||
session.bindGameRole = 'Mock测试角色'
|
session.bindGameRole = 'Mock测试角色'
|
||||||
session.bindGameRoleId = `mock-role-${session.sessionId.slice(-6)}`
|
|
||||||
session.bindGameChannel = '虎牙'
|
session.bindGameChannel = '虎牙'
|
||||||
session.message = 'Mock 已识别游戏角色,请确认绑定'
|
session.message = 'Mock 已识别游戏角色,请确认绑定'
|
||||||
}
|
}
|
||||||
@@ -192,7 +188,6 @@ export function createMockLoginSession(input: {
|
|||||||
bindingQrExpiresAtMs: 0,
|
bindingQrExpiresAtMs: 0,
|
||||||
bindGameAccount: '',
|
bindGameAccount: '',
|
||||||
bindGameRole: '',
|
bindGameRole: '',
|
||||||
bindGameRoleId: '',
|
|
||||||
bindGameChannel: '',
|
bindGameChannel: '',
|
||||||
message: 'Mock 登录二维码已生成,轮询后会自动模拟扫码成功',
|
message: 'Mock 登录二维码已生成,轮询后会自动模拟扫码成功',
|
||||||
tickets: new Map(),
|
tickets: new Map(),
|
||||||
@@ -235,7 +230,6 @@ export function sendMockHuyaSmsCode(input: {
|
|||||||
bindingQrExpiresAtMs: 0,
|
bindingQrExpiresAtMs: 0,
|
||||||
bindGameAccount: '',
|
bindGameAccount: '',
|
||||||
bindGameRole: '',
|
bindGameRole: '',
|
||||||
bindGameRoleId: '',
|
|
||||||
bindGameChannel: '',
|
bindGameChannel: '',
|
||||||
message: 'Mock 短信验证码已发送,请输入 123456',
|
message: 'Mock 短信验证码已发送,请输入 123456',
|
||||||
tickets: new Map(),
|
tickets: new Map(),
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
* 虎牙 checkUserBindGameAccount 返回的 gameRole.roleId 是 40 位加密串,
|
|
||||||
* 无法直接和客户填写的游戏编号比对;平台侧可用于核对的是登录账号的数值 UID(huyaUid)。
|
|
||||||
*
|
|
||||||
* 取数优先级:角色 ID 本身是纯数字 > 数值 UID > 原始角色 ID。
|
|
||||||
* 这样既兼容未来虎牙直接下发数值角色 ID,也避免把加密串展示给客户。
|
|
||||||
*/
|
|
||||||
export function resolveBindingRoleId(roleId: unknown, huyaUid: unknown): string {
|
|
||||||
const rawRoleId = String(roleId ?? '').trim()
|
|
||||||
|
|
||||||
if (/^\d+$/.test(rawRoleId)) return rawRoleId
|
|
||||||
|
|
||||||
const uid = String(huyaUid ?? '').trim()
|
|
||||||
|
|
||||||
return /^\d+$/.test(uid) ? uid : rawRoleId
|
|
||||||
}
|
|
||||||
@@ -95,10 +95,8 @@ export type ConnectorBindingSession = {
|
|||||||
| 'failed'
|
| 'failed'
|
||||||
qrImage: string
|
qrImage: string
|
||||||
qrExpiresAt: string
|
qrExpiresAt: string
|
||||||
qrUrl: string
|
|
||||||
bindGameAccount: string
|
bindGameAccount: string
|
||||||
bindGameRole: string
|
bindGameRole: string
|
||||||
bindGameRoleId: string
|
|
||||||
bindGameChannel: string
|
bindGameChannel: string
|
||||||
hasExistingBinding?: boolean
|
hasExistingBinding?: boolean
|
||||||
rebindRequested?: boolean
|
rebindRequested?: boolean
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import path from 'node:path'
|
|||||||
import { runtimeConfig } from '../../../config/runtime.js'
|
import { runtimeConfig } from '../../../config/runtime.js'
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
import { logWarn } from '../../../utils/logger.js'
|
import { logWarn } from '../../../utils/logger.js'
|
||||||
import { resolveBindingRoleId } from '../binding-role-id.js'
|
|
||||||
import type {
|
import type {
|
||||||
ConnectorBindingSession,
|
ConnectorBindingSession,
|
||||||
ConnectorBrowserTicket,
|
ConnectorBrowserTicket,
|
||||||
@@ -63,12 +62,10 @@ type NativeBindingStatus = ConnectorBindingSession['status']
|
|||||||
type NativeBindState = {
|
type NativeBindState = {
|
||||||
status: NativeBindingStatus
|
status: NativeBindingStatus
|
||||||
qrImage: string
|
qrImage: string
|
||||||
qrUrl: string
|
|
||||||
qrToken: string
|
qrToken: string
|
||||||
expiresAtMs: number
|
expiresAtMs: number
|
||||||
account: string
|
account: string
|
||||||
role: string
|
role: string
|
||||||
roleId: string
|
|
||||||
channel: string
|
channel: string
|
||||||
hasExistingBinding: boolean
|
hasExistingBinding: boolean
|
||||||
rebindRequested: boolean
|
rebindRequested: boolean
|
||||||
@@ -138,12 +135,10 @@ function defaultBindingState(): NativeBindState {
|
|||||||
return {
|
return {
|
||||||
status: 'not_started',
|
status: 'not_started',
|
||||||
qrImage: '',
|
qrImage: '',
|
||||||
qrUrl: '',
|
|
||||||
qrToken: '',
|
qrToken: '',
|
||||||
expiresAtMs: 0,
|
expiresAtMs: 0,
|
||||||
account: '',
|
account: '',
|
||||||
role: '',
|
role: '',
|
||||||
roleId: '',
|
|
||||||
channel: '',
|
channel: '',
|
||||||
hasExistingBinding: false,
|
hasExistingBinding: false,
|
||||||
rebindRequested: false,
|
rebindRequested: false,
|
||||||
@@ -228,29 +223,18 @@ function smsBehavior(page = 'https://www.huya.com/g'): { behavior: string; page:
|
|||||||
function parseRole(response: HuyaBindResponse | null): {
|
function parseRole(response: HuyaBindResponse | null): {
|
||||||
account: string
|
account: string
|
||||||
role: string
|
role: string
|
||||||
roleId: string
|
|
||||||
channel: string
|
channel: string
|
||||||
} {
|
} {
|
||||||
if (!response || response.status !== 200) {
|
if (!response || response.status !== 200) return { account: '', role: '', channel: '' }
|
||||||
return { account: '', role: '', roleId: '', channel: '' }
|
|
||||||
}
|
|
||||||
const data: HuyaGameAccountData = response.accountData
|
const data: HuyaGameAccountData = response.accountData
|
||||||
const account = data.gameAccount.nick || data.gameRole.roleName
|
const account = data.gameAccount.nick || data.gameRole.roleName
|
||||||
const role = data.gameRole.roleName
|
const role = data.gameRole.roleName
|
||||||
const channel = [data.gameRole.platName, data.gameRole.areaName].filter(Boolean).join(' · ')
|
const channel = [data.gameRole.platName, data.gameRole.areaName].filter(Boolean).join(' · ')
|
||||||
// 虎牙 checkUserBindGameAccount 的 gameRole.roleId 就是平台侧角色 ID;
|
return { account, role, channel }
|
||||||
// 该字段一直存在于 RPC 响应里,之前被丢弃,导致客户页无法做两侧角色比对。
|
|
||||||
const roleId = String(data.gameRole.roleId || '').trim()
|
|
||||||
return { account, role, roleId, channel }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindingRoleKey(role: {
|
function bindingRoleKey(role: { account: string; role: string; channel: string }): string {
|
||||||
account: string
|
return [role.account, role.role, role.channel].join('\u0000')
|
||||||
role: string
|
|
||||||
roleId?: string
|
|
||||||
channel: string
|
|
||||||
}): string {
|
|
||||||
return [role.account, role.role, role.roleId || '', role.channel].join('\u0000')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function configValues() {
|
function configValues() {
|
||||||
@@ -736,7 +720,6 @@ export class HuyaNativeConnector {
|
|||||||
}
|
}
|
||||||
session.binding.status = 'qr_pending'
|
session.binding.status = 'qr_pending'
|
||||||
session.binding.qrImage = mini.image
|
session.binding.qrImage = mini.image
|
||||||
session.binding.qrUrl = urls.qrUrl
|
|
||||||
session.binding.qrToken = mini.token
|
session.binding.qrToken = mini.token
|
||||||
session.binding.expiresAtMs = this.now() + BINDING_QR_TTL_MS
|
session.binding.expiresAtMs = this.now() + BINDING_QR_TTL_MS
|
||||||
session.binding.message = options.forceRebind
|
session.binding.message = options.forceRebind
|
||||||
@@ -1059,13 +1042,12 @@ export class HuyaNativeConnector {
|
|||||||
|
|
||||||
private markRoleReady(
|
private markRoleReady(
|
||||||
session: NativeSession,
|
session: NativeSession,
|
||||||
role: { account: string; role: string; roleId?: string; channel: string },
|
role: { account: string; role: string; channel: string },
|
||||||
existingBinding = false,
|
existingBinding = false,
|
||||||
): void {
|
): void {
|
||||||
session.binding.status = 'role_ready'
|
session.binding.status = 'role_ready'
|
||||||
session.binding.account = role.account
|
session.binding.account = role.account
|
||||||
session.binding.role = role.role
|
session.binding.role = role.role
|
||||||
session.binding.roleId = role.roleId || ''
|
|
||||||
session.binding.channel = role.channel
|
session.binding.channel = role.channel
|
||||||
session.binding.message = existingBinding
|
session.binding.message = existingBinding
|
||||||
? '当前账号已有绑定角色,请确认直接使用'
|
? '当前账号已有绑定角色,请确认直接使用'
|
||||||
@@ -1149,10 +1131,8 @@ export class HuyaNativeConnector {
|
|||||||
status: binding.status,
|
status: binding.status,
|
||||||
qrImage: binding.qrImage,
|
qrImage: binding.qrImage,
|
||||||
qrExpiresAt: binding.expiresAtMs ? iso(binding.expiresAtMs) : '',
|
qrExpiresAt: binding.expiresAtMs ? iso(binding.expiresAtMs) : '',
|
||||||
qrUrl: binding.qrUrl,
|
|
||||||
bindGameAccount: binding.account,
|
bindGameAccount: binding.account,
|
||||||
bindGameRole: binding.role,
|
bindGameRole: binding.role,
|
||||||
bindGameRoleId: resolveBindingRoleId(binding.roleId, session.uid),
|
|
||||||
bindGameChannel: binding.channel,
|
bindGameChannel: binding.channel,
|
||||||
hasExistingBinding: binding.hasExistingBinding,
|
hasExistingBinding: binding.hasExistingBinding,
|
||||||
rebindRequested: binding.rebindRequested,
|
rebindRequested: binding.rebindRequested,
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
import assert from 'node:assert/strict'
|
|
||||||
import { describe, it } from 'node:test'
|
|
||||||
|
|
||||||
import { resolveBindingRoleId } from './binding-role-id.js'
|
|
||||||
import { compareBindingRole, normalizeCustomerProfile } from './semi-auto-task-service.js'
|
|
||||||
|
|
||||||
const task = (id: string, name: string, huyaUid = '') => ({
|
|
||||||
customer_game_id: id,
|
|
||||||
customer_game_role: name,
|
|
||||||
huya_uid: huyaUid,
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('待确认角色 UID 取数(huyaUid)', () => {
|
|
||||||
it('角色 ID 本身是纯数字时直接使用', () => {
|
|
||||||
assert.equal(resolveBindingRoleId('1192083858', 1199608910400), '1192083858')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('角色 ID 是加密串时改用数值 UID', () => {
|
|
||||||
assert.equal(
|
|
||||||
resolveBindingRoleId('834ee3e75a1bfc9f65fb6b966bac7ef318e68ea', '1199608910400'),
|
|
||||||
'1199608910400',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('缺少角色 ID 时回退到数值 UID', () => {
|
|
||||||
assert.equal(resolveBindingRoleId('', 1199608910400), '1199608910400')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('没有可用数值 UID 时保留原始角色 ID', () => {
|
|
||||||
assert.equal(
|
|
||||||
resolveBindingRoleId('834ee3e75a1bfc9f65fb6b966bac7ef318e68ea', ''),
|
|
||||||
'834ee3e75a1bfc9f65fb6b966bac7ef318e68ea',
|
|
||||||
)
|
|
||||||
assert.equal(resolveBindingRoleId(null, undefined), '')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('虎牙加密串角色 ID 改用 huyaUid 参与比对', () => {
|
|
||||||
const result = compareBindingRole(task('1199608910400', '大鲨鱼伊塞53', '1199608910400'), {
|
|
||||||
bindGameRole: '大鲨鱼伊塞53',
|
|
||||||
bindGameRoleId: '834ee3e75a1bfc9f65fb6b966bac7ef318e68ea',
|
|
||||||
})
|
|
||||||
assert.equal(result.verdict, 'match')
|
|
||||||
assert.equal(result.boundRoleId, '1199608910400')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('没有 huyaUid 时保留原始加密串,仍判不一致', () => {
|
|
||||||
const result = compareBindingRole(task('1199608910400', '大鲨鱼伊塞53'), {
|
|
||||||
bindGameRole: '大鲨鱼伊塞53',
|
|
||||||
bindGameRoleId: '834ee3e75a1bfc9f65fb6b966bac7ef318e68ea',
|
|
||||||
})
|
|
||||||
assert.equal(result.verdict, 'mismatch')
|
|
||||||
assert.equal(result.boundRoleId, '834ee3e75a1bfc9f65fb6b966bac7ef318e68ea')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('客户填写角色与平台识别角色比对', () => {
|
|
||||||
it('两侧角色 ID 一致时允许确认', () => {
|
|
||||||
const result = compareBindingRole(task('1192083858', '巫师基尔斯84'), {
|
|
||||||
bindGameRole: '巫师基尔斯84',
|
|
||||||
bindGameRoleId: '1192083858',
|
|
||||||
})
|
|
||||||
assert.equal(result.verdict, 'match')
|
|
||||||
assert.equal(result.customerRoleId, '1192083858')
|
|
||||||
assert.equal(result.boundRoleId, '1192083858')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('两侧角色 ID 不一致时禁止确认', () => {
|
|
||||||
const result = compareBindingRole(task('1192083858', '巫师基尔斯84'), {
|
|
||||||
bindGameRole: '我是火星人2222',
|
|
||||||
bindGameRoleId: '166909256',
|
|
||||||
})
|
|
||||||
assert.equal(result.verdict, 'mismatch')
|
|
||||||
assert.match(result.message, /不一致/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('缺少角色 ID 时退回角色名称比对', () => {
|
|
||||||
const matched = compareBindingRole(task('', '巫师基尔斯84'), {
|
|
||||||
bindGameRole: '巫师基尔斯84',
|
|
||||||
bindGameRoleId: '',
|
|
||||||
})
|
|
||||||
assert.equal(matched.verdict, 'match')
|
|
||||||
|
|
||||||
const mismatched = compareBindingRole(task('', '巫师基尔斯84'), {
|
|
||||||
bindGameRole: '我是火星人2222',
|
|
||||||
bindGameRoleId: '',
|
|
||||||
})
|
|
||||||
assert.equal(mismatched.verdict, 'mismatch')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('两侧都没有可核对信息时标记为 unknown', () => {
|
|
||||||
const result = compareBindingRole(task('', ''), { bindGameRole: '', bindGameRoleId: '' })
|
|
||||||
assert.equal(result.verdict, 'unknown')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('客户角色资料校验', () => {
|
|
||||||
it('清理空白并保留纯数字角色 ID', () => {
|
|
||||||
const profile = normalizeCustomerProfile({
|
|
||||||
roleName: ' 巫师基尔斯84 ',
|
|
||||||
roleId: ' 1192083858 ',
|
|
||||||
serverZone: ' 苹果V区 ',
|
|
||||||
})
|
|
||||||
assert.deepEqual(profile, {
|
|
||||||
roleName: '巫师基尔斯84',
|
|
||||||
roleId: '1192083858',
|
|
||||||
serverZone: '苹果V区',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('角色 ID 非数字时拒绝保存', () => {
|
|
||||||
assert.throws(
|
|
||||||
() => normalizeCustomerProfile({ roleName: '巫师基尔斯84', roleId: 'abc123' }),
|
|
||||||
/只能是数字/,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('缺区服时拒绝保存', () => {
|
|
||||||
assert.throws(
|
|
||||||
() => normalizeCustomerProfile({ roleName: '巫师基尔斯84', roleId: '1192083858' }),
|
|
||||||
/区服只能选择/,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('区服不在固定选项时拒绝保存', () => {
|
|
||||||
assert.throws(
|
|
||||||
() =>
|
|
||||||
normalizeCustomerProfile({
|
|
||||||
roleName: '巫师基尔斯84',
|
|
||||||
roleId: '1192083858',
|
|
||||||
serverZone: '苹果微信区',
|
|
||||||
}),
|
|
||||||
/区服只能选择/,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('角色名称与角色 ID 都为空时拒绝保存', () => {
|
|
||||||
assert.throws(
|
|
||||||
() => normalizeCustomerProfile({ roleName: ' ', roleId: '', serverZone: '苹果V区' }),
|
|
||||||
/请完整填写区服/,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -98,14 +98,7 @@ type TaskRow = {
|
|||||||
platform_nickname: string
|
platform_nickname: string
|
||||||
bind_game_account: string
|
bind_game_account: string
|
||||||
bind_game_role: string
|
bind_game_role: string
|
||||||
bind_game_role_id: string
|
|
||||||
bind_game_channel: string
|
bind_game_channel: string
|
||||||
customer_game_role_name: string
|
|
||||||
customer_game_role_id: string
|
|
||||||
customer_game_server_zone: string
|
|
||||||
customer_game_id: string
|
|
||||||
customer_game_role: string
|
|
||||||
customer_server_zone: string
|
|
||||||
target_quantity: number
|
target_quantity: number
|
||||||
completed_quantity: number
|
completed_quantity: number
|
||||||
assigned_admin_user_id: number | null
|
assigned_admin_user_id: number | null
|
||||||
@@ -182,20 +175,7 @@ function displayStatus(task: TaskRow) {
|
|||||||
return { key: 'failed', label: '需客服处理' }
|
return { key: 'failed', label: '需客服处理' }
|
||||||
}
|
}
|
||||||
if (task.login_status === 'authenticated') {
|
if (task.login_status === 'authenticated') {
|
||||||
// 虎牙需要客户确认绑定角色后才算授权完成;微博登录即授权。
|
return { key: 'waiting_operator', label: '等待客服处理' }
|
||||||
if (task.platform === 'weibo' || task.binding_status === 'confirmed') {
|
|
||||||
return { key: 'waiting_operator', label: '等待客服处理' }
|
|
||||||
}
|
|
||||||
if (task.binding_status === 'role_ready') {
|
|
||||||
return { key: 'binding_confirm', label: '待确认绑定角色' }
|
|
||||||
}
|
|
||||||
if (task.binding_status === 'expired') {
|
|
||||||
return { key: 'binding_expired', label: '绑定二维码已过期' }
|
|
||||||
}
|
|
||||||
if (task.binding_status === 'failed') {
|
|
||||||
return { key: 'binding_failed', label: '绑定失败,需重新扫码' }
|
|
||||||
}
|
|
||||||
return { key: 'waiting_binding', label: '等待客户绑定角色' }
|
|
||||||
}
|
}
|
||||||
if (task.login_status === 'scanned') return { key: 'login_scanned', label: '已扫码,确认中' }
|
if (task.login_status === 'scanned') return { key: 'login_scanned', label: '已扫码,确认中' }
|
||||||
if (task.login_status === 'qr_pending') return { key: 'waiting_login', label: '等待客户登录' }
|
if (task.login_status === 'qr_pending') return { key: 'waiting_login', label: '等待客户登录' }
|
||||||
@@ -225,11 +205,7 @@ function mapTask(task: TaskRow) {
|
|||||||
platformNickname: task.platform_nickname,
|
platformNickname: task.platform_nickname,
|
||||||
bindGameAccount: task.bind_game_account,
|
bindGameAccount: task.bind_game_account,
|
||||||
bindGameRole: task.bind_game_role,
|
bindGameRole: task.bind_game_role,
|
||||||
bindGameRoleId: task.bind_game_role_id,
|
|
||||||
bindGameChannel: task.bind_game_channel,
|
bindGameChannel: task.bind_game_channel,
|
||||||
customerGameRoleId: task.customer_game_id || '',
|
|
||||||
customerGameRoleName: task.customer_game_role || '',
|
|
||||||
customerServerZone: task.customer_server_zone || '',
|
|
||||||
targetQuantity: Number(task.target_quantity || 1),
|
targetQuantity: Number(task.target_quantity || 1),
|
||||||
completedQuantity: Number(task.completed_quantity || 0),
|
completedQuantity: Number(task.completed_quantity || 0),
|
||||||
assignedAdminUserId: task.assigned_admin_user_id ? Number(task.assigned_admin_user_id) : null,
|
assignedAdminUserId: task.assigned_admin_user_id ? Number(task.assigned_admin_user_id) : null,
|
||||||
@@ -313,41 +289,13 @@ const TASK_SELECT = `
|
|||||||
cfg.semi_auto_config_json,
|
cfg.semi_auto_config_json,
|
||||||
credential.credential_status,
|
credential.credential_status,
|
||||||
credential.credential_source,
|
credential.credential_source,
|
||||||
credential.credential_updated_at,
|
credential.credential_updated_at
|
||||||
COALESCE(
|
|
||||||
NULLIF(task.customer_game_role_id, ''),
|
|
||||||
wo.material_json->'collect'->'fields'->>'gameId',
|
|
||||||
wo.material_json->'collect'->'fields'->>'characterId',
|
|
||||||
''
|
|
||||||
) AS customer_game_id,
|
|
||||||
COALESCE(
|
|
||||||
NULLIF(task.customer_game_role_name, ''),
|
|
||||||
NULLIF(wo.material_json->'collect'->'fields'->>'gameNickname', ''),
|
|
||||||
NULLIF(wo.material_json->'collect'->'fields'->>'roleName', ''),
|
|
||||||
''
|
|
||||||
) AS customer_game_role,
|
|
||||||
COALESCE(
|
|
||||||
NULLIF(task.customer_game_server_zone, ''),
|
|
||||||
wo.material_json->'collect'->'fields'->>'serverZone',
|
|
||||||
''
|
|
||||||
) AS customer_server_zone
|
|
||||||
FROM semi_auto_tasks task
|
FROM semi_auto_tasks task
|
||||||
JOIN orders o ON o.id = task.order_id
|
JOIN orders o ON o.id = task.order_id
|
||||||
JOIN order_items oi ON oi.id = task.order_item_id
|
JOIN order_items oi ON oi.id = task.order_item_id
|
||||||
LEFT JOIN manual_fulfillment_configs cfg ON cfg.id = task.manual_fulfillment_config_id
|
LEFT JOIN manual_fulfillment_configs cfg ON cfg.id = task.manual_fulfillment_config_id
|
||||||
LEFT JOIN admin_users admin ON admin.id = task.assigned_admin_user_id
|
LEFT JOIN admin_users admin ON admin.id = task.assigned_admin_user_id
|
||||||
LEFT JOIN semi_auto_credentials credential ON credential.id = task.credential_id
|
LEFT JOIN semi_auto_credentials credential ON credential.id = task.credential_id
|
||||||
LEFT JOIN LATERAL (
|
|
||||||
SELECT candidate.material_json
|
|
||||||
FROM work_orders candidate
|
|
||||||
WHERE candidate.order_item_id = task.order_item_id
|
|
||||||
OR (candidate.order_item_id IS NULL AND candidate.order_id = task.order_id)
|
|
||||||
OR (o.platform_order_id <> '' AND candidate.platform_order_id = o.platform_order_id)
|
|
||||||
ORDER BY (candidate.order_item_id = task.order_item_id) DESC NULLS LAST,
|
|
||||||
(candidate.order_id = task.order_id) DESC NULLS LAST,
|
|
||||||
candidate.id DESC
|
|
||||||
LIMIT 1
|
|
||||||
) wo ON TRUE
|
|
||||||
`
|
`
|
||||||
|
|
||||||
async function loadTask(taskId: number): Promise<TaskRow> {
|
async function loadTask(taskId: number): Promise<TaskRow> {
|
||||||
@@ -406,24 +354,6 @@ function assertHuyaTask(task: TaskRow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 半自动订单没有工单,资料页 /c/<订单号> 是唯一入口。
|
|
||||||
* 未提交区服 / 游戏编号 / 角色名时,不允许开始登录,避免后续无法比对角色。
|
|
||||||
*/
|
|
||||||
function assertCustomerProfileReady(task: TaskRow) {
|
|
||||||
if (task.platform !== 'huya') return
|
|
||||||
if (
|
|
||||||
!String(task.customer_game_server_zone || '').trim() ||
|
|
||||||
!String(task.customer_game_role_id || '').trim() ||
|
|
||||||
!String(task.customer_game_role_name || '').trim()
|
|
||||||
) {
|
|
||||||
throw createHttpError('请先提交游戏角色资料', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'semi_auto_profile_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createLoginSessionForTask(
|
async function createLoginSessionForTask(
|
||||||
task: TaskRow,
|
task: TaskRow,
|
||||||
orderNo: string,
|
orderNo: string,
|
||||||
@@ -749,98 +679,6 @@ export async function markCustomerLinkOpened(orderNoInput: unknown, taskIdInput:
|
|||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomerProfileInput = {
|
|
||||||
roleName?: unknown
|
|
||||||
roleId?: unknown
|
|
||||||
serverZone?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
const CUSTOMER_SERVER_ZONES = new Set(['苹果Q区', '苹果V区', '安卓Q区', '安卓V区'])
|
|
||||||
|
|
||||||
export function normalizeCustomerProfile(input: CustomerProfileInput = {}) {
|
|
||||||
const roleName = String(input.roleName ?? '')
|
|
||||||
.trim()
|
|
||||||
.slice(0, 100)
|
|
||||||
const roleId = String(input.roleId ?? '')
|
|
||||||
.replace(/\s+/g, '')
|
|
||||||
.slice(0, 64)
|
|
||||||
const serverZone = String(input.serverZone ?? '')
|
|
||||||
.trim()
|
|
||||||
.slice(0, 100)
|
|
||||||
if (roleId && !/^\d+$/.test(roleId)) {
|
|
||||||
throw createHttpError('游戏编号 / 角色 ID 只能是数字', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'semi_auto_profile_role_id_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!CUSTOMER_SERVER_ZONES.has(serverZone)) {
|
|
||||||
throw createHttpError('区服只能选择 苹果Q区 / 苹果V区 / 安卓Q区 / 安卓V区', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'semi_auto_profile_server_zone_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!roleName || !roleId) {
|
|
||||||
throw createHttpError('请完整填写区服、游戏编号和角色名', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'semi_auto_profile_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return { roleName, roleId, serverZone }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 半自动订单没有 work_orders,客户资料只能在授权页 /c/<订单号> 收集。
|
|
||||||
* 保存后立即参与“客户填写角色 vs 平台识别角色”的比对。
|
|
||||||
*/
|
|
||||||
export async function savePublicTaskProfile(
|
|
||||||
orderNoInput: unknown,
|
|
||||||
taskIdInput: unknown,
|
|
||||||
payload: CustomerProfileInput = {},
|
|
||||||
) {
|
|
||||||
const orderNo = normalizeOrderNo(orderNoInput)
|
|
||||||
const taskId = normalizeId(taskIdInput)
|
|
||||||
const task = await loadTaskForOrder(orderNo, taskId)
|
|
||||||
if (task.platform !== 'huya') {
|
|
||||||
throw createHttpError('当前平台不需要填写游戏角色', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'semi_auto_profile_not_supported',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (task.execution_status === 'completed' || task.execution_status === 'cancelled') {
|
|
||||||
throw createHttpError('当前订单已结束,不能修改角色资料', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'semi_auto_task_closed',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (task.binding_status === 'confirmed') {
|
|
||||||
throw createHttpError('绑定已确认,不能修改角色资料', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'semi_auto_profile_locked',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const profile = normalizeCustomerProfile(payload)
|
|
||||||
await withTransaction(async (client) => {
|
|
||||||
await client.query(
|
|
||||||
`
|
|
||||||
UPDATE semi_auto_tasks
|
|
||||||
SET customer_game_role_name = $2,
|
|
||||||
customer_game_role_id = $3,
|
|
||||||
customer_game_server_zone = $4,
|
|
||||||
updated_at = NOW()
|
|
||||||
WHERE id = $1
|
|
||||||
`,
|
|
||||||
[task.id, profile.roleName, profile.roleId, profile.serverZone],
|
|
||||||
)
|
|
||||||
await appendTaskEvent(client, task.id, {
|
|
||||||
eventType: 'customer_profile_saved',
|
|
||||||
actorType: 'customer',
|
|
||||||
detail: profile,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const updated = await loadTask(task.id)
|
|
||||||
return { task: mapTask(updated) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapLoginUpdate(session: ConnectorLoginSession) {
|
function mapLoginUpdate(session: ConnectorLoginSession) {
|
||||||
const statusMap: Record<ConnectorLoginSession['status'], LoginStatus> = {
|
const statusMap: Record<ConnectorLoginSession['status'], LoginStatus> = {
|
||||||
verification_pending: 'waiting_login',
|
verification_pending: 'waiting_login',
|
||||||
@@ -867,13 +705,8 @@ async function updateLoginTask(
|
|||||||
options: { credentialSource?: 'qr' | 'sms'; method?: 'qr' | 'sms' } = {},
|
options: { credentialSource?: 'qr' | 'sms'; method?: 'qr' | 'sms' } = {},
|
||||||
) {
|
) {
|
||||||
const mapped = mapLoginUpdate(session)
|
const mapped = mapLoginUpdate(session)
|
||||||
// 虎牙登录成功还不算授权完成,必须等客户确认绑定角色;微博登录即授权。
|
|
||||||
const nextExecutionStatus: ExecutionStatus =
|
const nextExecutionStatus: ExecutionStatus =
|
||||||
mapped.status === 'authenticated'
|
mapped.status === 'authenticated' ? 'waiting_operator' : task.execution_status
|
||||||
? task.platform === 'weibo' || task.binding_status === 'confirmed'
|
|
||||||
? 'waiting_operator'
|
|
||||||
: 'waiting_customer'
|
|
||||||
: task.execution_status
|
|
||||||
const storedCredential =
|
const storedCredential =
|
||||||
mapped.status === 'authenticated' && mapped.hasCredential
|
mapped.status === 'authenticated' && mapped.hasCredential
|
||||||
? task.platform === 'weibo'
|
? task.platform === 'weibo'
|
||||||
@@ -946,86 +779,27 @@ function mapBindingUpdate(session: ConnectorBindingSession) {
|
|||||||
return {
|
return {
|
||||||
status: statusMap[session.status] || 'failed',
|
status: statusMap[session.status] || 'failed',
|
||||||
qrImage: String(session.qrImage || ''),
|
qrImage: String(session.qrImage || ''),
|
||||||
qrExpiresAt: String(session.qrExpiresAt || ''),
|
|
||||||
qrUrl: String(session.qrUrl || ''),
|
|
||||||
bindGameAccount: String(session.bindGameAccount || ''),
|
bindGameAccount: String(session.bindGameAccount || ''),
|
||||||
bindGameRole: String(session.bindGameRole || ''),
|
bindGameRole: String(session.bindGameRole || ''),
|
||||||
bindGameRoleId: String(session.bindGameRoleId || ''),
|
|
||||||
bindGameChannel: String(session.bindGameChannel || ''),
|
bindGameChannel: String(session.bindGameChannel || ''),
|
||||||
hasExistingBinding: session.hasExistingBinding === true,
|
|
||||||
rebindRequested: session.rebindRequested === true,
|
|
||||||
message: String(session.message || ''),
|
message: String(session.message || ''),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BindingRoleComparison = {
|
|
||||||
verdict: 'match' | 'mismatch' | 'unknown'
|
|
||||||
customerRoleName: string
|
|
||||||
customerRoleId: string
|
|
||||||
boundRoleName: string
|
|
||||||
boundRoleId: string
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRoleText(value: unknown): string {
|
|
||||||
return String(value ?? '')
|
|
||||||
.replace(/\s+/g, '')
|
|
||||||
.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 客户填写角色与平台识别角色必须一致才允许确认绑定。
|
|
||||||
*
|
|
||||||
* 两侧都有角色 ID 时以 ID 为准(截图里的“两侧角色 ID 不一致”);
|
|
||||||
* 任一侧缺 ID 时退回角色名比对;两边都取不到才判 unknown,转客服核对。
|
|
||||||
*/
|
|
||||||
export function compareBindingRole(
|
|
||||||
task: Pick<TaskRow, 'customer_game_id' | 'customer_game_role' | 'huya_uid'>,
|
|
||||||
binding: { bindGameRole?: string; bindGameRoleId?: string },
|
|
||||||
): BindingRoleComparison {
|
|
||||||
const customerRoleName = String(task.customer_game_role || '').trim()
|
|
||||||
const customerRoleId = String(task.customer_game_id || '').trim()
|
|
||||||
const boundRoleName = String(binding.bindGameRole || '').trim()
|
|
||||||
const rawBoundRoleId = String(binding.bindGameRoleId || '').trim()
|
|
||||||
// 虎牙 gameRole.roleId 是 40 位加密串,客户填写的是数值游戏编号;
|
|
||||||
// 平台侧可比对的是登录账号的数值 UID(huyaUid),不能拿加密串直接比对。
|
|
||||||
const numericHuyaUid = /^\d+$/.test(String(task.huya_uid || '').trim())
|
|
||||||
? String(task.huya_uid).trim()
|
|
||||||
: ''
|
|
||||||
const boundRoleId = /^\d+$/.test(rawBoundRoleId)
|
|
||||||
? rawBoundRoleId
|
|
||||||
: numericHuyaUid || rawBoundRoleId
|
|
||||||
const base = { customerRoleName, customerRoleId, boundRoleName, boundRoleId }
|
|
||||||
if (customerRoleId && boundRoleId) {
|
|
||||||
return customerRoleId === boundRoleId
|
|
||||||
? { ...base, verdict: 'match', message: '两侧角色 ID 一致,可确认绑定' }
|
|
||||||
: { ...base, verdict: 'mismatch', message: '两侧角色 ID 不一致,无法确认绑定' }
|
|
||||||
}
|
|
||||||
if (customerRoleName && boundRoleName) {
|
|
||||||
return normalizeRoleText(customerRoleName) === normalizeRoleText(boundRoleName)
|
|
||||||
? { ...base, verdict: 'match', message: '两侧角色名称一致,可确认绑定' }
|
|
||||||
: { ...base, verdict: 'mismatch', message: '两侧角色名称不一致,无法确认绑定' }
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
verdict: 'unknown',
|
|
||||||
message: '未获取到可核对的两侧角色信息,请联系客服核对后再确认',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateBindingTask(
|
async function updateBindingTask(
|
||||||
task: TaskRow,
|
task: TaskRow,
|
||||||
session: ConnectorBindingSession,
|
session: ConnectorBindingSession,
|
||||||
actorType: TaskActorType,
|
actorType: TaskActorType,
|
||||||
) {
|
) {
|
||||||
const mapped = mapBindingUpdate(session)
|
const mapped = mapBindingUpdate(session)
|
||||||
const comparison = compareBindingRole(task, mapped)
|
|
||||||
const executionStatus: ExecutionStatus =
|
const executionStatus: ExecutionStatus =
|
||||||
mapped.status === 'confirmed'
|
mapped.status === 'confirmed'
|
||||||
? 'waiting_operator'
|
? 'waiting_operator'
|
||||||
: task.execution_status === 'completed' || task.execution_status === 'cancelled'
|
: mapped.status === 'failed' || mapped.status === 'expired'
|
||||||
? task.execution_status
|
? 'failed'
|
||||||
: 'waiting_customer'
|
: task.execution_status === 'completed' || task.execution_status === 'cancelled'
|
||||||
|
? task.execution_status
|
||||||
|
: 'waiting_customer'
|
||||||
await withTransaction(async (client) => {
|
await withTransaction(async (client) => {
|
||||||
await client.query(
|
await client.query(
|
||||||
`
|
`
|
||||||
@@ -1033,7 +807,6 @@ async function updateBindingTask(
|
|||||||
SET binding_status = $2,
|
SET binding_status = $2,
|
||||||
bind_game_account = CASE WHEN $3 <> '' THEN $3 ELSE bind_game_account END,
|
bind_game_account = CASE WHEN $3 <> '' THEN $3 ELSE bind_game_account END,
|
||||||
bind_game_role = CASE WHEN $4 <> '' THEN $4 ELSE bind_game_role END,
|
bind_game_role = CASE WHEN $4 <> '' THEN $4 ELSE bind_game_role END,
|
||||||
bind_game_role_id = CASE WHEN $8 <> '' THEN $8 ELSE bind_game_role_id END,
|
|
||||||
bind_game_channel = CASE WHEN $5 <> '' THEN $5 ELSE bind_game_channel END,
|
bind_game_channel = CASE WHEN $5 <> '' THEN $5 ELSE bind_game_channel END,
|
||||||
bound_at = CASE WHEN $2 IN ('role_ready', 'confirmed') THEN COALESCE(bound_at, NOW()) ELSE bound_at END,
|
bound_at = CASE WHEN $2 IN ('role_ready', 'confirmed') THEN COALESCE(bound_at, NOW()) ELSE bound_at END,
|
||||||
execution_status = $6,
|
execution_status = $6,
|
||||||
@@ -1049,7 +822,6 @@ async function updateBindingTask(
|
|||||||
mapped.bindGameChannel,
|
mapped.bindGameChannel,
|
||||||
executionStatus,
|
executionStatus,
|
||||||
mapped.message,
|
mapped.message,
|
||||||
mapped.bindGameRoleId,
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
await appendTaskEvent(client, task.id, {
|
await appendTaskEvent(client, task.id, {
|
||||||
@@ -1062,9 +834,7 @@ async function updateBindingTask(
|
|||||||
detail: {
|
detail: {
|
||||||
gameAccount: mapped.bindGameAccount,
|
gameAccount: mapped.bindGameAccount,
|
||||||
gameRole: mapped.bindGameRole,
|
gameRole: mapped.bindGameRole,
|
||||||
gameRoleId: mapped.bindGameRoleId,
|
|
||||||
gameChannel: mapped.bindGameChannel,
|
gameChannel: mapped.bindGameChannel,
|
||||||
verdict: comparison.verdict,
|
|
||||||
message: mapped.message,
|
message: mapped.message,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1072,16 +842,10 @@ async function updateBindingTask(
|
|||||||
return {
|
return {
|
||||||
status: mapped.status,
|
status: mapped.status,
|
||||||
qrImage: mapped.qrImage,
|
qrImage: mapped.qrImage,
|
||||||
qrExpiresAt: mapped.qrExpiresAt,
|
|
||||||
qrUrl: mapped.qrUrl,
|
|
||||||
bindGameAccount: mapped.bindGameAccount,
|
bindGameAccount: mapped.bindGameAccount,
|
||||||
bindGameRole: mapped.bindGameRole,
|
bindGameRole: mapped.bindGameRole,
|
||||||
bindGameRoleId: mapped.bindGameRoleId,
|
|
||||||
bindGameChannel: mapped.bindGameChannel,
|
bindGameChannel: mapped.bindGameChannel,
|
||||||
hasExistingBinding: mapped.hasExistingBinding,
|
|
||||||
rebindRequested: mapped.rebindRequested,
|
|
||||||
message: mapped.message,
|
message: mapped.message,
|
||||||
comparison,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1090,7 +854,6 @@ export async function startPublicLogin(orderNoInput: unknown, taskIdInput: unkno
|
|||||||
const taskId = normalizeId(taskIdInput)
|
const taskId = normalizeId(taskIdInput)
|
||||||
await ensureSemiAutoTasksForOrder(orderNo)
|
await ensureSemiAutoTasksForOrder(orderNo)
|
||||||
const task = await loadTaskForOrder(orderNo, taskId)
|
const task = await loadTaskForOrder(orderNo, taskId)
|
||||||
assertCustomerProfileReady(task)
|
|
||||||
if (task.execution_status === 'completed' || task.execution_status === 'cancelled') {
|
if (task.execution_status === 'completed' || task.execution_status === 'cancelled') {
|
||||||
throw createHttpError('当前订单已结束,不能重新登录', {
|
throw createHttpError('当前订单已结束,不能重新登录', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
@@ -1144,13 +907,6 @@ export async function startPublicLogin(orderNoInput: unknown, taskIdInput: unkno
|
|||||||
UPDATE semi_auto_tasks
|
UPDATE semi_auto_tasks
|
||||||
SET connector_session_id = $2,
|
SET connector_session_id = $2,
|
||||||
login_status = 'qr_pending',
|
login_status = 'qr_pending',
|
||||||
binding_status = 'not_started',
|
|
||||||
bind_game_account = '',
|
|
||||||
bind_game_role = '',
|
|
||||||
bind_game_role_id = '',
|
|
||||||
bind_game_channel = '',
|
|
||||||
binding_started_at = NULL,
|
|
||||||
bound_at = NULL,
|
|
||||||
execution_status = 'waiting_customer',
|
execution_status = 'waiting_customer',
|
||||||
auth_started_at = COALESCE(auth_started_at, NOW()),
|
auth_started_at = COALESCE(auth_started_at, NOW()),
|
||||||
last_error = '',
|
last_error = '',
|
||||||
@@ -1271,13 +1027,6 @@ export async function confirmPublicBinding(orderNoInput: unknown, taskIdInput: u
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const session = await getBindingForTask(task, task.connector_session_id)
|
const session = await getBindingForTask(task, task.connector_session_id)
|
||||||
const comparison = compareBindingRole(task, session)
|
|
||||||
if (comparison.verdict === 'mismatch') {
|
|
||||||
throw createHttpError(comparison.message, {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'semi_auto_binding_role_mismatch',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const binding = await updateBindingTask(task, session, 'customer')
|
const binding = await updateBindingTask(task, session, 'customer')
|
||||||
const updated = await loadTask(task.id)
|
const updated = await loadTask(task.id)
|
||||||
return { task: mapTask(updated), binding }
|
return { task: mapTask(updated), binding }
|
||||||
@@ -1292,7 +1041,6 @@ export async function sendPublicHuyaSmsCode(
|
|||||||
const taskId = normalizeId(taskIdInput)
|
const taskId = normalizeId(taskIdInput)
|
||||||
const task = await loadTaskForOrder(orderNo, taskId)
|
const task = await loadTaskForOrder(orderNo, taskId)
|
||||||
assertHuyaTask(task)
|
assertHuyaTask(task)
|
||||||
assertCustomerProfileReady(task)
|
|
||||||
if (task.execution_status === 'completed' || task.execution_status === 'cancelled') {
|
if (task.execution_status === 'completed' || task.execution_status === 'cancelled') {
|
||||||
throw createHttpError('当前订单已结束,不能重新登录', {
|
throw createHttpError('当前订单已结束,不能重新登录', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
@@ -1442,7 +1190,6 @@ export async function loginPublicHuyaSms(
|
|||||||
const taskId = normalizeId(taskIdInput)
|
const taskId = normalizeId(taskIdInput)
|
||||||
const task = await loadTaskForOrder(orderNo, taskId)
|
const task = await loadTaskForOrder(orderNo, taskId)
|
||||||
assertHuyaTask(task)
|
assertHuyaTask(task)
|
||||||
assertCustomerProfileReady(task)
|
|
||||||
if (task.execution_status === 'completed' || task.execution_status === 'cancelled') {
|
if (task.execution_status === 'completed' || task.execution_status === 'cancelled') {
|
||||||
throw createHttpError('当前订单已结束,不能重新登录', {
|
throw createHttpError('当前订单已结束,不能重新登录', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
|
|||||||
@@ -1076,10 +1076,8 @@ export class WeiboNativeConnector {
|
|||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
qrImage: '',
|
qrImage: '',
|
||||||
qrExpiresAt: '',
|
qrExpiresAt: '',
|
||||||
qrUrl: '',
|
|
||||||
bindGameAccount: account?.nickname || '',
|
bindGameAccount: account?.nickname || '',
|
||||||
bindGameRole: account?.uid || '',
|
bindGameRole: account?.uid || '',
|
||||||
bindGameRoleId: account?.uid || '',
|
|
||||||
bindGameChannel: '微博',
|
bindGameChannel: '微博',
|
||||||
hasExistingBinding: true,
|
hasExistingBinding: true,
|
||||||
rebindRequested: false,
|
rebindRequested: false,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import type {
|
|||||||
} from '../../domain/consignment-deposit-policy.js'
|
} from '../../domain/consignment-deposit-policy.js'
|
||||||
import { settleWorkOrderDepositWithClient } from '../../repositories/worker-platform/work-order-deposit-repo.js'
|
import { settleWorkOrderDepositWithClient } from '../../repositories/worker-platform/work-order-deposit-repo.js'
|
||||||
import { buildConsignmentRevokeInventoryUpdate } from '../../repositories/worker-platform/consignment-revoke-inventory-sql.js'
|
import { buildConsignmentRevokeInventoryUpdate } from '../../repositories/worker-platform/consignment-revoke-inventory-sql.js'
|
||||||
|
import { releaseConsignmentAllocationsForRevokedFulfillmentWithClient } from '../../repositories/worker-platform/consignment-allocation-repo.js'
|
||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
import { safeParseJson } from '../admin/admin-query-utils.js'
|
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||||
import { publishWorkOrderRealtimeChange } from '../realtime/realtime-event-service.js'
|
import { publishWorkOrderRealtimeChange } from '../realtime/realtime-event-service.js'
|
||||||
@@ -1083,7 +1084,7 @@ export async function revokeWorkOrderConsignmentFulfillment(input: {
|
|||||||
const lockedMaterial = safeParseJson(lockedOrder.material_json)
|
const lockedMaterial = safeParseJson(lockedOrder.material_json)
|
||||||
resolveRevocableConsignmentInventoryId(safeParseJson(lockedMaterial.consignment))
|
resolveRevocableConsignmentInventoryId(safeParseJson(lockedMaterial.consignment))
|
||||||
|
|
||||||
let inv: ConsignmentInventoryRow | null = null
|
let inv: ConsignmentInventoryRow | null
|
||||||
|
|
||||||
// 1. 处理关联库存解绑与状态流转。
|
// 1. 处理关联库存解绑与状态流转。
|
||||||
// 必须带「仍被本工单占用」条件:否则会把别的工单正在使用的现货抢过来解绑,
|
// 必须带「仍被本工单占用」条件:否则会把别的工单正在使用的现货抢过来解绑,
|
||||||
@@ -1112,6 +1113,22 @@ export async function revokeWorkOrderConsignmentFulfillment(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1.1 释放占用记录:remaining_quantity 与卖家端「进行中」都以 allocation 为准。
|
||||||
|
// 旧实现只改 consignment_inventories(status/bound_*),占用行停在 reserved、余量停在 0,
|
||||||
|
// 现货回库却永远匹配不到,卖家端还一直显示占用(工单 217368 / 库存 1584)。
|
||||||
|
await releaseConsignmentAllocationsForRevokedFulfillmentWithClient(client, {
|
||||||
|
workOrderId: Number(lockedOrder.id),
|
||||||
|
inventoryId: Number(inv.id),
|
||||||
|
archive: archiveOnly,
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
// 释放后余量/状态已变,重读一次让接口返回真实值,前端不用再等下一次刷新。
|
||||||
|
const refreshedInv = await client.query<ConsignmentInventoryRow>(
|
||||||
|
'SELECT * FROM consignment_inventories WHERE id = $1',
|
||||||
|
[inv.id],
|
||||||
|
)
|
||||||
|
inv = refreshedInv.rows[0] || inv
|
||||||
|
|
||||||
// 卖家发布押金按选择走:auto 时回库保持冻结、作废退还;只有显式「扣除没收」才扣。
|
// 卖家发布押金按选择走:auto 时回库保持冻结、作废退还;只有显式「扣除没收」才扣。
|
||||||
// 买家退款不因已确认加好友而没收卖家押金。
|
// 买家退款不因已确认加好友而没收卖家押金。
|
||||||
const consignmentDepositAction = resolveConsignmentDepositAction(input.consignmentDeposit, {
|
const consignmentDepositAction = resolveConsignmentDepositAction(input.consignmentDeposit, {
|
||||||
|
|||||||
@@ -32,6 +32,21 @@ test('撤销寄售履约的现货去向和押金都按运营选择,已确认
|
|||||||
assert.ok(source.includes('inventoryArchived: archiveOnly'))
|
assert.ok(source.includes('inventoryArchived: archiveOnly'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('撤销寄售履约必须释放占用记录,不能只把库存状态改回在库', async () => {
|
||||||
|
const source = await readServiceSource()
|
||||||
|
const start = source.indexOf('export async function revokeWorkOrderConsignmentFulfillment(')
|
||||||
|
assert.ok(start > 0, '未找到撤销寄售履约函数')
|
||||||
|
const body = source.slice(start, source.indexOf('\n}\n', start))
|
||||||
|
|
||||||
|
const inventoryUpdateIndex = body.indexOf('buildConsignmentRevokeInventoryUpdate(')
|
||||||
|
const releaseIndex = body.indexOf('releaseConsignmentAllocationsForRevokedFulfillmentWithClient(')
|
||||||
|
assert.ok(inventoryUpdateIndex > 0, '撤销必须先解绑库存')
|
||||||
|
assert.ok(
|
||||||
|
releaseIndex > inventoryUpdateIndex,
|
||||||
|
'解绑库存后必须释放 allocation:remaining_quantity 与卖家端「进行中」都以它为准,只改库存会让现货永远匹配不到',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test('已确认加好友后作废现货:必须关掉匹配开关并留痕', async () => {
|
test('已确认加好友后作废现货:必须关掉匹配开关并留痕', async () => {
|
||||||
const source = await readServiceSource()
|
const source = await readServiceSource()
|
||||||
// 解绑 SQL 已抽到 consignment-revoke-inventory-sql.ts(参数个数由该模块自己的单测锁定)
|
// 解绑 SQL 已抽到 consignment-revoke-inventory-sql.ts(参数个数由该模块自己的单测锁定)
|
||||||
|
|||||||
@@ -138,21 +138,6 @@ function applySessionExpiryHeaders(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** HTTP 状态码兜底文案:避免把英文 statusText(如 Not Found)原样抛给用户 */
|
|
||||||
const HTTP_STATUS_MESSAGES: Record<number, string> = {
|
|
||||||
400: '请求参数有误',
|
|
||||||
401: '登录状态已失效,请重新登录',
|
|
||||||
403: '没有操作权限',
|
|
||||||
404: '接口不存在,请刷新页面后重试',
|
|
||||||
409: '数据已更新,请刷新后重试',
|
|
||||||
422: '提交的数据不合法',
|
|
||||||
429: '操作过于频繁,请稍后再试',
|
|
||||||
500: '服务器开小差了,请稍后重试',
|
|
||||||
502: '服务暂时不可用,请稍后重试',
|
|
||||||
503: '服务维护中,请稍后重试',
|
|
||||||
504: '请求超时,请稍后重试',
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveFallbackHttpMessage(error: unknown) {
|
function resolveFallbackHttpMessage(error: unknown) {
|
||||||
const message = String((error as { message?: string })?.message ?? '')
|
const message = String((error as { message?: string })?.message ?? '')
|
||||||
|
|
||||||
@@ -164,13 +149,6 @@ function resolveFallbackHttpMessage(error: unknown) {
|
|||||||
return '网络连接错误'
|
return '网络连接错误'
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = Number((error as { response?: { status?: number } })?.response?.status || 0)
|
|
||||||
const statusMessage = status > 0 ? HTTP_STATUS_MESSAGES[status] : ''
|
|
||||||
|
|
||||||
if (statusMessage) {
|
|
||||||
return statusMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof (error as { response?: { statusText?: string } })?.response?.statusText === 'string') {
|
if (typeof (error as { response?: { statusText?: string } })?.response?.statusText === 'string') {
|
||||||
return String((error as { response: { statusText: string } }).response.statusText).trim()
|
return String((error as { response: { statusText: string } }).response.statusText).trim()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ const STATUS_COLORS: Record<string, string> = {
|
|||||||
login_scanned: 'cyan',
|
login_scanned: 'cyan',
|
||||||
waiting_binding: 'gold',
|
waiting_binding: 'gold',
|
||||||
binding_confirm: 'orange',
|
binding_confirm: 'orange',
|
||||||
binding_expired: 'red',
|
|
||||||
binding_failed: 'red',
|
|
||||||
waiting_operator: 'volcano',
|
waiting_operator: 'volcano',
|
||||||
credential_expired: 'red',
|
credential_expired: 'red',
|
||||||
processing: 'processing',
|
processing: 'processing',
|
||||||
@@ -69,16 +67,6 @@ const PLATFORM_LABELS: Record<string, string> = {
|
|||||||
weibo: '微博',
|
weibo: '微博',
|
||||||
}
|
}
|
||||||
|
|
||||||
const BINDING_STATUS_LABELS: Record<string, string> = {
|
|
||||||
not_started: '未开始',
|
|
||||||
qr_pending: '待扫码',
|
|
||||||
scanned: '已扫码',
|
|
||||||
role_ready: '待确认角色',
|
|
||||||
confirmed: '已确认',
|
|
||||||
failed: '失败',
|
|
||||||
expired: '已过期',
|
|
||||||
}
|
|
||||||
|
|
||||||
type CompleteFormValues = {
|
type CompleteFormValues = {
|
||||||
note?: string
|
note?: string
|
||||||
externalOrderNo?: string
|
externalOrderNo?: string
|
||||||
@@ -356,16 +344,6 @@ export default function AdminSemiAutoTasksPage() {
|
|||||||
<Space direction="vertical" size={2}>
|
<Space direction="vertical" size={2}>
|
||||||
<Tag>{task.displayStatus.label}</Tag>
|
<Tag>{task.displayStatus.label}</Tag>
|
||||||
<Text type="secondary">{task.huyaUid ? `UID ${task.huyaUid}` : '未获取 UID'}</Text>
|
<Text type="secondary">{task.huyaUid ? `UID ${task.huyaUid}` : '未获取 UID'}</Text>
|
||||||
<Text type="secondary">
|
|
||||||
{task.platform === 'huya'
|
|
||||||
? `绑定:${BINDING_STATUS_LABELS[task.bindingStatus] || task.bindingStatus}`
|
|
||||||
: '微博登录即授权'}
|
|
||||||
</Text>
|
|
||||||
{task.platform === 'huya' && (task.bindGameRole || task.bindGameRoleId) ? (
|
|
||||||
<Text type="secondary">
|
|
||||||
{`角色:${task.bindGameRole || '-'}${task.bindGameRoleId ? `(${task.bindGameRoleId})` : '(无角色ID)'}`}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
CK:
|
CK:
|
||||||
{task.credentialStatus === 'valid'
|
{task.credentialStatus === 'valid'
|
||||||
@@ -638,24 +616,6 @@ export default function AdminSemiAutoTasksPage() {
|
|||||||
<Descriptions.Item label="登录状态">
|
<Descriptions.Item label="登录状态">
|
||||||
{detail.task.loginStatus === 'authenticated' ? 'CK 已获取' : '等待客户登录'}
|
{detail.task.loginStatus === 'authenticated' ? 'CK 已获取' : '等待客户登录'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="绑定状态">
|
|
||||||
{detail.task.platform === 'weibo'
|
|
||||||
? '微博登录即授权'
|
|
||||||
: BINDING_STATUS_LABELS[detail.task.bindingStatus] || detail.task.bindingStatus}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="绑定角色">
|
|
||||||
{detail.task.bindGameRole
|
|
||||||
? `${detail.task.bindGameRole}${detail.task.bindGameRoleId ? `(${detail.task.bindGameRoleId})` : '(无角色ID)'}`
|
|
||||||
: '-'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="客户填写角色">
|
|
||||||
{detail.task.customerGameRoleName
|
|
||||||
? `${detail.task.customerGameRoleName}${detail.task.customerGameRoleId ? `(${detail.task.customerGameRoleId})` : ''}`
|
|
||||||
: '-'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="区服">
|
|
||||||
{detail.task.customerServerZone || '-'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="客服">
|
<Descriptions.Item label="客服">
|
||||||
{detail.task.assignedAdminName || '未领取'}
|
{detail.task.assignedAdminName || '未领取'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
@@ -805,7 +765,6 @@ function taskNeedsCustomerReauth(task: SemiAutoTask) {
|
|||||||
function taskCanUseCredential(task: SemiAutoTask) {
|
function taskCanUseCredential(task: SemiAutoTask) {
|
||||||
return (
|
return (
|
||||||
task.loginStatus === 'authenticated' &&
|
task.loginStatus === 'authenticated' &&
|
||||||
(task.platform === 'weibo' || task.bindingStatus === 'confirmed') &&
|
|
||||||
task.credentialReady &&
|
task.credentialReady &&
|
||||||
task.credentialStatus !== 'invalid' &&
|
task.credentialStatus !== 'invalid' &&
|
||||||
task.credentialStatus !== 'error'
|
task.credentialStatus !== 'error'
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,5 @@
|
|||||||
import { apiGet, apiPost } from '@/lib/http'
|
import { apiGet, apiPost } from '@/lib/http'
|
||||||
import type {
|
import type { CustomerLoginInfo, CustomerSemiAutoTask } from '@/types/customer-fulfillment'
|
||||||
CustomerBindingInfo,
|
|
||||||
CustomerLoginInfo,
|
|
||||||
CustomerSemiAutoTask,
|
|
||||||
} from '@/types/customer-fulfillment'
|
|
||||||
|
|
||||||
export function fetchCustomerFulfillment(orderNo: string) {
|
export function fetchCustomerFulfillment(orderNo: string) {
|
||||||
return apiGet<CustomerSemiAutoTask[]>(
|
return apiGet<CustomerSemiAutoTask[]>(
|
||||||
@@ -18,17 +14,6 @@ export function markCustomerFulfillmentOpened(orderNo: string, taskId: number) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveCustomerTaskProfile(
|
|
||||||
orderNo: string,
|
|
||||||
taskId: number,
|
|
||||||
input: { roleName: string; roleId: string; serverZone: string },
|
|
||||||
) {
|
|
||||||
return apiPost<{ task: CustomerSemiAutoTask }>(
|
|
||||||
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/profile`,
|
|
||||||
input,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startCustomerLogin(orderNo: string, taskId: number) {
|
export function startCustomerLogin(orderNo: string, taskId: number) {
|
||||||
return apiPost<{ task: CustomerSemiAutoTask; login: CustomerLoginInfo }>(
|
return apiPost<{ task: CustomerSemiAutoTask; login: CustomerLoginInfo }>(
|
||||||
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/login/start`,
|
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/login/start`,
|
||||||
@@ -42,30 +27,6 @@ export function pollCustomerLogin(orderNo: string, taskId: number) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startCustomerBinding(
|
|
||||||
orderNo: string,
|
|
||||||
taskId: number,
|
|
||||||
options: { forceRebind?: boolean } = {},
|
|
||||||
) {
|
|
||||||
return apiPost<{ task: CustomerSemiAutoTask; binding: CustomerBindingInfo }>(
|
|
||||||
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/binding/start`,
|
|
||||||
options.forceRebind ? { forceRebind: true } : {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pollCustomerBinding(orderNo: string, taskId: number) {
|
|
||||||
return apiGet<{ task: CustomerSemiAutoTask; binding: CustomerBindingInfo | null }>(
|
|
||||||
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/binding/status`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function confirmCustomerBinding(orderNo: string, taskId: number) {
|
|
||||||
return apiPost<{ task: CustomerSemiAutoTask; binding: CustomerBindingInfo }>(
|
|
||||||
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/binding/confirm`,
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomerHuyaChallengeBase = {
|
type CustomerHuyaChallengeBase = {
|
||||||
image: string
|
image: string
|
||||||
width: number
|
width: number
|
||||||
|
|||||||
@@ -1437,47 +1437,54 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-page {
|
.customer-auth-page {
|
||||||
--auth-accent: #2f6fed;
|
--auth-accent: #ff8a00;
|
||||||
--auth-accent-hover: #4a83ff;
|
--auth-accent-hover: #ffa12f;
|
||||||
--auth-panel: #232936;
|
--auth-panel: #07142e;
|
||||||
--auth-panel-soft: #2a3140;
|
--auth-panel-soft: #0d1d3d;
|
||||||
--auth-line: rgba(255, 255, 255, 0.08);
|
--auth-line: rgba(255, 255, 255, 0.11);
|
||||||
--auth-text: #f5f7fa;
|
--auth-text: #f8fafc;
|
||||||
--auth-muted: #9aa3b2;
|
--auth-muted: #a6b0c3;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 0;
|
padding: clamp(20px, 4vw, 48px) 16px;
|
||||||
color: var(--auth-text);
|
color: var(--auth-text);
|
||||||
background: #232936;
|
background: #202124;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-shell {
|
.customer-auth-shell {
|
||||||
width: min(560px, 100%);
|
width: min(760px, 100%);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-self: stretch;
|
align-self: flex-start;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: none;
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 0;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: var(--auth-panel);
|
||||||
box-shadow: none;
|
box-shadow: 0 28px 70px rgba(0, 0, 0, 0.34);
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-hero {
|
.customer-auth-hero {
|
||||||
min-height: auto;
|
min-height: 176px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
padding: 34px 38px;
|
||||||
padding: 38px 16px 10px;
|
background-image:
|
||||||
background: none;
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(3, 18, 55, 0.97) 0%,
|
||||||
|
rgba(5, 23, 65, 0.83) 48%,
|
||||||
|
rgba(7, 20, 43, 0.45) 100%
|
||||||
|
),
|
||||||
|
url('https://game.gtimg.cn/images/gp/web201908/inside_kv.jpg');
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-brand {
|
.customer-auth-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 18px;
|
||||||
gap: 0;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1498,30 +1505,28 @@
|
|||||||
.customer-auth-brand h1 {
|
.customer-auth-brand h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 20px;
|
font-size: clamp(18px, 2.3vw, 22px);
|
||||||
line-height: 1.4;
|
line-height: 1.3;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
text-align: center;
|
white-space: nowrap;
|
||||||
white-space: normal;
|
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-warning {
|
.customer-auth-warning {
|
||||||
padding: 6px 16px 10px;
|
padding: 19px 24px;
|
||||||
color: #ff6975;
|
color: #ff6975;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
background: transparent;
|
background: #06122b;
|
||||||
border-bottom: none;
|
border-bottom: 1px solid var(--auth-line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-main {
|
.customer-auth-main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-bottom: 24px;
|
|
||||||
color: var(--auth-text);
|
color: var(--auth-text);
|
||||||
background: transparent;
|
background: var(--auth-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-content-stack {
|
.customer-auth-content-stack {
|
||||||
@@ -1600,8 +1605,8 @@
|
|||||||
|
|
||||||
.customer-auth-section {
|
.customer-auth-section {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 16px;
|
padding: 28px 26px 30px;
|
||||||
border-bottom: none;
|
border-bottom: 1px solid var(--auth-line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-section-heading {
|
.customer-auth-section-heading {
|
||||||
@@ -1611,24 +1616,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-section-icon {
|
.customer-auth-section-icon {
|
||||||
width: 36px;
|
width: 42px;
|
||||||
height: 36px;
|
height: 42px;
|
||||||
flex: none;
|
flex: none;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
border-radius: 8px;
|
border-radius: 7px;
|
||||||
color: var(--auth-accent);
|
color: var(--auth-accent);
|
||||||
font-size: 18px;
|
font-size: 22px;
|
||||||
background: rgba(255, 255, 255, 0.035);
|
background: rgba(255, 255, 255, 0.035);
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-section-heading h2,
|
.customer-auth-section-heading h2,
|
||||||
.customer-auth-state h2 {
|
.customer-auth-state h2 {
|
||||||
margin: 0 0 6px;
|
margin: 0 0 8px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 18px;
|
font-size: 23px;
|
||||||
line-height: 1.4;
|
line-height: 1.35;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2076,607 +2081,6 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-binding {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-binding-tip {
|
|
||||||
color: #ffb454;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-profile,
|
|
||||||
.customer-auth-profile-summary {
|
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-profile > p {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
color: var(--auth-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-profile-summary {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-profile-summary span {
|
|
||||||
color: var(--auth-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-profile-summary strong {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-profile-summary em {
|
|
||||||
color: var(--auth-muted);
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 客户资料页(图 1) */
|
|
||||||
.customer-collect-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #0b1220;
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-hero {
|
|
||||||
position: relative;
|
|
||||||
height: 210px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-hero-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: left center;
|
|
||||||
filter: saturate(0.9) brightness(0.72);
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-hero-mask {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: linear-gradient(180deg, rgba(11, 18, 32, 0.1), rgba(11, 18, 32, 0.96));
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-warning {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 22px;
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
color: #f87171;
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-body {
|
|
||||||
max-width: 560px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 24px 20px 48px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-label {
|
|
||||||
margin-top: 8px;
|
|
||||||
color: #e5e7eb;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-label span {
|
|
||||||
color: #f87171;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-readonly {
|
|
||||||
height: 56px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #1b2436;
|
|
||||||
border: 1px solid #26344d;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-card {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid #1e2f4a;
|
|
||||||
background: #141d30;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-card > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
color: #cbd5e1;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-card > div > span {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-card > div strong {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-width: 0;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: right;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-card > p {
|
|
||||||
margin: 6px 0 0;
|
|
||||||
color: #fbbf24;
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-input.ant-input {
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #1b2436;
|
|
||||||
border-color: #26344d;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-input.ant-input::placeholder {
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-select.ant-select {
|
|
||||||
width: 100%;
|
|
||||||
height: 56px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 15px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid #26344d;
|
|
||||||
background: #1b2436;
|
|
||||||
color: #fff;
|
|
||||||
--ant-select-height: 56px;
|
|
||||||
--ant-select-border-radius: 12px;
|
|
||||||
--ant-select-border-color: #26344d;
|
|
||||||
--ant-select-background-color: #1b2436;
|
|
||||||
--ant-select-color: #fff;
|
|
||||||
--ant-select-hover-border-color: #3b4d6b;
|
|
||||||
--ant-select-active-border-color: #2563eb;
|
|
||||||
--ant-select-active-outline-color: rgba(37, 99, 235, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-select.ant-select:hover {
|
|
||||||
border-color: #3b4d6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-select.ant-select.ant-select-focused {
|
|
||||||
border-color: #2563eb;
|
|
||||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-select .ant-select-content {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-select .ant-select-selection-item {
|
|
||||||
color: #fff;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-select .ant-select-placeholder {
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-select .ant-select-suffix {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-dropdown.customer-collect-dropdown {
|
|
||||||
padding: 6px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #141d30;
|
|
||||||
box-shadow: 0 14px 34px rgba(2, 6, 23, 0.55);
|
|
||||||
--ant-select-option-active-bg: #1b2436;
|
|
||||||
--ant-select-option-selected-bg: #1e3a8a;
|
|
||||||
--ant-select-option-selected-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-dropdown .ant-select-item {
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-dropdown .ant-select-item-option-active {
|
|
||||||
background: #1b2436 !important;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-dropdown .ant-select-item-option-selected {
|
|
||||||
background: #1e3a8a !important;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-page .customer-collect-submit.ant-btn {
|
|
||||||
height: 56px;
|
|
||||||
margin-top: 14px;
|
|
||||||
border-radius: 14px;
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 600;
|
|
||||||
--ant-btn-bg-color: #2563eb;
|
|
||||||
--ant-btn-bg-color-hover: #1d4ed8;
|
|
||||||
--ant-btn-bg-color-active: #1e40af;
|
|
||||||
--ant-btn-text-color: #fff;
|
|
||||||
--ant-btn-text-color-hover: #fff;
|
|
||||||
--ant-btn-text-color-active: #fff;
|
|
||||||
--ant-btn-border-color: #2563eb;
|
|
||||||
--ant-btn-border-color-hover: #1d4ed8;
|
|
||||||
--ant-btn-border-color-active: #1e40af;
|
|
||||||
background: #2563eb !important;
|
|
||||||
border-color: #2563eb !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-collect-page .customer-collect-submit.ant-btn:hover,
|
|
||||||
.customer-collect-page .customer-collect-submit.ant-btn:focus-visible {
|
|
||||||
background: #1d4ed8 !important;
|
|
||||||
border-color: #1d4ed8 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 登录与绑定页(图 2-10) */
|
|
||||||
.customer-login-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-login-hint {
|
|
||||||
margin: 0 0 2px;
|
|
||||||
color: #d5dae3;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-login-mobile-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-login-mobile.ant-input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
height: 52px;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-color: #3a4150;
|
|
||||||
background: #1b2130;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-login-mobile.ant-input::placeholder {
|
|
||||||
color: #6b7482;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-login-code-button.ant-btn {
|
|
||||||
flex: none;
|
|
||||||
width: 118px;
|
|
||||||
height: 52px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #3b82f6 !important;
|
|
||||||
border-color: #3b82f6 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-login-code-input.ant-input {
|
|
||||||
height: 52px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-color: #3a4150;
|
|
||||||
background: #1b2130;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 15px;
|
|
||||||
letter-spacing: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-login-code-input.ant-input::placeholder {
|
|
||||||
color: #6b7482;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-login-submit.ant-btn {
|
|
||||||
height: 52px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #0aa53a !important;
|
|
||||||
border-color: #0aa53a !important;
|
|
||||||
color: #fff !important;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-login-alt.ant-btn {
|
|
||||||
height: 52px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: transparent !important;
|
|
||||||
border-color: #3a4150 !important;
|
|
||||||
color: #cbd5e1 !important;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-order-card {
|
|
||||||
margin: 10px 16px 0;
|
|
||||||
padding: 16px;
|
|
||||||
border: 1px solid #3a4150;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #212733;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-order-card h3 {
|
|
||||||
margin: 0 0 4px;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-order-card > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
color: #cbd5e1;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-order-card > div > span {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: #9aa3b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-order-card > div strong {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-width: 0;
|
|
||||||
color: #fff;
|
|
||||||
text-align: right;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-stage {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-info {
|
|
||||||
padding: 16px;
|
|
||||||
border: 1px solid #3a4150;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #212733;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-info h3 {
|
|
||||||
margin: 0 0 4px;
|
|
||||||
color: #9aa3b2;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-info h3 small {
|
|
||||||
color: #64748b;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-info > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
color: #cbd5e1;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-info > div > span {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-info > div strong {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-width: 0;
|
|
||||||
color: #fff;
|
|
||||||
text-align: right;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-alert {
|
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-alert.is-warning {
|
|
||||||
color: #fbbf24;
|
|
||||||
border-color: #78350f;
|
|
||||||
background: rgba(120, 53, 15, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-alert.is-info {
|
|
||||||
color: #93c5fd;
|
|
||||||
border-color: #1e3a8a;
|
|
||||||
background: rgba(30, 58, 138, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-qr-card {
|
|
||||||
padding: 18px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #fff;
|
|
||||||
color: #111827;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-qr-card h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #6b7280;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-qr-hint {
|
|
||||||
margin: 0;
|
|
||||||
color: #6b7280;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-qr-tip {
|
|
||||||
margin: 0;
|
|
||||||
color: #f59e0b;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-qr-countdown {
|
|
||||||
margin: 0;
|
|
||||||
color: #6b7280;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-qr-actions {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 4px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-qr-actions .ant-btn {
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-next.ant-btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 46px;
|
|
||||||
margin-top: 6px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #2563eb !important;
|
|
||||||
border-color: #2563eb !important;
|
|
||||||
color: #fff !important;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-modal-footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-modal-actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-modal-actions .ant-btn {
|
|
||||||
height: 42px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-modal-error {
|
|
||||||
margin: 0;
|
|
||||||
color: #dc2626;
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-modal-warning {
|
|
||||||
margin: 0;
|
|
||||||
color: #d97706;
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-compare {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-compare-side {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: rgba(15, 23, 42, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-compare-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-compare-side strong {
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 16px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-binding-compare-side em {
|
|
||||||
color: #64748b;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.customer-binding-compare {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-button-stack {
|
.customer-auth-button-stack {
|
||||||
width: min(100%, 580px);
|
width: min(100%, 580px);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2786,43 +2190,49 @@
|
|||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.customer-auth-page {
|
.customer-auth-page {
|
||||||
height: auto;
|
height: 100dvh;
|
||||||
min-height: 100vh;
|
min-height: 0;
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: var(--auth-panel);
|
background: var(--auth-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-shell {
|
.customer-auth-shell {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: 100%;
|
||||||
display: block;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-hero {
|
.customer-auth-hero {
|
||||||
min-height: auto;
|
min-height: 116px;
|
||||||
padding: 38px 16px 10px;
|
flex: none;
|
||||||
background: none;
|
padding: 20px 22px;
|
||||||
|
background-position: 58% center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-brand h1 {
|
.customer-auth-brand h1 {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-warning {
|
.customer-auth-warning {
|
||||||
padding: 6px 16px 10px;
|
flex: none;
|
||||||
font-size: 14px;
|
padding: 12px 14px;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-main {
|
.customer-auth-main {
|
||||||
overflow: visible;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-content-stack {
|
.customer-auth-content-stack {
|
||||||
height: auto;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2847,7 +2257,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-section {
|
.customer-auth-section {
|
||||||
padding: 16px;
|
padding: 14px 16px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-auth-section-copy h2,
|
.customer-auth-section-copy h2,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export type SemiAutoTask = {
|
|||||||
platform: SemiAutoPlatform
|
platform: SemiAutoPlatform
|
||||||
platformLabel: string
|
platformLabel: string
|
||||||
loginStatus: string
|
loginStatus: string
|
||||||
bindingStatus: string
|
|
||||||
executionStatus: string
|
executionStatus: string
|
||||||
connectorMode: 'embedded' | 'external' | 'mock'
|
connectorMode: 'embedded' | 'external' | 'mock'
|
||||||
displayStatus: SemiAutoDisplayStatus
|
displayStatus: SemiAutoDisplayStatus
|
||||||
@@ -24,13 +23,6 @@ export type SemiAutoTask = {
|
|||||||
credentialUpdatedAt: string | null
|
credentialUpdatedAt: string | null
|
||||||
huyaUid: string
|
huyaUid: string
|
||||||
platformNickname: string
|
platformNickname: string
|
||||||
bindGameAccount: string
|
|
||||||
bindGameRole: string
|
|
||||||
bindGameRoleId: string
|
|
||||||
bindGameChannel: string
|
|
||||||
customerGameRoleId: string
|
|
||||||
customerGameRoleName: string
|
|
||||||
customerServerZone: string
|
|
||||||
targetQuantity: number
|
targetQuantity: number
|
||||||
completedQuantity: number
|
completedQuantity: number
|
||||||
assignedAdminUserId: number | null
|
assignedAdminUserId: number | null
|
||||||
|
|||||||
@@ -1,12 +1,3 @@
|
|||||||
export type CustomerBindingComparison = {
|
|
||||||
verdict: 'match' | 'mismatch' | 'unknown'
|
|
||||||
customerRoleName: string
|
|
||||||
customerRoleId: string
|
|
||||||
boundRoleName: string
|
|
||||||
boundRoleId: string
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CustomerSemiAutoTask = {
|
export type CustomerSemiAutoTask = {
|
||||||
id: number
|
id: number
|
||||||
platform: 'huya' | 'weibo'
|
platform: 'huya' | 'weibo'
|
||||||
@@ -17,18 +8,10 @@ export type CustomerSemiAutoTask = {
|
|||||||
skuCode: string
|
skuCode: string
|
||||||
quantity: number
|
quantity: number
|
||||||
loginStatus: string
|
loginStatus: string
|
||||||
bindingStatus: string
|
|
||||||
executionStatus: string
|
executionStatus: string
|
||||||
displayStatus: { key: string; label: string }
|
displayStatus: { key: string; label: string }
|
||||||
huyaUid: string
|
huyaUid: string
|
||||||
platformNickname: string
|
platformNickname: string
|
||||||
bindGameAccount: string
|
|
||||||
bindGameRole: string
|
|
||||||
bindGameRoleId: string
|
|
||||||
bindGameChannel: string
|
|
||||||
customerGameRoleId: string
|
|
||||||
customerGameRoleName: string
|
|
||||||
customerServerZone: string
|
|
||||||
completedAt: string | null
|
completedAt: string | null
|
||||||
completionNote: string
|
completionNote: string
|
||||||
semiAutoConfig: {
|
semiAutoConfig: {
|
||||||
@@ -45,18 +28,3 @@ export type CustomerLoginInfo = {
|
|||||||
expiresAt: string
|
expiresAt: string
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomerBindingInfo = {
|
|
||||||
status: string
|
|
||||||
qrImage: string
|
|
||||||
qrExpiresAt: string
|
|
||||||
qrUrl: string
|
|
||||||
bindGameAccount: string
|
|
||||||
bindGameRole: string
|
|
||||||
bindGameRoleId: string
|
|
||||||
bindGameChannel: string
|
|
||||||
hasExistingBinding: boolean
|
|
||||||
rebindRequested: boolean
|
|
||||||
message: string
|
|
||||||
comparison: CustomerBindingComparison
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user