fix(firstleg): scope loading evidence to cabinet tasks

This commit is contained in:
2026-08-31 18:14:36 +08:00
parent 4795ab958c
commit df0fbc1c7f
31 changed files with 695 additions and 1314 deletions
@@ -1,97 +1,40 @@
SET NAMES utf8mb4;
USE baiyue_firstleg;
/* 装柜证据、PDA 拣货复核和派送完成。五类证据按有效记录计数,同类型允许多图追加;作废记录保留审计。 */
CREATE TABLE IF NOT EXISTS fl_loading_evidence (
id BIGINT UNSIGNED NOT NULL COMMENT '装柜证据记录ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID',
evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1空仓/PHOTO_STEP_2半仓/PHOTO_STEP_3满仓/PHOTO_STEP_4锁仓/SEAL带锁码照',
file_id BIGINT UNSIGNED NOT NULL COMMENT 'File 服务逻辑文件ID',
file_name VARCHAR(255) NULL COMMENT '上传时文件名称快照',
seal_no VARCHAR(64) NULL COMMENT '锁码文本;带锁码照必填,客户不可见',
remark VARCHAR(1000) NULL COMMENT '证据补充说明',
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID已作废;错误照片保留记录不硬删除',
void_reason VARCHAR(500) NULL COMMENT '照片作废原因',
voided_by BIGINT UNSIGNED NULL COMMENT '执行作废的平台操作人ID',
voided_by_name VARCHAR(64) NULL COMMENT '执行作废的平台操作人姓名快照',
voided_at DATETIME(3) NULL COMMENT '照片作废时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增',
created_by BIGINT UNSIGNED NOT NULL COMMENT '上传证据的操作人ID',
created_by_name VARCHAR(64) NULL COMMENT '上传证据的操作人姓名快照',
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID',
updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '证据记录创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '证据记录最后更新时间',
CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence (
id BIGINT UNSIGNED NOT NULL,
container_booking_id BIGINT UNSIGNED NOT NULL,
source_task_id BIGINT UNSIGNED NOT NULL,
evidence_type VARCHAR(32) NOT NULL,
file_id BIGINT UNSIGNED NOT NULL,
file_name VARCHAR(255) NOT NULL,
seal_no VARCHAR(64) NULL,
remark VARCHAR(1000) NULL,
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
active_flag TINYINT UNSIGNED NULL DEFAULT 1,
operation_key VARCHAR(128) NOT NULL,
request_hash CHAR(64) NOT NULL,
void_reason VARCHAR(500) NULL,
voided_by BIGINT UNSIGNED NULL,
voided_by_name VARCHAR(128) NULL,
voided_at DATETIME(3) NULL,
version INT NOT NULL DEFAULT 0,
created_by BIGINT UNSIGNED NOT NULL,
created_by_name VARCHAR(128) NULL,
updated_by BIGINT UNSIGNED NOT NULL,
updated_by_name VARCHAR(128) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status),
KEY idx_loading_evidence_seal (tenant_id, seal_no, status),
KEY idx_loading_evidence_time (tenant_id, inbound_id, updated_at),
CONSTRAINT fk_loading_evidence_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
) ENGINE=InnoDB COMMENT='订单装柜空仓/半仓/满仓/锁仓/带锁码照证据';
/* Repeatable migration for installations that already created the single-slot table. */
DROP PROCEDURE IF EXISTS phase3_loading_add_column;
DELIMITER $$
CREATE PROCEDURE phase3_loading_add_column(IN p_column VARCHAR(64), IN p_definition VARCHAR(512))
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND column_name=p_column
) THEN
SET @phase3_loading_ddl = CONCAT('ALTER TABLE fl_loading_evidence ADD COLUMN `', p_column, '` ', p_definition);
PREPARE phase3_loading_stmt FROM @phase3_loading_ddl;
EXECUTE phase3_loading_stmt;
DEALLOCATE PREPARE phase3_loading_stmt;
END IF;
END$$
DELIMITER ;
CALL phase3_loading_add_column('status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''证据状态:ACTIVE有效、VOID已作废;错误照片保留记录不硬删除''');
CALL phase3_loading_add_column('void_reason', 'VARCHAR(500) NULL COMMENT ''照片作废原因''');
CALL phase3_loading_add_column('voided_by', 'BIGINT UNSIGNED NULL COMMENT ''执行作废的平台操作人ID''');
CALL phase3_loading_add_column('voided_by_name', 'VARCHAR(64) NULL COMMENT ''执行作废的平台操作人姓名快照''');
CALL phase3_loading_add_column('voided_at', 'DATETIME(3) NULL COMMENT ''照片作废时间''');
DROP PROCEDURE IF EXISTS phase3_loading_add_column;
SET @phase3_loading_has_legacy_type_index = (
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND index_name='uk_loading_evidence_type'
);
SET @phase3_loading_drop_legacy_type_index = IF(@phase3_loading_has_legacy_type_index > 0,
'ALTER TABLE fl_loading_evidence DROP INDEX uk_loading_evidence_type', 'SELECT 1');
PREPARE phase3_loading_drop_stmt FROM @phase3_loading_drop_legacy_type_index;
EXECUTE phase3_loading_drop_stmt;
DEALLOCATE PREPARE phase3_loading_drop_stmt;
SET @phase3_loading_has_type_index = (
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND index_name='idx_loading_evidence_type'
);
SET @phase3_loading_add_type_index = IF(@phase3_loading_has_type_index = 0,
'ALTER TABLE fl_loading_evidence ADD KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status)', 'SELECT 1');
PREPARE phase3_loading_type_stmt FROM @phase3_loading_add_type_index;
EXECUTE phase3_loading_type_stmt;
DEALLOCATE PREPARE phase3_loading_type_stmt;
SET @phase3_loading_has_legacy_seal_index = (
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND index_name='uk_loading_evidence_seal'
);
SET @phase3_loading_drop_legacy_seal_index = IF(@phase3_loading_has_legacy_seal_index > 0,
'ALTER TABLE fl_loading_evidence DROP INDEX uk_loading_evidence_seal', 'SELECT 1');
PREPARE phase3_loading_drop_seal_stmt FROM @phase3_loading_drop_legacy_seal_index;
EXECUTE phase3_loading_drop_seal_stmt;
DEALLOCATE PREPARE phase3_loading_drop_seal_stmt;
SET @phase3_loading_has_seal_index = (
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema=DATABASE() AND table_name='fl_loading_evidence' AND index_name='idx_loading_evidence_seal'
);
SET @phase3_loading_add_seal_index = IF(@phase3_loading_has_seal_index = 0,
'ALTER TABLE fl_loading_evidence ADD KEY idx_loading_evidence_seal (tenant_id, seal_no, status)', 'SELECT 1');
PREPARE phase3_loading_seal_stmt FROM @phase3_loading_add_seal_index;
EXECUTE phase3_loading_seal_stmt;
DEALLOCATE PREPARE phase3_loading_seal_stmt;
UNIQUE KEY uk_cabinet_evidence_active (container_booking_id,evidence_type,active_flag),
UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key),
KEY idx_cabinet_evidence_container (container_booking_id,status),
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
CONSTRAINT fk_cabinet_evidence_booking FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking(id),
CONSTRAINT fk_cabinet_evidence_task FOREIGN KEY (source_task_id) REFERENCES fl_loading_pick_task(id),
CONSTRAINT chk_cabinet_evidence_type CHECK (evidence_type IN ('PHOTO_STEP_1','PHOTO_STEP_2','PHOTO_STEP_3','PHOTO_STEP_4','SEAL')),
CONSTRAINT chk_cabinet_evidence_status CHECK (status IN ('ACTIVE','VOID'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='柜次物理装柜证据;task仅记录来源审计';
CREATE TABLE IF NOT EXISTS fl_loading_operation (
id BIGINT UNSIGNED NOT NULL COMMENT '装柜写操作记录ID',
@@ -1,67 +1,8 @@
openapi: 3.0.3
info:
title: Phase 3 Loading Evidence and Picking API
title: Phase 3 Loading and Picking API
version: 1.0.0
paths:
/api/platform/v1/inbounds/{inboundId}/loading/evidence/pda:
get:
summary: List loading evidence for the active PDA worker
description: Requires the associated inbound to have entered the loading stage. PDA workflowStatus AWAITING_REVIEW does not bypass this gate; canonical claim or picking advances WAIT_SPLIT to LOADING.
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}]
responses:
'200': {description: Business envelope; requires platform:firstleg:pda:work}
post:
summary: Append one PDA-captured loading evidence photo
description: Requires the associated inbound process status to be LOADING. PDA workflowStatus AWAITING_REVIEW is a review projection and does not replace the inbound loading-stage gate.
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody:
required: true
content:
application/json: {schema: {$ref: '#/components/schemas/UpsertEvidenceRequest'}}
responses:
'200': {description: Business envelope; requires platform:firstleg:pda:work}
/api/platform/v1/inbounds/{inboundId}/loading/evidence:
get:
summary: List loading evidence bound to one customer inbound
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}]
responses:
'200': {description: Business envelope containing the stable evidence array}
post:
summary: Append one real File service object to a loading evidence type
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody:
required: true
content:
application/json: {schema: {$ref: '#/components/schemas/UpsertEvidenceRequest'}}
responses:
'200': {description: 'Business envelope; ownership, type and version failures use business error codes'}
/api/platform/v1/inbounds/{inboundId}/loading/evidence/{evidenceId}/content/pda:
get:
summary: Download one loading evidence photo for the active PDA worker
description: Uses the existing platform:firstleg:pda:work authority and validates customer, inbound and evidence ownership before proxying File service content.
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/EvidenceId'}, {$ref: '#/components/parameters/TenantId'}]
responses:
'200': {description: Authenticated image bytes}
/api/platform/v1/inbounds/{inboundId}/loading/evidence/{evidenceId}/void/pda:
post:
summary: Void an incorrect PDA loading evidence photo
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/EvidenceId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody:
required: true
content:
application/json: {schema: {$ref: '#/components/schemas/VoidEvidenceRequest'}}
responses:
'200': {description: Business envelope; requires platform:firstleg:pda:work}
/api/platform/v1/inbounds/{inboundId}/loading/evidence/{evidenceId}/void:
post:
summary: Void an incorrect loading evidence photo without hard deletion
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/EvidenceId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody:
required: true
content:
application/json: {schema: {$ref: '#/components/schemas/VoidEvidenceRequest'}}
responses:
'200': {description: Business envelope containing the voided record and audit fields}
/api/platform/v1/inbounds/{inboundId}/loading/picking/start:
post:
summary: Refresh the compatibility projection from canonical container PDA tasks
@@ -107,38 +48,8 @@ components:
parameters:
InboundId: {name: inboundId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
TenantId: {name: tenantId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
EvidenceId: {name: evidenceId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
schemas:
EvidenceResponse:
type: object
required: [id, inboundId, tenantId, evidenceType, fileId, previewUrl, version, status]
properties:
id: {type: integer, format: int64}
inboundId: {type: integer, format: int64}
tenantId: {type: integer, format: int64}
evidenceType: {type: string, enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL]}
fileId: {type: integer, format: int64}
fileName: {type: string}
previewUrl: {type: string, description: Authenticated PDA image-content path; request with the current bearer token}
version: {type: integer}
status: {type: string}
UpsertEvidenceRequest:
type: object
required: [expectedVersion, evidenceType, fileId]
properties:
expectedVersion: {type: integer, minimum: 0}
evidenceType: {type: string, enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL], description: PHOTO_STEP_1=空仓,PHOTO_STEP_2=半仓,PHOTO_STEP_3=满仓,PHOTO_STEP_4=锁仓,SEAL=带锁码照;同类型可多次追加}
fileId: {type: integer, format: int64, minimum: 1}
fileName: {type: string, maxLength: 255, deprecated: true, description: Ignored; server uses File metadata}
sealNo: {type: string, maxLength: 64, description: Required when evidenceType is SEAL}
remark: {type: string, maxLength: 1000}
VoidEvidenceRequest:
type: object
required: [expectedVersion, reason]
properties:
expectedVersion: {type: integer, minimum: 0}
reason: {type: string, minLength: 1, maxLength: 500, description: 作废原因,记录操作人和时间}
PickingRequest:
type: object
required: [expectedVersion, expectedCount, pickedCount]
@@ -119,6 +119,57 @@ paths:
parameters: [{$ref: '#/components/parameters/TaskId'}]
responses:
'200': {description: Ordered operation history}
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/evidence:
get:
summary: 查询当前领取人的柜次装柜证据
parameters:
- {$ref: '#/components/parameters/TaskId'}
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
responses: {'200': {description: 仅platform:firstleg:pda:work且任务必须由当前用户领取}}
post:
summary: 通过Firstleg代理上传并登记当前柜次装柜证据
parameters:
- {$ref: '#/components/parameters/TaskId'}
- {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
- {name: expectedVersion, in: query, required: true, schema: {type: integer, minimum: 0}}
- {name: evidenceType, in: query, required: true, schema: {type: string, enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL]}}
- {name: sealNo, in: query, required: false, schema: {type: string, maxLength: 64}}
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties: {file: {type: string, format: binary}}
responses: {'200': {description: 柜级证据列表}}
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/evidence/{evidenceId}/content:
get:
summary: 下载当前柜次有效装柜证据
parameters:
- {$ref: '#/components/parameters/TaskId'}
- {name: evidenceId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
responses: {'200': {description: 图片字节}}
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/evidence/{evidenceId}/void:
post:
summary: 作废当前柜次错误证据
parameters:
- {$ref: '#/components/parameters/TaskId'}
- {name: evidenceId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [warehouseId, expectedVersion, reason]
properties:
warehouseId: {type: integer, format: int64, minimum: 1}
expectedVersion: {type: integer, minimum: 0}
reason: {type: string, minLength: 1, maxLength: 500}
responses: {'200': {description: 作废后柜级证据列表}}
components:
parameters:
TaskId: {name: taskId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}