增加人工渠道

This commit is contained in:
yml2213
2026-04-23 11:59:57 +08:00
parent 871d99cc93
commit 512b58856f
16 changed files with 2206 additions and 2 deletions
@@ -0,0 +1,376 @@
// @ts-check
import { createTaskClaimToken } from '../claim/claim-service.js'
import {
closeClaimSessionForAdminTask,
createClaimSessionForAdminTask,
getClaimDetailForAdminTask,
getClaimSessionSummaryForAdminTask,
reloadClaimSessionForAdminTask,
} from '../claim/claim-session-service.js'
import { reserveInventoryForTask } from '../order/inventory-service.js'
import { createHttpError } from '../../utils/http.js'
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
import { createOrder, findOrderByPlatformOrderId } from '../../repositories/order-repo.js'
import { getFulfillmentProfileByKey, listFulfillmentProfileRequirements } from '../../repositories/fulfillment-profile-repo.js'
import { findFirstAvailableInventoryItemBySkuCode } from '../../repositories/inventory-repo.js'
import { createTask, getTaskById, updateTask } from '../../repositories/task-repo.js'
import { createTaskEvent } from '../../repositories/task-event-repo.js'
import { nowIso } from '../../utils/time.js'
import { randomId } from '../../utils/random.js'
import { closeAdminTask, confirmAdminTaskAssistedRole, redeemAdminTaskAssisted } from './admin-write-service.js'
import { isAssistedClaimTask, parseTaskContext } from './admin-read-shared-helpers.js'
const MANUAL_PROVIDER = 'manual'
const MANUAL_PLATFORM = 'manual_redeem'
const MANUAL_SOURCE_TYPE = 'admin_manual_redeem'
const ASSISTED_PROFILE_KEY = 'tencent_claim_assisted'
/** @typedef {import('../../types/admin-read-inputs.js').AdminEntityIdInput} AdminEntityIdInput */
/** @typedef {import('../../types/admin-read-inputs.js').AdminViewerSessionInput} AdminViewerSessionInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminManualRedeemCreateInput} AdminManualRedeemCreateInput */
/** @param {AdminManualRedeemCreateInput} [payload] */
/** @param {AdminViewerSessionInput | null} [session] */
export async function createAdminManualRedeemTask(
payload = /** @type {AdminManualRedeemCreateInput} */ ({}),
session = null,
) {
const proofValue = normalizeManualProofValue(payload.proofValue)
const skuCode = String(payload.skuCode || '').trim()
const skuName = String(payload.skuName || '').trim() || skuCode
const remark = String(payload.remark || '').trim()
if (!proofValue) {
throw createHttpError('请先输入唯一凭据', {
statusCode: 400,
errorCode: 'admin_manual_redeem_missing_proof',
})
}
if (!skuCode) {
throw createHttpError('请先选择内部履约 SKU', {
statusCode: 400,
errorCode: 'admin_manual_redeem_missing_sku',
})
}
const existingOrder = await findOrderByPlatformOrderId({
provider: MANUAL_PROVIDER,
platform: MANUAL_PLATFORM,
shopId: '',
platformOrderId: proofValue,
})
if (existingOrder) {
throw createHttpError('该唯一凭据已经创建过人工兑换任务,请勿重复提交', {
statusCode: 409,
errorCode: 'admin_manual_redeem_duplicate_proof',
})
}
const profile = await getFulfillmentProfileByKey(ASSISTED_PROFILE_KEY)
if (!profile) {
throw createHttpError('人工兑换所需的履约档案不存在,请先检查系统初始化', {
statusCode: 500,
errorCode: 'admin_manual_redeem_profile_missing',
})
}
const requirements = await listFulfillmentProfileRequirements(profile.id)
const primaryRequirement = requirements.find((item) => item.is_required !== false) || requirements[0] || null
const credentialType = String(primaryRequirement?.credential_type || primaryRequirement?.credentialType || 'tencent_code').trim() || 'tencent_code'
const roleKey = String(primaryRequirement?.role_key || primaryRequirement?.roleKey || 'primary_code').trim() || 'primary_code'
const availableInventory = await findFirstAvailableInventoryItemBySkuCode(skuCode, credentialType)
if (!availableInventory) {
throw createHttpError('当前 SKU 没有可用库存,无法创建人工兑换任务', {
statusCode: 409,
errorCode: 'admin_manual_redeem_inventory_unavailable',
})
}
const now = nowIso()
const manualContext = {
sourceType: MANUAL_SOURCE_TYPE,
proofValue,
remark,
createdAt: now,
createdBy: session
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
role: String(session.role || '').trim(),
}
: null,
}
let order
try {
order = await createOrder({
provider: MANUAL_PROVIDER,
platform: MANUAL_PLATFORM,
shopId: '',
shopName: '人工兑换',
platformOrderId: proofValue,
orderStatus: 'manual_created',
payStatus: 'paid',
buyerId: '',
buyerName: '',
receiverContact: '',
totalAmount: 0,
currency: 'CNY',
rawPayloadJson: JSON.stringify({
manualRedeem: manualContext,
}),
paidAt: now,
createdAt: now,
updatedAt: now,
})
} catch (error) {
if (String(error?.code || '') === '23505') {
throw createHttpError('该唯一凭据已经创建过人工兑换任务,请勿重复提交', {
statusCode: 409,
errorCode: 'admin_manual_redeem_duplicate_proof',
})
}
throw error
}
if (!order) {
throw createHttpError('人工兑换订单创建失败', {
statusCode: 500,
errorCode: 'admin_manual_redeem_order_create_failed',
})
}
const orderItems = await replaceOrderItems(order.id, [
{
skuCode,
skuName,
quantity: 1,
specJson: JSON.stringify({
title: skuName,
sourceType: MANUAL_SOURCE_TYPE,
}),
itemSnapshotJson: JSON.stringify({
manualRedeem: manualContext,
skuCode,
skuName,
}),
createdAt: now,
updatedAt: now,
},
])
const orderItem = orderItems[0]
if (!orderItem) {
throw createHttpError('人工兑换订单商品创建失败', {
statusCode: 500,
errorCode: 'admin_manual_redeem_order_item_create_failed',
})
}
const createdTask = await createTask({
orderId: order.id,
orderItemId: orderItem.id,
unitIndex: 1,
provider: MANUAL_PROVIDER,
platform: MANUAL_PLATFORM,
shopId: '',
shopName: '人工兑换',
platformOrderId: proofValue,
taskNo: randomId('MR'),
profileId: Number(profile.id),
executorKey: String(profile.executor_key || ASSISTED_PROFILE_KEY),
taskStatus: 'paid',
inventoryStatus: 'pending',
deliveryStatus: 'pending',
resultCode: '',
resultMessage: '',
claimToken: '',
claimExpiresAt: null,
automationMode: 'manual',
requiresClaim: true,
userActionStatus: 'pending_claim',
attemptCount: 0,
lastError: '',
contextJson: JSON.stringify({
profileKey: String(profile.profile_key || ASSISTED_PROFILE_KEY),
profileName: String(profile.name || ''),
skuCode,
skuName,
inventorySkuCode: skuCode,
primaryRequirement: primaryRequirement
? {
roleKey,
credentialType,
}
: null,
manualRedeem: manualContext,
}),
createdAt: now,
updatedAt: now,
})
if (!createdTask) {
throw createHttpError('人工兑换任务创建失败', {
statusCode: 500,
errorCode: 'admin_manual_redeem_task_create_failed',
})
}
const reservedInventory = await reserveInventoryForTask({
skuCode,
taskId: createdTask.id,
credentialType,
roleKey,
})
if (!reservedInventory) {
await updateTask(createdTask.id, {
task_status: 'waiting_inventory',
inventory_status: 'pending',
last_error: '库存不足,无法为人工兑换任务预占库存',
updated_at: nowIso(),
})
throw createHttpError('库存刚刚被其他任务占用,请重新选择或稍后再试', {
statusCode: 409,
errorCode: 'admin_manual_redeem_inventory_race_lost',
})
}
const claimToken = await createTaskClaimToken(createdTask.id)
await updateTask(createdTask.id, {
task_status: 'link_generated',
inventory_status: 'reserved',
claim_token: claimToken.token,
claim_expires_at: claimToken.expired_at,
user_action_status: 'pending_claim',
last_error: '',
updated_at: nowIso(),
})
await createTaskEvent(createdTask.id, 'manual_redeem_created', {
sourceType: MANUAL_SOURCE_TYPE,
proofValue,
skuCode,
skuName,
inventoryItemId: Number(reservedInventory.id || 0) || null,
createdBy: manualContext.createdBy,
remark,
}, nowIso())
return getAdminManualRedeemDetail(createdTask.id)
}
/** @param {AdminEntityIdInput} taskId */
export async function getAdminManualRedeemDetail(taskId) {
const task = await getRequiredManualRedeemTask(taskId)
const detail = await getClaimDetailForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {{ loginType?: string, forceRecreate?: boolean }} [payload] */
export async function createAdminManualRedeemSession(taskId, payload = {}) {
const task = await getRequiredManualRedeemTask(taskId)
const detail = await createClaimSessionForAdminTask(task.id, payload)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
export async function getAdminManualRedeemSessionSummary(taskId) {
const task = await getRequiredManualRedeemTask(taskId)
const detail = await getClaimSessionSummaryForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
export async function reloadAdminManualRedeemSession(taskId) {
const task = await getRequiredManualRedeemTask(taskId)
const detail = await reloadClaimSessionForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
export async function closeAdminManualRedeemSession(taskId) {
const task = await getRequiredManualRedeemTask(taskId)
const detail = await closeClaimSessionForAdminTask(task.id)
return decorateManualRedeemDetail(task, detail)
}
/** @param {AdminEntityIdInput} taskId */
export async function closeAdminManualRedeemTask(taskId) {
const task = await getRequiredManualRedeemTask(taskId)
await closeAdminTask(task.id)
return getAdminManualRedeemDetail(task.id)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function confirmAdminManualRedeemRole(taskId, session = null) {
const task = await getRequiredManualRedeemTask(taskId)
await confirmAdminTaskAssistedRole(task.id, session)
return getAdminManualRedeemDetail(task.id)
}
/** @param {AdminEntityIdInput} taskId */
/** @param {AdminViewerSessionInput | null} [session] */
export async function redeemAdminManualRedeemTask(taskId, session = null) {
const task = await getRequiredManualRedeemTask(taskId)
await redeemAdminTaskAssisted(task.id, session)
return getAdminManualRedeemDetail(task.id)
}
async function getRequiredManualRedeemTask(taskId) {
const task = await getTaskById(Number(taskId))
if (!task) {
throw createHttpError('人工兑换任务不存在', {
statusCode: 404,
errorCode: 'admin_manual_redeem_task_not_found',
})
}
const taskContext = parseTaskContext(task)
const sourceType = String(taskContext?.manualRedeem?.sourceType || '').trim()
if (!isAssistedClaimTask(task) || sourceType !== MANUAL_SOURCE_TYPE) {
throw createHttpError('当前任务不是人工兑换任务', {
statusCode: 409,
errorCode: 'admin_manual_redeem_task_invalid',
})
}
return task
}
function decorateManualRedeemDetail(task, detail) {
const taskContext = parseTaskContext(task)
const manualRedeem = taskContext.manualRedeem && typeof taskContext.manualRedeem === 'object'
? taskContext.manualRedeem
: {}
return {
...detail,
claimUrl: '',
manualRequest: {
sourceType: String(manualRedeem.sourceType || MANUAL_SOURCE_TYPE).trim() || MANUAL_SOURCE_TYPE,
proofValue: String(manualRedeem.proofValue || detail?.order?.platformOrderId || '').trim(),
remark: String(manualRedeem.remark || '').trim(),
},
result: detail.result
? {
...detail.result,
screenshotUrl: detail.result.screenshotReady ? `/api/v1/admin/tasks/${detail.task.taskId}/screenshot` : '',
}
: null,
}
}
function normalizeManualProofValue(value) {
return String(value || '').replace(/\s+/g, ' ').trim()
}