优化电子凭证核销码生成
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user