统一付款详情与批次短编号(08-01-reimbursement-notification-progress-clarity)
这个提交包含在:
@@ -514,3 +514,86 @@ const finalSummary = summarizeFinalItemStatuses(finalItemStatuses);
|
||||
batch.execution_summary_json = finalSummary;
|
||||
batch.status = finalSummary.failedCount > 0 ? "failed" : "executed";
|
||||
```
|
||||
|
||||
## Scenario: PostgreSQL Advisory-Lock Namespaces
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: allocating an application-level sequence or serializing a small
|
||||
cross-row critical section that cannot be protected by locking one existing
|
||||
row.
|
||||
- Use this only for PostgreSQL-backed paths whose correctness is also verified
|
||||
against a disposable real database; do not introduce advisory locks for
|
||||
ordinary single-row updates already covered by `FOR UPDATE`.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Shared registry: `server/src/constants/advisoryLocks.js`.
|
||||
- Lock shape: `pg_advisory_xact_lock(namespace_int4, object_key_int4)`.
|
||||
- Registered namespace: `paymentBatchDailySequence = 1001`; its object key is
|
||||
the company-local `YYYYMMDD` integer.
|
||||
- Prisma call for a void-returning lock statement: `tx.$executeRaw`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Register every new first-argument namespace in the shared constant file;
|
||||
never reuse an existing namespace for an unrelated critical section.
|
||||
- Use the two-`int4` form with an explicit, inspectable object key. Do not use
|
||||
`hashtext` or an undocumented magic pair at the call site.
|
||||
- Acquire the transaction-level lock before reading the protected maximum or
|
||||
current state. Lock acquisition, protected read and write must stay in the
|
||||
same Prisma interactive transaction so commit or rollback releases the lock.
|
||||
- Keep one stable lock order when a transaction needs more than one lock.
|
||||
- A PostgreSQL uniqueness error aborts the transaction. Do not catch `P2002`
|
||||
and retry inside the same interactive transaction; any fallback must retry
|
||||
the complete transaction from the beginning.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- `$queryRaw` reads `SELECT pg_advisory_xact_lock(...)` -> Prisma driver may
|
||||
fail with `P2010` because PostgreSQL returns a `void` column.
|
||||
- `$executeRaw` executes the same `SELECT` -> lock is acquired without result
|
||||
deserialization; continue to the protected query.
|
||||
- Namespace or object key outside signed `int4` -> fail before business writes;
|
||||
correct the registry/key design rather than silently casting or hashing.
|
||||
- Protected daily sequence exhausted -> stable `409 VALID_001` and rollback;
|
||||
do not widen or randomize the public identifier format at the boundary.
|
||||
- Concurrent callers with the same pair -> one waits, then reads the committed
|
||||
maximum and allocates the next value.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: two simultaneous payment-batch creations lock `(1001, YYYYMMDD)`, then
|
||||
commit distinct consecutive identifiers.
|
||||
- Base: different company-local dates use different object keys and do not
|
||||
contend.
|
||||
- Bad: query the maximum before taking the lock, use a process mutex, or retry a
|
||||
unique violation inside the already-aborted transaction.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Unit test the exact call order and error boundary with mocked
|
||||
`$executeRaw`/`$queryRaw`; assert no create, item or audit write on exhaustion.
|
||||
- Run `bash scripts/qa/run-web-gates.sh payment-concurrency-local` against a
|
||||
disposable PostgreSQL database. The daily-sequence race must create two
|
||||
different sources concurrently and assert two consecutive unique numbers.
|
||||
- Run `node --check` on the route, namespace registry and concurrency script,
|
||||
plus Prisma validate and a read-only review for affected payment paths.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```js
|
||||
await tx.$queryRaw`SELECT pg_advisory_xact_lock(${namespace}, ${dateKey})`;
|
||||
const latest = await readLatestSequence(tx);
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
await tx.$executeRaw`
|
||||
SELECT pg_advisory_xact_lock(${namespace}::int, ${dateKey}::int)
|
||||
`;
|
||||
const latest = await readLatestSequence(tx);
|
||||
```
|
||||
|
||||
在新工单中引用
屏蔽一个用户