diff --git a/apps/backend/src/services/admin/admin-audit-service.js b/apps/backend/src/services/admin/admin-audit-service.js index 71371f2f..965e320e 100644 --- a/apps/backend/src/services/admin/admin-audit-service.js +++ b/apps/backend/src/services/admin/admin-audit-service.js @@ -1,7 +1,7 @@ import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js' import { nowIso } from '../../utils/time.js' -export function writeAdminAuditLog(session, payload = {}) { +export async function writeAdminAuditLog(session, payload = {}) { if (!session?.userId) { return null } @@ -18,10 +18,10 @@ export function writeAdminAuditLog(session, payload = {}) { }) } -export function getAdminAuditLogs(query = {}) { +export async function getAdminAuditLogs(query = {}) { const page = normalizePage(query.page) const pageSize = normalizePageSize(query.pageSize) - const { items, total } = listAdminAuditLogs({ + const { items, total } = await listAdminAuditLogs({ page, pageSize, actorUsername: String(query.actorUsername || '').trim(), @@ -33,8 +33,8 @@ export function getAdminAuditLogs(query = {}) { return { items: items.map((item) => ({ - logId: item.id, - actorUserId: item.actor_user_id, + logId: Number(item.id), + actorUserId: Number(item.actor_user_id || 0), actorUsername: item.actor_username, actorRole: item.actor_role, action: item.action, @@ -76,7 +76,7 @@ function normalizeDateQuery(rawValue, endOfDay = false) { function safeParseJson(value) { try { - return JSON.parse(String(value || '{}')) + return typeof value === 'string' ? JSON.parse(value) : value || {} } catch { return {} } diff --git a/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.js b/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.js new file mode 100644 index 00000000..0b7f3e3b --- /dev/null +++ b/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.js @@ -0,0 +1,126 @@ +import { runtimeConfig } from '../../config/runtime.js' +import { + getFulfillmentProfileByKey, + listFulfillmentProfileRequirements, + replaceFulfillmentProfileRequirements, + upsertFulfillmentProfile, + upsertSkuFulfillmentBinding, +} from '../../repositories/fulfillment-profile-repo.js' +import { nowIso } from '../../utils/time.js' + +const CORE_PROFILES = [ + { + profileKey: 'manual_review', + name: '人工发货', + executorKey: 'manual_dispatch', + requiresClaim: false, + autoDispatch: false, + inventoryStrategy: 'static_pool', + requirements: [], + }, + { + profileKey: 'tencent_claim_redeem', + name: '腾讯领取兑换', + executorKey: 'tencent_claim_redeem', + requiresClaim: true, + autoDispatch: false, + inventoryStrategy: 'static_pool', + requirements: [ + { + roleKey: 'primary_code', + credentialType: 'tencent_code', + quantityPerUnit: 1, + isRequired: true, + }, + ], + }, +] + +export async function ensureFulfillmentCatalogBootstrapped() { + const timestamp = nowIso() + const profileMap = {} + + for (const profile of CORE_PROFILES) { + const saved = await upsertFulfillmentProfile({ + profileKey: profile.profileKey, + name: profile.name, + executorKey: profile.executorKey, + requiresClaim: profile.requiresClaim, + autoDispatch: profile.autoDispatch, + inventoryStrategy: profile.inventoryStrategy, + configJson: '{}', + createdAt: timestamp, + updatedAt: timestamp, + }) + + if (!saved) { + continue + } + + profileMap[profile.profileKey] = saved + const currentRequirements = await listFulfillmentProfileRequirements(saved.id) + if (currentRequirements.length !== profile.requirements.length) { + await replaceFulfillmentProfileRequirements(saved.id, profile.requirements, timestamp) + } + } + + const configuredBindings = normalizeConfiguredBindings(runtimeConfig.orders?.fulfillmentBindings) + const bindingsToApply = configuredBindings.length > 0 + ? configuredBindings + : inferDefaultBindingsFromSkuMappings(runtimeConfig.orders?.skuMappings) + + for (const binding of bindingsToApply) { + const profile = profileMap[binding.profileKey] || await getFulfillmentProfileByKey(binding.profileKey) + if (!profile) { + continue + } + + await upsertSkuFulfillmentBinding({ + skuCode: binding.skuCode, + provider: binding.provider, + platform: binding.platform, + shopId: binding.shopId, + profileId: profile.id, + enabled: binding.enabled, + priority: binding.priority, + configJson: JSON.stringify(binding.config || {}), + createdAt: timestamp, + updatedAt: timestamp, + }) + } +} + +function normalizeConfiguredBindings(bindings) { + if (!Array.isArray(bindings)) { + return [] + } + + return bindings + .map((binding) => ({ + skuCode: String(binding?.skuCode || '').trim(), + provider: String(binding?.provider || '').trim(), + platform: String(binding?.platform || '').trim(), + shopId: String(binding?.shopId || '').trim(), + profileKey: String(binding?.profileKey || '').trim() || 'manual_review', + enabled: binding?.enabled !== false, + priority: Number(binding?.priority || 100), + config: binding?.config || {}, + })) + .filter((binding) => binding.skuCode) +} + +function inferDefaultBindingsFromSkuMappings(skuMappings) { + const values = Object.values(skuMappings || {}) + const uniqueSkuCodes = [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))] + + return uniqueSkuCodes.map((skuCode) => ({ + skuCode, + provider: '', + platform: '', + shopId: '', + profileKey: 'tencent_claim_redeem', + enabled: true, + priority: 100, + config: {}, + })) +} diff --git a/apps/backend/src/services/claim/claim-service.js b/apps/backend/src/services/claim/claim-service.js index 517e02fb..0882fdc6 100644 --- a/apps/backend/src/services/claim/claim-service.js +++ b/apps/backend/src/services/claim/claim-service.js @@ -3,10 +3,10 @@ import { createClaimToken } from '../../repositories/claim-token-repo.js' import { addHours, nowIso } from '../../utils/time.js' import { randomToken } from '../../utils/random.js' -export function createTaskClaimToken(taskId) { +export async function createTaskClaimToken(taskId) { const createdAt = nowIso() const expiredAt = addHours(createdAt, Number(runtimeConfig.orders.tokenTtlHours || 24)) - const token = createClaimToken({ + const token = await createClaimToken({ taskId, token: randomToken(24), status: 'active', diff --git a/apps/backend/src/services/order/cdk-service.js b/apps/backend/src/services/order/cdk-service.js index 05ae33a9..fec2ba32 100644 --- a/apps/backend/src/services/order/cdk-service.js +++ b/apps/backend/src/services/order/cdk-service.js @@ -1,16 +1,33 @@ -import { assignReservedCdk, findFirstAvailableCdkBySkuCode } from '../../repositories/cdk-repo.js' +import { assignReservedCdk, findFirstAvailableCdkBySkuCode, releaseReservedCdk } from '../../repositories/cdk-repo.js' import { nowIso } from '../../utils/time.js' -export function reserveCdkForTask(skuCode, taskId) { +export async function reserveCdkForTask({ skuCode, taskId, credentialType = 'tencent_code', roleKey = 'primary_code' }) { if (!skuCode) { return null } - const available = findFirstAvailableCdkBySkuCode(skuCode) + const available = await findFirstAvailableCdkBySkuCode(skuCode, credentialType) if (!available) { return null } - return assignReservedCdk(available.id, taskId, nowIso()) + return assignReservedCdk(available.id, taskId, nowIso(), roleKey) +} + +export async function releaseReservedCdks(cdkIds = [], updatedAt = nowIso()) { + const released = [] + + for (const cdkId of cdkIds) { + if (!cdkId) { + continue + } + + const item = await releaseReservedCdk(cdkId, updatedAt) + if (item) { + released.push(item) + } + } + + return released }