feat(selfop): add bounded independent quantity diagnostic capture

This commit is contained in:
2026-10-13 23:40:44 +08:00
parent 54f79b3902
commit 3d3507a770
18 changed files with 1190 additions and 1 deletions
@@ -0,0 +1,38 @@
-- Independent diagnostic tables in the Lastleg schema. No business fact/queue writes.
-- Run only after explicit TEST SQL approval. Not executed by service startup.
CREATE TABLE ll_selfop_quantity_diagnostic_session (
diagnostic_session_id VARCHAR(128) NOT NULL COMMENT '固定有界数量诊断会话标识',
configuration_hash CHAR(64) NOT NULL COMMENT '冻结范围、UTC起止时间及限额的SHA256摘要',
source_account VARCHAR(64) NOT NULL COMMENT '来源账号别名,不保存凭据',
collection_session_id VARCHAR(128) NOT NULL COMMENT '被观察的来源采集会话标识',
root_task_id VARCHAR(128) NOT NULL COMMENT '被观察的账号采集根任务标识',
starts_at VARCHAR(40) NOT NULL COMMENT '诊断开始时刻,ISO8601 UTC字符串',
expires_at VARCHAR(40) NOT NULL COMMENT '诊断截止时刻,距开始最多30分钟,ISO8601 UTC字符串',
accepted_orders INT NOT NULL DEFAULT 0 COMMENT '已接收完整配对订单数,跨重启累计',
accepted_sku_rows INT NOT NULL DEFAULT 0 COMMENT '已接收归并SKU行数,跨重启累计',
accepted_bytes BIGINT NOT NULL DEFAULT 0 COMMENT '已持久化白名单规范JSON的UTF8字节数',
stop_reason VARCHAR(64) NULL COMMENT '诊断终止原因,空值表示尚未终止',
PRIMARY KEY (diagnostic_session_id),
CONSTRAINT chk_selfop_qdiag_limits CHECK (accepted_orders BETWEEN 0 AND 100
AND accepted_sku_rows BETWEEN 0 AND 2000 AND accepted_bytes BETWEEN 0 AND 2097152)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='独立数量对照诊断会话,重启不重置限额';
CREATE TABLE ll_selfop_quantity_diagnostic_order (
diagnostic_session_id VARCHAR(128) NOT NULL COMMENT '所属固定诊断会话标识',
order_key_hash CHAR(64) NOT NULL COMMENT '账号、仓库、货主、出库单业务键的SHA256摘要',
source_account VARCHAR(64) NOT NULL COMMENT '来源账号别名',
warehouse_code VARCHAR(64) NOT NULL COMMENT '来源仓库编码',
owner_code VARCHAR(128) NOT NULL COMMENT '来源货主编码',
order_no VARCHAR(128) NOT NULL COMMENT '来源出库单号',
payload_hash CHAR(64) NOT NULL COMMENT '白名单规范载荷SHA256摘要,用于幂等校验',
comparison VARCHAR(40) NOT NULL COMMENT 'SKU集合与三类数量比较结果',
sku_rows INT NOT NULL COMMENT '本单归并后的SKU并集行数',
payload_bytes INT NOT NULL COMMENT '本单白名单规范JSON的UTF8字节数',
evidence_json JSON NOT NULL COMMENT '类型化列表与详情观测及SKU三量证据,不含原始响应或收件信息',
received_at VARCHAR(40) NOT NULL COMMENT '接收时刻,ISO8601 UTC字符串',
PRIMARY KEY (diagnostic_session_id, order_key_hash),
CONSTRAINT fk_selfop_qdiag_session FOREIGN KEY (diagnostic_session_id)
REFERENCES ll_selfop_quantity_diagnostic_session(diagnostic_session_id),
CONSTRAINT chk_selfop_qdiag_order_limits CHECK (sku_rows BETWEEN 1 AND 200
AND payload_bytes BETWEEN 1 AND 262144)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='独立配对订单数量诊断证据,不参与销量发布';
@@ -0,0 +1,105 @@
openapi: 3.0.3
info:
title: Internal bounded self-operated quantity diagnostics
version: 1.0.0
paths:
/internal/lastleg/self-operated-analytics/quantity-diagnostics/orders:
post:
operationId: receiveSelfopQuantityDiagnosticOrder
description: >-
Internal service-token only; never routed through Gateway. One complete paired order per request.
Quantities are evidence only. Session scope and UTC time interval are frozen by server configuration.
Maximum 100 orders, 2000 SKU rows and 2097152 canonical UTF8 bytes per session;
200 SKU rows and 262144 wire/canonical bytes per request; at most 30 minutes.
First known forecast/actual difference closes capture regardless of comparison classification.
Same identity and canonical evidence is idempotent, including after closure; changed evidence is 409.
security:
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PairedOrder'
responses:
'200':
description: Diagnostic receipt in the standard ApiResponse wrapper; code is string 200.
content:
application/json:
schema:
type: object
properties:
code: { type: string, example: '200' }
data: { $ref: '#/components/schemas/Receipt' }
'400': { description: 'Invalid JSON, unknown fields, invalid scope/evidence, duplicate SKU or request limit.' }
'401': { description: Invalid internal service token. }
'409': { description: Session configuration changed or order identity reused with different evidence. }
'503': { description: Internal service token unconfigured or diagnostic storage unavailable. }
components:
securitySchemes:
internalServiceToken:
type: apiKey
in: header
name: X-Internal-Service-Token
schemas:
PairedOrder:
type: object
additionalProperties: false
required: [diagnosticSessionId, collectionSessionId, rootTaskId, sourceAccount, warehouseCode, ownerCode, orderNo, listObservation, detailObservation, skus]
properties:
diagnosticSessionId: { type: string, maxLength: 128 }
collectionSessionId: { type: string, maxLength: 128 }
rootTaskId: { type: string, maxLength: 128 }
sourceAccount: { type: string, maxLength: 64 }
warehouseCode: { type: string, maxLength: 64 }
ownerCode: { type: string, maxLength: 128 }
orderNo: { type: string, maxLength: 128 }
listObservation: { $ref: '#/components/schemas/Observation' }
detailObservation: { $ref: '#/components/schemas/Observation' }
skus:
type: array
minItems: 1
maxItems: 200
description: Union of list/detail SKUs; duplicate source lines aggregated, unknown never zero-filled.
items: { $ref: '#/components/schemas/Sku' }
Observation:
type: object
additionalProperties: false
required: [observedAt, attemptId, skuSetComplete]
properties:
status: { type: string, nullable: true, maxLength: 32 }
outboundTime:
type: string
nullable: true
maxLength: 32
description: Original Asia/Shanghai source text, null if absent; not reinterpreted as another timezone.
observedAt: { type: string, format: date-time }
attemptId: { type: string, maxLength: 128 }
skuSetComplete: { type: boolean }
Sku:
type: object
additionalProperties: false
required: [sku, listPresent, detailPresent]
properties:
sku: { type: string, maxLength: 128 }
listQty: { type: number, nullable: true, minimum: 0, multipleOf: 0.000001 }
forecastQty: { type: number, nullable: true, minimum: 0, multipleOf: 0.000001 }
actualQty: { type: number, nullable: true, minimum: 0, multipleOf: 0.000001 }
listPresent: { type: boolean }
detailPresent: { type: boolean }
Receipt:
type: object
required: [outcome, acceptedOrders, acceptedSkuRows, acceptedBytes]
properties:
outcome: { type: string, enum: [ACCEPTED, DUPLICATE, STOPPED] }
comparison:
type: string
nullable: true
enum: [SKU_SET_MISMATCH, QUANTITY_UNKNOWN, FORECAST_ACTUAL_DIFFERENCE, ALL_EQUAL, LIST_QUANTITY_DIFFERENCE]
stopReason:
type: string
nullable: true
enum: [DISABLED, NOT_STARTED, EXPIRED, LIMIT_REACHED, FORECAST_ACTUAL_DIFFERENCE]
acceptedOrders: { type: integer }
acceptedSkuRows: { type: integer }
acceptedBytes: { type: integer, format: int64, description: Canonical whitelist JSON UTF8 bytes persisted in this session. }