优化电子凭证核销码生成

This commit is contained in:
yml2213
2026-07-07 16:36:07 +08:00
parent 59f6f5ee1e
commit 2471df0086
2 changed files with 122 additions and 29 deletions
@@ -1,3 +1,5 @@
import crypto from 'node:crypto'
import { query } from '../db/client.js'
import type {
KuaishouIndustryVoucherUpdatePatch,
@@ -11,16 +13,42 @@ type VoucherPatchColumn = {
cast?: string
}
const VOUCHER_CODE_PREFIX = 'KSV'
const VOUCHER_CODE_RANDOM_BYTES = 10
const VOUCHER_CODE_MAX_GENERATE_ATTEMPTS = 5
const CROCKFORD_BASE32_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
export async function upsertKuaishouIndustryVoucher(
input: KuaishouIndustryVoucherUpsertInput,
): Promise<KuaishouIndustryVoucherRow | null> {
for (let attempt = 1; attempt <= VOUCHER_CODE_MAX_GENERATE_ATTEMPTS; attempt += 1) {
try {
return await insertKuaishouIndustryVoucher(input, generateKuaishouIndustryVoucherCode())
} catch (error) {
if (attempt < VOUCHER_CODE_MAX_GENERATE_ATTEMPTS && isVoucherCodeUniqueViolation(error)) {
continue
}
throw error
}
}
return null
}
export function generateKuaishouIndustryVoucherCode(
randomBytes: (size: number) => Uint8Array = crypto.randomBytes,
): string {
return `${VOUCHER_CODE_PREFIX}${encodeCrockfordBase32(randomBytes(VOUCHER_CODE_RANDOM_BYTES))}`
}
async function insertKuaishouIndustryVoucher(
input: KuaishouIndustryVoucherUpsertInput,
voucherCode: string,
): Promise<KuaishouIndustryVoucherRow | null> {
const result = await query<KuaishouIndustryVoucherRow>(
`
WITH next_id AS (
SELECT nextval(pg_get_serial_sequence('kuaishou_industry_vouchers', 'id')) AS id
)
INSERT INTO kuaishou_industry_vouchers (
id,
voucher_code,
oid,
order_id,
@@ -38,9 +66,7 @@ export async function upsertKuaishouIndustryVoucher(
created_at,
updated_at
)
SELECT
next_id.id,
next_id.id::text,
VALUES (
$1,
$2,
$3,
@@ -50,13 +76,14 @@ export async function upsertKuaishouIndustryVoucher(
$7,
$8,
$9,
$10::jsonb,
$11,
$10,
$11::jsonb,
$12,
$13::jsonb,
$14,
$15
FROM next_id
$13,
$14::jsonb,
$15,
$16
)
ON CONFLICT (oid, unit_index) DO UPDATE
SET
token = CASE
@@ -72,6 +99,7 @@ export async function upsertKuaishouIndustryVoucher(
RETURNING *
`,
[
voucherCode,
input.oid,
normalizeNullableId(input.orderId),
normalizeNullableId(input.taskId),
@@ -135,7 +163,8 @@ export async function findKuaishouIndustryVoucherByCode(
return null
}
const params: unknown[] = [normalizedCode]
const candidateCodes = Array.from(new Set([normalizedCode, normalizedCode.toUpperCase()]))
const params: unknown[] = [candidateCodes]
const oidFilter = normalizedOid ? 'AND oid = $2' : ''
if (normalizedOid) {
params.push(normalizedOid)
@@ -145,7 +174,7 @@ export async function findKuaishouIndustryVoucherByCode(
`
SELECT *
FROM kuaishou_industry_vouchers
WHERE voucher_code = $1
WHERE voucher_code = ANY($1::text[])
${oidFilter}
LIMIT 1
`,
@@ -252,6 +281,42 @@ function normalizeVoucherPatchColumns(
return columns
}
function encodeCrockfordBase32(bytes: Uint8Array): string {
let bits = 0
let value = 0
let output = ''
for (const byte of bytes) {
value = (value << 8) | byte
bits += 8
while (bits >= 5) {
output += CROCKFORD_BASE32_ALPHABET[(value >>> (bits - 5)) & 31] || ''
bits -= 5
}
}
if (bits > 0) {
output += CROCKFORD_BASE32_ALPHABET[(value << (5 - bits)) & 31] || ''
}
return output
}
function isVoucherCodeUniqueViolation(error: unknown): boolean {
const current =
error && typeof error === 'object'
? (error as { code?: unknown; constraint?: unknown; detail?: unknown })
: {}
if (String(current.code || '') !== '23505') {
return false
}
const marker = `${String(current.constraint || '')} ${String(current.detail || '')}`
return marker.includes('voucher_code')
}
function normalizeNullableId(value: unknown): number | null {
if (value === null || value === undefined || value === '') {
return null
@@ -1,30 +1,58 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { generateKuaishouIndustryVoucherCode } from '../../../repositories/kuaishou-industry-voucher-repo.js'
import { resolveKuaishouIndustryVoucherValidity } from './voucher-service.js'
test('generateKuaishouIndustryVoucherCode creates non-numeric stable voucher code', () => {
let requestedSize = 0
const code = generateKuaishouIndustryVoucherCode((size) => {
requestedSize = size
return Buffer.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
})
assert.equal(requestedSize, 10)
assert.equal(code.length, 19)
assert.match(code, /^KSV[0-9ABCDEFGHJKMNPQRSTVWXYZ]{16}$/)
assert.doesNotMatch(code, /^\d+$/)
assert.doesNotMatch(code, /[ILOU]/)
})
test('generateKuaishouIndustryVoucherCode changes with random bytes', () => {
const first = generateKuaishouIndustryVoucherCode(() => Buffer.alloc(10, 0))
const second = generateKuaishouIndustryVoucherCode(() => Buffer.alloc(10, 1))
assert.notEqual(first, second)
})
test('resolveKuaishouIndustryVoucherValidity uses certExpDays when explicit times are zero', () => {
const nowMs = 1783394218325
const validity = resolveKuaishouIndustryVoucherValidity({
certActualStartTime: 0,
certActualEndTime: 0,
certStartTime: 0,
certEndTime: 0,
certExpDays: 3,
}, nowMs)
const validity = resolveKuaishouIndustryVoucherValidity(
{
certActualStartTime: 0,
certActualEndTime: 0,
certStartTime: 0,
certEndTime: 0,
certExpDays: 3,
},
nowMs,
)
assert.equal(validity.validStartTime, nowMs)
assert.equal(validity.validEndTime, nowMs + 3 * 86_400_000)
})
test('resolveKuaishouIndustryVoucherValidity prefers actual certificate time range', () => {
const validity = resolveKuaishouIndustryVoucherValidity({
certActualStartTime: 1000,
certActualEndTime: 5000,
certStartTime: 2000,
certEndTime: 6000,
certExpDays: 3,
}, 9000)
const validity = resolveKuaishouIndustryVoucherValidity(
{
certActualStartTime: 1000,
certActualEndTime: 5000,
certStartTime: 2000,
certEndTime: 6000,
certExpDays: 3,
},
9000,
)
assert.equal(validity.validStartTime, 1000)
assert.equal(validity.validEndTime, 5000)