feat(firstleg): split customs receipt and final document flow

This commit is contained in:
2026-09-26 13:28:18 +08:00
parent 5a895bf214
commit ff2386e5bf
13 changed files with 405 additions and 110 deletions
+2
View File
@@ -4,6 +4,8 @@
`db/recent-schema-incremental-20260923.sql`:汇总 2026-09-21 至 2026-09-22 的近期结构变更,依次显式指向 `baiyue_iam`、`baiyue_firstleg`、`baiyue_lastleg`、`baiyue_lastlag_warehouse`;补齐国内 PDA 作业仓字段及索引、人工 FBA 报价可空字段、墨家 SKU 测量字段、换标申请普通索引和 A0 共享溢出位字段。所有字段和索引变更均经 `information_schema` 判断,可重复执行;不创建数据库、不含凭据、权限种子或业务状态修正,末尾输出逐库 `READY/CHECK_REQUIRED`。由操作者选择并确认目标连接后执行,任一 DDL 报错立即停止。
`db/customs-two-file-incremental-20260926.sql`:报关两文件流程增量结构,保留报关行回执与新增真实报关完税文件,补充“已发送给报关行执行”的操作人和时间;仅新增结构,可重复执行,不修改业务数据。
`local-return-relabel-acceptance.mjs` 的 `LOCAL_RETURN_VERIFY_REMAINING=true` 专项新建3件退货,先换1件并客户确认,再对同一退货单剩余2件中的1件新建第二申请,验证同键重放、第二轮完工后剩余1件;不执行出库,报告 scope 为 `REMAINING_RETURN_RELABEL`,不代表出库验收通过。仍保留单据、使用正式API和合成照片,默认单票模式不变。
`local-return-relabel-outbound.mjs`:仅本机网关,读取显式 `LOCAL_RETURN_SOURCE` 单票报告,为其中同客户、同仓、已完成换标的唯一一件可用库存执行平台创建出库、原键重放、PDA拣货及完成。沿用平台账号环境参数,报告默认 `reports/local-return-relabel-outbound-<marker>.json` 或 `--output`;创建临时PDA并finally停用,不改客户权限。平台库存核验不替代源报告的客户读权限失败。会推进库存与出库状态,保留单据及报告;失败后按已保存单号核查,不自动清理,不可对已出库库存盲目重跑。
@@ -17,10 +17,14 @@ export const REQUIRED_ROUTE_OPERATIONS = Object.freeze([
'merchantDeleteDocument',
'merchantConfirmReceipt',
'platformStart',
'platformUploadCompletion',
'platformUploadReceipt',
'platformSubmitToBroker',
'platformUploadFinal',
'platformComplete',
'platformDownload',
'merchantDownload',
'platformDownloadReceipt',
'platformDownloadFinal',
'merchantDownloadReceipt',
'merchantDownloadFinal',
'outboundState',
'satisfyOutboundPrerequisites',
])
@@ -29,7 +33,7 @@ export const CONTAINER_CUSTOMS_MATRIX = Object.freeze([
{ priority: 'P0', fixture: 'ALL_BUY_DECLARATION', scenario: 'LOADED详情与买单XLSX', expected: 'XLSX只含柜内买单实装行,下载窗口可用' },
{ priority: 'P0', fixture: 'ALL_BUY_DECLARATION', scenario: '开始与完成', expected: 'READY_TO_START→DECLARING→COMPLETED,重放返回同一结果' },
{ priority: 'P0', fixture: 'ALL_TAX_REFUND', scenario: '客户多对多资料', expected: '上传、覆盖、改关联、删除均保留版本与客户范围' },
{ priority: 'P0', fixture: 'ALL_TAX_REFUND', scenario: '冻结、回执确认与逐份完成文件', expected: '开始冻结来源文件和关联;最后一份平台回执上传后由客户确认整份回执,才允许整柜完成' },
{ priority: 'P0', fixture: 'ALL_TAX_REFUND', scenario: '冻结、回执确认、报关行执行与完税文件', expected: '开始冻结来源文件和关联;上传回执、客户确认、发送报关行执行,最后上传真实报关完税文件才允许整柜完成' },
{ priority: 'P0', fixture: 'MIXED', scenario: '买单与退税混合', expected: '买单行进入XLSX,退税票逐票被资料覆盖,整柜统一开始/完成' },
{ priority: 'P0', fixture: 'ALL_TAX_REFUND', scenario: '缺资料禁止开始', expected: 'A0409且聚合、文件、关联、历史零漂移' },
{ priority: 'P0', fixture: 'MIXED', scenario: '自动离仓失败恢复', expected: '报关COMPLETED不回滚;待条件满足后后台重试并单次离仓' },
@@ -91,6 +95,7 @@ export const READ_ONLY_DB_EVIDENCE = Object.freeze([
'fl_customs_frozen_document_inbound',
'fl_customs_completion_document',
'fl_customs_completion_document_version',
'fl_customs_final_document',
'fl_customs_command',
'fl_customs_container_history',
'fl_outbound_dispatch',
@@ -88,16 +88,27 @@ function routes(context, merchantToken) {
platformStart: (ctx, fixture, request, idempotencyKey) => ctx.request('POST',
`${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}/start`, request, ctx.token,
{ 'Idempotency-Key': idempotencyKey }),
platformUploadCompletion: (ctx, fixture, request, idempotencyKey) => multipart(
`${PLATFORM}/documents/${encodeURIComponent(id(request.documentId))}/completion/upload?expectedVersion=${Number(request.expectedVersion)}&finalConfirmation=${request.finalConfirmation === true}`,
platformUploadReceipt: (ctx, fixture, request, idempotencyKey) => multipart(
`${PLATFORM}/documents/${encodeURIComponent(id(request.documentId))}/receipt/upload?expectedVersion=${Number(request.expectedVersion)}`,
{}, request, ctx.token, idempotencyKey),
platformSubmitToBroker: (ctx, fixture, request, idempotencyKey) => ctx.request('POST',
`${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}/submit-to-broker`, request, ctx.token,
{ 'Idempotency-Key': idempotencyKey }),
platformComplete: (ctx, fixture, request, idempotencyKey) => ctx.request('POST',
`${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}/complete-all-buy`, request, ctx.token,
{ 'Idempotency-Key': idempotencyKey }),
platformDownload: (ctx, fixture, request) => binary('GET',
`${PLATFORM}/documents/${encodeURIComponent(id(request.documentId))}/${request.completion ? 'completion/' : ''}download`),
merchantDownload: (ctx, fixture, customer, request) => binary('GET',
`${MERCHANT}/documents/${encodeURIComponent(id(request.documentId))}/${request.completion ? 'completion/' : ''}download`,
platformUploadFinal: (ctx, fixture, request, idempotencyKey) => multipart(
`${PLATFORM}/documents/${encodeURIComponent(id(request.documentId))}/final/upload?expectedVersion=${Number(request.expectedVersion)}`,
{}, request, ctx.token, idempotencyKey),
platformDownloadReceipt: (ctx, fixture, request) => binary('GET',
`${PLATFORM}/documents/${encodeURIComponent(id(request.documentId))}/receipt/download`),
platformDownloadFinal: (ctx, fixture, request) => binary('GET',
`${PLATFORM}/documents/${encodeURIComponent(id(request.documentId))}/final/download`),
merchantDownloadReceipt: (ctx, fixture, customer, request) => binary('GET',
`${MERCHANT}/documents/${encodeURIComponent(id(request.documentId))}/receipt/download`,
customer.customerToken || merchantToken),
merchantDownloadFinal: (ctx, fixture, customer, request) => binary('GET',
`${MERCHANT}/documents/${encodeURIComponent(id(request.documentId))}/final/download`,
customer.customerToken || merchantToken),
buyDeclarationXlsx: async (ctx, request) => {
const created = await ctx.request('POST', '/api/platform/v1/files/export-tasks', {
@@ -183,56 +183,71 @@ async function runRefund(context, routes, fixture) {
const documents = frozenSnapshot?.documents || []
for (const [index, document] of documents.entries()) {
const completion = await routes.platformUploadCompletion(context, fixture, {
const receipt = await routes.platformUploadReceipt(context, fixture, {
documentId: id(document.documentId), expectedVersion: Number(document.source?.version),
fileName: `${context.marker}-completion-${index + 1}.done`,
contentType: 'application/x-customs-completion', content: Uint8Array.from([0x43, index + 1]),
}, key(context, `completion-${index + 1}`))
context.assert(MODULE, `逐份上传完成文件${index + 1}`, completion,
success(completion)
&& zeroDrift(completion.body?.data?.inboundIds, document.inboundIds),
evidence(completion, `sourceDocumentId=${id(document.documentId)}`), OPTIONS)
if (index + 1 < documents.length) {
const beforeEarlyComplete = await platformDetail(context, routes, fixture)
const early = await routes.platformComplete(context, fixture, {
expectedVersion: Number(beforeEarlyComplete.body?.data?.version), remark: 'not-last-completion',
}, key(context, `early-complete-${index + 1}`))
await assertZeroDrift(context, '最后一份完成文件前禁止整柜完成', routes, fixture, beforeEarlyComplete, early)
}
fileName: `${context.marker}-broker-receipt-${index + 1}.pdf`,
contentType: 'application/pdf', content: Uint8Array.from([0x52, index + 1]),
}, key(context, `receipt-${index + 1}`))
context.assert(MODULE, `逐份上传报关行回执${index + 1}`, receipt,
success(receipt) && zeroDrift(receipt.body?.data?.inboundIds, document.inboundIds),
evidence(receipt, `sourceDocumentId=${id(document.documentId)}`), OPTIONS)
}
const completedReady = await platformDetail(context, routes, fixture)
const beforeConfirm = await platformDetail(context, routes, fixture)
const earlyFinal = await routes.platformUploadFinal(context, fixture, {
documentId: id(documents[0]?.documentId), expectedVersion: 0,
fileName: `${context.marker}-early-final.pdf`, contentType: 'application/pdf', content: Uint8Array.from([0x45]),
}, key(context, 'early-final'))
await assertZeroDrift(context, '回执未确认且未发送执行时禁止上传完税文件', routes, fixture, beforeConfirm, earlyFinal)
const confirmed = []
for (const customer of customers) {
const document = documents.find((item) => (item.inboundIds || []).some((inboundId) =>
customer.inboundIds.map(id).includes(id(inboundId))))
if (!document) continue
const version = Number(document.source?.version || completedReady.body?.data?.documents
?.find((item) => id(item.documentId) === id(document.documentId))?.source?.version || 0)
const current = await platformDetail(context, routes, fixture)
const live = current.body?.data?.documents?.find((item) => id(item.documentId) === id(document.documentId))
const version = Number(live?.receipt?.version || 0)
const response = await routes.merchantConfirmReceipt(context, fixture, customer, {
documentId: id(document.documentId), expectedVersion: version, confirm: true, remark: context.marker,
}, key(context, `refund-receipt-confirm-${customer.customerId}`))
confirmed.push(response)
}
const afterConfirm = await platformDetail(context, routes, fixture)
context.assert(MODULE, '客户确认整份回执后仍处于报关中', afterConfirm,
success(afterConfirm) && afterConfirm.body?.data?.status === 'DECLARING' && confirmed.every(success),
evidence(afterConfirm), OPTIONS)
const submitted = await routes.platformSubmitToBroker(context, fixture,
{ expectedVersion: Number(afterConfirm.body?.data?.version), remark: context.marker }, key(context, 'submit-to-broker'))
context.assert(MODULE, '平台发送客户确认回执给报关行执行', submitted,
success(submitted) && submitted.body?.data?.status === 'DECLARING', evidence(submitted), OPTIONS)
const submitReplay = await routes.platformSubmitToBroker(context, fixture,
{ expectedVersion: Number(submitted.body?.data?.version), remark: context.marker }, key(context, 'submit-to-broker'))
context.assert(MODULE, '发送报关行执行同键重放', submitReplay, success(submitReplay), evidence(submitReplay), OPTIONS)
for (const [index, document] of documents.entries()) {
const finalDocument = await routes.platformUploadFinal(context, fixture, {
documentId: id(document.documentId), expectedVersion: 0,
fileName: `${context.marker}-customs-tax-final-${index + 1}.pdf`,
contentType: 'application/pdf', content: Uint8Array.from([0x46, index + 1]),
}, key(context, `final-${index + 1}`))
context.assert(MODULE, `逐份上传真实报关完税文件${index + 1}`, finalDocument,
success(finalDocument) && zeroDrift(finalDocument.body?.data?.inboundIds, document.inboundIds),
evidence(finalDocument), OPTIONS)
}
const completed = await platformDetail(context, routes, fixture)
context.assert(MODULE, '客户确认整份回执后纯退税柜完成', completed,
success(completed) && completed.body?.data?.status === 'COMPLETED'
&& confirmed.every(success), evidence(completed), OPTIONS)
context.assert(MODULE, '真实报关完税文件齐全后纯退税柜完成', completed,
success(completed) && completed.body?.data?.status === 'COMPLETED', evidence(completed), OPTIONS)
const firstDocument = documents[0]
if (firstDocument) {
const platformDownload = await routes.platformDownload(context, fixture, {
documentId: id(firstDocument.documentId), completion: true,
})
context.assert(MODULE, 'Admin下载完成文件窗口', platformDownload,
platformDownload.http === 200 && platformDownload.bytes?.length > 0,
`HTTP=${platformDownload.http}, bytes=${platformDownload.bytes?.length ?? 0}`, OPTIONS)
const merchantDownload = await routes.merchantDownload(context, fixture, mutable, {
documentId: id(firstDocument.documentId), completion: true,
})
context.assert(MODULE, 'Client仅下载本客户完成文件窗口', merchantDownload,
merchantDownload.http === 200 && merchantDownload.bytes?.length > 0,
`HTTP=${merchantDownload.http}, bytes=${merchantDownload.bytes?.length ?? 0}`, OPTIONS)
const platformReceipt = await routes.platformDownloadReceipt(context, fixture, { documentId: id(firstDocument.documentId) })
context.assert(MODULE, 'Admin下载报关行回执', platformReceipt,
platformReceipt.http === 200 && platformReceipt.bytes?.length > 0, evidence(platformReceipt), OPTIONS)
const platformFinal = await routes.platformDownloadFinal(context, fixture, { documentId: id(firstDocument.documentId) })
context.assert(MODULE, 'Admin下载真实报关完税文件', platformFinal,
platformFinal.http === 200 && platformFinal.bytes?.length > 0, evidence(platformFinal), OPTIONS)
const merchantFinal = await routes.merchantDownloadFinal(context, fixture, mutable, { documentId: id(firstDocument.documentId) })
context.assert(MODULE, 'Client仅下载本客户真实报关完税文件', merchantFinal,
merchantFinal.http === 200 && merchantFinal.bytes?.length > 0, evidence(merchantFinal), OPTIONS)
}
}
async function runMixedConcurrency(context, routes, fixture) {
@@ -320,44 +335,55 @@ async function runMixedConcurrency(context, routes, fixture) {
}, key(context, 'locked-scope'))
await assertZeroDrift(context, '开始后资料范围锁定A0409零漂移', routes, fixture, { body: { data: frozen } }, locked)
const completionRequest = {
const receiptRequest = {
documentId: customer.documentId, expectedVersion: 0,
finalConfirmation: true, fileName: `${context.marker}-mixed-result.done`,
contentType: 'application/x-customs-completion', content: Uint8Array.from([0x52, 0x45, 0x53]),
fileName: `${context.marker}-mixed-broker-receipt.pdf`,
contentType: 'application/pdf', content: Uint8Array.from([0x52, 0x45, 0x43]),
}
const completionKey = key(context, 'mixed-completion')
const completion = await routes.platformUploadCompletion(context, fixture, completionRequest, completionKey)
const completionReplay = success(completion)
? await routes.platformUploadCompletion(context, fixture, completionRequest, completionKey) : null
const completionConverged = success(completion) && success(completionReplay)
&& id(completion.body?.data?.documentId) === id(completionReplay.body?.data?.documentId)
&& id(completion.body?.data?.completion?.fileId) === id(completionReplay.body?.data?.completion?.fileId)
context.assert(MODULE, '最后一份TAX完成文件二次确认并同键重放', completionReplay || completion,
completionConverged,
[completion, completionReplay].filter(Boolean).map((response) => evidence(response)).join('; '), OPTIONS)
if (!completionConverged) {
context.addCoverageGap(MODULE, OPTIONS.scenario, '最后一份完成文件二次确认',
'完成文件未成功或同键重放未收敛,已停止后续自动离仓写入',
'completion-idempotency', { deploymentBlocker: true })
return
}
const completionDifferent = await routes.platformUploadCompletion(context, fixture,
{ ...completionRequest, content: Uint8Array.from([0x44, 0x49, 0x46]) }, completionKey)
context.assert(MODULE, '完成文件同键异载荷A0409', completionDifferent,
a0409(completionDifferent), evidence(completionDifferent), OPTIONS)
const receiptVersion = Number(completion.body?.data?.source?.version || completion.body?.data?.version || 0)
const receiptKey = key(context, 'mixed-receipt-upload')
const receipt = await routes.platformUploadReceipt(context, fixture, receiptRequest, receiptKey)
const receiptReplay = success(receipt)
? await routes.platformUploadReceipt(context, fixture, receiptRequest, receiptKey) : null
const receiptConverged = success(receipt) && success(receiptReplay)
&& id(receipt.body?.data?.documentId) === id(receiptReplay.body?.data?.documentId)
&& id(receipt.body?.data?.receipt?.fileId) === id(receiptReplay.body?.data?.receipt?.fileId)
context.assert(MODULE, '报关行回执同键重放收敛', receiptReplay || receipt, receiptConverged,
[receipt, receiptReplay].filter(Boolean).map((response) => evidence(response)).join('; '), OPTIONS)
const receiptDifferent = await routes.platformUploadReceipt(context, fixture,
{ ...receiptRequest, content: Uint8Array.from([0x44, 0x49, 0x46]) }, receiptKey)
context.assert(MODULE, '回执同键异载荷A0409', receiptDifferent, a0409(receiptDifferent), evidence(receiptDifferent), OPTIONS)
const receiptVersion = Number(receipt.body?.data?.receipt?.version || 0)
const receiptConfirm = await routes.merchantConfirmReceipt(context, fixture, customer, {
documentId: customer.documentId, expectedVersion: receiptVersion, confirm: true, remark: context.marker,
}, key(context, 'mixed-receipt-confirm'))
const receiptReplay = await routes.merchantConfirmReceipt(context, fixture, customer, {
const receiptConfirmReplay = await routes.merchantConfirmReceipt(context, fixture, customer, {
documentId: customer.documentId, expectedVersion: receiptVersion, confirm: true, remark: context.marker,
}, key(context, 'mixed-receipt-confirm'))
context.assert(MODULE, '客户确认整份回执并同键重放', receiptReplay || receiptConfirm,
success(receiptConfirm) && success(receiptReplay)
&& receiptConfirm.body?.data?.status === 'COMPLETED'
&& receiptReplay.body?.data?.status === 'COMPLETED',
[receiptConfirm, receiptReplay].map((response) => evidence(response)).join('; '), OPTIONS)
context.assert(MODULE, '客户确认回执后仍未完成整柜', receiptConfirmReplay || receiptConfirm,
success(receiptConfirm) && success(receiptConfirmReplay)
&& receiptConfirm.body?.data?.status === 'DECLARING'
&& receiptConfirmReplay.body?.data?.status === 'DECLARING',
[receiptConfirm, receiptConfirmReplay].map((response) => evidence(response)).join('; '), OPTIONS)
const submitted = await routes.platformSubmitToBroker(context, fixture,
{ expectedVersion: Number(receiptConfirm.body?.data?.version), remark: context.marker }, key(context, 'mixed-submit-to-broker'))
context.assert(MODULE, '混合柜发送确认回执给报关行执行', submitted,
success(submitted) && submitted.body?.data?.status === 'DECLARING', evidence(submitted), OPTIONS)
const finalRequest = {
documentId: customer.documentId, expectedVersion: 0,
fileName: `${context.marker}-mixed-customs-tax-final.pdf`,
contentType: 'application/pdf', content: Uint8Array.from([0x46, 0x49, 0x4e]),
}
const finalKey = key(context, 'mixed-final')
const finalDocument = await routes.platformUploadFinal(context, fixture, finalRequest, finalKey)
const finalReplay = success(finalDocument)
? await routes.platformUploadFinal(context, fixture, finalRequest, finalKey) : null
const finalConverged = success(finalDocument) && success(finalReplay)
&& id(finalDocument.body?.data?.finalDocument?.fileId) === id(finalReplay.body?.data?.finalDocument?.fileId)
context.assert(MODULE, '真实报关完税文件同键重放收敛', finalReplay || finalDocument, finalConverged,
[finalDocument, finalReplay].filter(Boolean).map((response) => evidence(response)).join('; '), OPTIONS)
const finalDifferent = await routes.platformUploadFinal(context, fixture,
{ ...finalRequest, content: Uint8Array.from([0x44, 0x49, 0x46]) }, finalKey)
context.assert(MODULE, '完税文件同键异载荷A0409', finalDifferent, a0409(finalDifferent), evidence(finalDifferent), OPTIONS)
let completed = await platformDetail(context, routes, fixture)
context.assert(MODULE, '混合柜最后一份完成文件后整柜COMPLETED', completed,
@@ -396,10 +422,10 @@ async function runMixedConcurrency(context, routes, fixture) {
evidence(queryDetail), OPTIONS)
}
const completionDownload = await routes.merchantDownload(context, fixture, customer, {
documentId: customer.documentId, completion: true,
const completionDownload = await routes.merchantDownloadFinal(context, fixture, customer, {
documentId: customer.documentId,
})
context.assert(MODULE, '整柜完成后Client可下载自身TAX完成文件', completionDownload,
context.assert(MODULE, '整柜完成后Client可下载自身真实报关完税文件', completionDownload,
completionDownload.http === 200 && completionDownload.bytes?.length > 0,
`HTTP=${completionDownload.http}, bytes=${completionDownload.bytes?.length ?? 0}`, OPTIONS)
}
@@ -0,0 +1,28 @@
-- 报关两文件流程:回执与真实报关完税文件分离。
-- 仅在已确认目标测试库后执行;所有 DDL 可重复执行,不创建数据库、不写业务数据。
USE baiyue_firstleg;
SET @ddl = IF(NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema=DATABASE() AND table_name='fl_customs_container' AND column_name='broker_submitted_at'
), 'ALTER TABLE fl_customs_container ADD COLUMN broker_submitted_by BIGINT UNSIGNED NULL COMMENT ''发送报关行执行的操作人ID'' AFTER completed_at, ADD COLUMN broker_submitted_by_name VARCHAR(64) NULL COMMENT ''发送报关行执行的操作人名称'' AFTER broker_submitted_by, ADD COLUMN broker_submitted_at DATETIME(3) NULL COMMENT ''发送报关行执行时间'' AFTER broker_submitted_by_name', 'SELECT 1');
PREPARE customs_container_broker_submit_stmt FROM @ddl; EXECUTE customs_container_broker_submit_stmt; DEALLOCATE PREPARE customs_container_broker_submit_stmt;
CREATE TABLE IF NOT EXISTS fl_customs_final_document (
id BIGINT UNSIGNED NOT NULL COMMENT '真实报关完税文件ID',
source_document_id BIGINT UNSIGNED NOT NULL COMMENT '对应冻结客户资料ID',
file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件ID',
file_name VARCHAR(255) NOT NULL COMMENT '真实报关完税文件名称快照',
file_hash CHAR(64) NOT NULL COMMENT '真实报关完税文件SHA-256摘要',
content_type VARCHAR(128) NULL COMMENT '真实报关完税文件媒体类型',
size_bytes BIGINT UNSIGNED NOT NULL COMMENT '真实报关完税文件大小(字节)',
uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传操作人ID',
uploaded_by_name VARCHAR(64) NULL COMMENT '上传操作人名称快照',
uploaded_at DATETIME(3) NOT NULL COMMENT '上传时间',
version INT UNSIGNED NOT NULL DEFAULT 1 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 '更新时间',
PRIMARY KEY(id),
UNIQUE KEY uk_customs_final_source(source_document_id),
CONSTRAINT fk_customs_final_source FOREIGN KEY(source_document_id) REFERENCES fl_customs_customer_document(id)
) ENGINE=InnoDB COMMENT='真实报关完税文件当前版本';