From a4794489b1fae362a9e55f1e7a79ef5d1658a8f6 Mon Sep 17 00:00:00 2001 From: liuwenxin <1009266660@qq.com> Date: Fri, 6 Nov 2026 14:49:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(T+=E4=B8=8E=E6=A0=B8=E7=AE=97):=20=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=E6=8A=B5=E9=A1=B6=E5=8F=A3=E5=BE=84=E5=92=8C=E6=89=98?= =?UTF-8?q?=E7=AE=A1=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根据审查结果统一以物抵顶的线下核实口径,移除当前模型、初始化脚本和前端接口中的自动折算误导文案。\n补充业务接入必须持有共享 T+ 命名锁的执行契约,明确来源快照稳定性依赖。\nWindows 部署说明区分新 supervisor 与旧任务迁移检查,避免现场并行启动旧 worker。\n\n影响范围:T+ 托管文档、业务接入契约、回款/台账模型注释和参数试算说明。\n\nCo-Authored-By: Claude Code --- backend/app/models/performance_ledger.py | 2 +- backend/app/models/receipt.py | 2 +- backend/app/services/formula.py | 11 ++- backend/app/services/param_service.py | 16 ++--- backend/app/services/tplus/business_sync.py | 7 +- db/init/init-dev.sql | 80 ++++++++++++++++++++- frontend/src/api/receipts.ts | 2 +- scripts/windows/README.md | 33 ++++++--- 8 files changed, 127 insertions(+), 26 deletions(-) diff --git a/backend/app/models/performance_ledger.py b/backend/app/models/performance_ledger.py index e58b17e..95657f5 100644 --- a/backend/app/models/performance_ledger.py +++ b/backend/app/models/performance_ledger.py @@ -148,7 +148,7 @@ class PerformanceLedger(Base, TimestampMixin): TINYINT_TYPE, nullable=False, server_default="0", - comment="以物抵顶折算:1已按 80% 折算 0否", + comment="以物抵顶处理标记:当前系统不自动折算,1表示由线下规则处理 0表示未标记", ) weight_r: Mapped[int | None] = mapped_column(comment="支付权重 R(回款占销售额百分比向下取整)") payment_weight: Mapped[Decimal | None] = mapped_column( diff --git a/backend/app/models/receipt.py b/backend/app/models/receipt.py index 0f6e740..3334120 100644 --- a/backend/app/models/receipt.py +++ b/backend/app/models/receipt.py @@ -72,7 +72,7 @@ class Receipt(Base, TimestampMixin, TPlusSyncMixin): TINYINT_TYPE, nullable=False, server_default="0", - comment="以物抵顶标记:1是 0否(M3 核算按应发绩效×barter.discount 折算,M2 仅标记)", + comment="以物抵顶标记:1是 0否(特殊绩效线下核实,系统不自动折算)", ) settlement_status: Mapped[int] = mapped_column( TINYINT_TYPE, diff --git a/backend/app/services/formula.py b/backend/app/services/formula.py index fcecc8c..0f10769 100644 --- a/backend/app/services/formula.py +++ b/backend/app/services/formula.py @@ -73,8 +73,11 @@ def converted_profit(revenue: Decimal, rate: Fraction, threshold: Fraction) -> D rate 为纯比例(profit_rate 的输出);threshold 为 % 数(存储口径,如 30)。 化简式等价于定义式「毛利润额 / 毛利润率 × (毛利润率 − 阈值)」,且避免 毛利润率为 0 时除零(对照表 #12/#13)。毛利润率低于阈值时结果为负 —— - 业务确认负值正常计算、不归零。 + 业务确认负值正常计算、不归零。零毛利润是独立特例:rate=0 时折算为 0, + 不能将该特例扩大为低于阈值的折算额一律归零。 """ + if rate == 0: + return Decimal("0") return _to_decimal(Fraction(revenue) * (rate - threshold / 100)) @@ -84,7 +87,11 @@ def performance_amount(base: Decimal, rate: Fraction) -> Decimal: def apply_barter(amount: Decimal, discount: Fraction) -> Decimal: - """以物抵顶折算:应发绩效 × 折算比例(discount 为 % 数,默认 80,对照表 #24)。""" + """保留的历史以物抵顶折算函数;当前正式核算不调用。 + + 当前口径是特殊抵顶由线下核实,系统不自动折算;参数和函数仅为历史 + 快照兼容保留,新增正式计算路径不得调用。 + """ return _to_decimal(Fraction(amount) * discount / 100) diff --git a/backend/app/services/param_service.py b/backend/app/services/param_service.py index fca9dae..a888ee0 100644 --- a/backend/app/services/param_service.py +++ b/backend/app/services/param_service.py @@ -622,14 +622,14 @@ def _preview_earning(ctx: _PreviewContext, decision: _PreviewRateDecision) -> _P def _preview_barter(ctx: _PreviewContext, performance: Decimal) -> _PreviewBarter: - """以物抵顶折算(按应发绩效的固定比例折算,比例来自参数)。""" - applied = ctx.payload.barter - after_barter: Decimal | None = None - if applied: - discount = ctx.take("barter.discount") - after_barter = formula.apply_barter(performance, discount) - base_amount = after_barter if applied else performance - return _PreviewBarter(applied=applied, after_barter=after_barter, base_amount=base_amount) + """以物抵顶标记(2026-11-03 口径:抵顶线下核算,系统不做自动折算)。 + + applied 为 False 表示没有自动折算;base_amount 照常取全额应发绩效, + after_barter 恒 None。barter.discount 参数保留作历史兼容,不参与当前试算。 + """ + applied = False + base_amount = performance + return _PreviewBarter(applied=applied, after_barter=None, base_amount=base_amount) def _preview_payment( diff --git a/backend/app/services/tplus/business_sync.py b/backend/app/services/tplus/business_sync.py index c0a544a..e1b8978 100644 --- a/backend/app/services/tplus/business_sync.py +++ b/backend/app/services/tplus/business_sync.py @@ -621,7 +621,12 @@ def _apply_document(ctx, entity, record, children, run_no, start): def execute_sync(session, run): - """按单据提交;中断后幂等重跑,不生成 PayrollInput 或工资。""" + """按单据提交;中断后幂等重跑,不生成 PayrollInput 或工资。 + + 调用方必须先持有与 T+ 原始采集相同的 MySQL 命名锁;这样完整来源 + manifest 在本次接入期间不会被另一进程发布新版本。这里仍在结束时 + 复核 manifest,作为防御性校验,不在本函数内再次获取同一把锁。 + """ from app.models import User from app.services.authorization_service import require_permission operator = session.get(User, run.requested_by) diff --git a/db/init/init-dev.sql b/db/init/init-dev.sql index 5eaf7a5..d70e36b 100644 --- a/db/init/init-dev.sql +++ b/db/init/init-dev.sql @@ -664,8 +664,12 @@ CREATE TABLE receipts ( shipment_id BIGINT UNSIGNED COMMENT '发货单ID(shipments.id;手动指定的回款对应批次,须与本行业务一致;NULL=暂未指定)', receipt_date DATE NOT NULL COMMENT '回款到账日(M3 按此日匹配参数版本,对照表 #11)', amount DECIMAL(14, 2) NOT NULL COMMENT '回款金额(≥0;一期禁负数/红字)', - is_barter TINYINT NOT NULL COMMENT '以物抵顶标记:1是 0否(M3 核算按应发绩效×barter.discount 折算,M2 仅标记)' DEFAULT '0', + is_barter TINYINT NOT NULL COMMENT '以物抵顶标记:1是 0否(特殊绩效线下核实,系统不自动折算)' DEFAULT '0', settlement_status TINYINT NOT NULL COMMENT '绩效核算状态:0未核算 1已核算(M3 核算防重锚点;已核算回款禁改禁删)' DEFAULT '0', + confirmation_status TINYINT NOT NULL DEFAULT 0 COMMENT '人工确认状态:0待确认 1已确认,独立于凭证及核算状态', + confirmed_by VARCHAR(64) COMMENT '人工确认人账号快照', + confirmed_at DATETIME COMMENT '人工确认时间', + confirmed_revision VARCHAR(64) COMMENT '人工确认时回款关键内容指纹', remark VARCHAR(255) COMMENT '备注', create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, @@ -877,7 +881,7 @@ CREATE TABLE performance_ledgers ( rate_text VARCHAR(32) NOT NULL COMMENT '计提比例原文(如 4/360,防 Fraction 截断;聚合行存 12.5%/5% 结构说明)', converted_profit NUMERIC(14, 2) COMMENT '折算毛利润额(可负;混合业务为各行汇总)', performance_amount NUMERIC(14, 2) COMMENT '绩效金额(可负,裁决:负绩效照算不归零)', - barter_applied TINYINT NOT NULL COMMENT '以物抵顶折算:1已按 80% 折算 0否' DEFAULT '0', + barter_applied TINYINT NOT NULL COMMENT '以物抵顶处理标记:当前系统不自动折算,1表示由线下规则处理 0表示未标记' DEFAULT '0', weight_r INTEGER COMMENT '支付权重 R(回款占销售额百分比向下取整)', payment_weight NUMERIC(7, 6) COMMENT '支付权重系数 R×R(展示用)', payable_amount NUMERIC(14, 2) COMMENT '当期应付绩效(绩效 × 支付权重;聚合行为当月汇总)', @@ -1712,4 +1716,74 @@ VALUES ('TPL_PREPAY_FIXED', '预付款·固定计提', 'prepay_fixed', JSON_OBJECT('rate','prepay.rate'), '乘法:预付回款额 × 预付比例', 50, 1, 1); -- 迁移版本记录:须等于当前 head,使 alembic upgrade head 成为空操作 -INSERT INTO alembic_version (version_num) VALUES ('28a9b0c1d2e3'); +INSERT INTO param_definitions (param_code,param_name,param_group,value_type,dimension_type,default_value,description,sort_order,status) SELECT 'receipt.confirmation.enabled','回款人工确认开关','回款确认','BOOL','NONE','1','默认开启。关闭仅跳过人工确认,继续校验凭证等资格;与工资审核独立。',16,1 WHERE NOT EXISTS (SELECT 1 FROM param_definitions WHERE param_code='receipt.confirmation.enabled'); +INSERT INTO permissions (perm_code,perm_name,perm_type,sort_order,status) SELECT 'business:receipts:confirm','回款管理:人工确认',2,100,1 WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE perm_code='business:receipts:confirm'); +UPDATE permissions p JOIN permissions parent ON parent.perm_code='business:receipts:view' SET p.parent_id=parent.id,p.status=1 WHERE p.perm_code='business:receipts:confirm'; +INSERT INTO role_permissions (role_id,permission_id) SELECT r.id,p.id FROM roles r JOIN permissions p ON p.perm_code='business:receipts:confirm' WHERE r.role_code IN ('admin','payroll_maker') AND NOT EXISTS (SELECT 1 FROM role_permissions old WHERE old.role_id=r.id AND old.permission_id=p.id); + +-- V30:业务接入与核算准备解耦;默认关闭自动接入,不执行回填或工资核算。 +ALTER TABLE products MODIFY product_category VARCHAR(32) NULL COMMENT '核算分类,NULL=待确认'; +ALTER TABLE customers MODIFY customer_type VARCHAR(32) NULL DEFAULT NULL COMMENT '核算客户类型,NULL=待确认'; +ALTER TABLE business_items MODIFY product_category VARCHAR(32) NULL COMMENT '核算分类,NULL=待确认'; +ALTER TABLE shipments MODIFY freight_amount DECIMAL(14,2) NULL DEFAULT NULL COMMENT '运费金额,NULL=来源待核实'; +ALTER TABLE businesses ADD integration_json JSON NULL COMMENT 'T+接入范围及来源人员摘要,非核算快照'; +ALTER TABLE tplus_source_heads ADD source_parent_id VARCHAR(64) NULL COMMENT '源父单据ID查询投影', ADD source_customer_id VARCHAR(64) NULL COMMENT '源客户ID查询投影', ADD source_date VARCHAR(10) NULL COMMENT '源业务日期查询投影'; +UPDATE tplus_source_heads h JOIN tplus_sync_records r ON r.id=h.record_id SET h.source_parent_id=NULLIF(JSON_UNQUOTE(JSON_EXTRACT(r.payload_json,'$.source_parent_id')),'null'), h.source_customer_id=NULLIF(JSON_UNQUOTE(COALESCE(JSON_EXTRACT(r.payload_json,'$.idcustomer'),JSON_EXTRACT(r.payload_json,'$.idpartner'))),'null'), h.source_date=LEFT(NULLIF(JSON_UNQUOTE(COALESCE(JSON_EXTRACT(r.payload_json,'$.voucherdate'),JSON_EXTRACT(r.payload_json,'$.voucherDate'))),'null'),10); +CREATE INDEX idx_tplus_source_heads_parent ON tplus_source_heads(source_database,entity_type,source_parent_id); +CREATE INDEX idx_tplus_source_heads_customer_date ON tplus_source_heads(source_database,entity_type,source_customer_id,source_date); +CREATE INDEX idx_tplus_source_heads_date ON tplus_source_heads(source_database,entity_type,source_date,id); +CREATE TABLE tplus_business_sync_configs ( + id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT, + source_database VARCHAR(128) NOT NULL COMMENT '源账套', + enabled INTEGER NOT NULL COMMENT '自动接入是否启用', + start_date DATE NOT NULL COMMENT '业务接入起点', + version INTEGER NOT NULL COMMENT '配置乐观版本', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '授权启用账号', + last_error VARCHAR(64) COMMENT '后台接入暂停原因', + create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT pk_tplus_business_sync_configs PRIMARY KEY (id), + CONSTRAINT uk_tplus_business_sync_configs_source UNIQUE (source_database) +)ENGINE=InnoDB CHARSET=utf8mb4 COMMENT='T+业务自动接入设置' COLLATE utf8mb4_general_ci; +CREATE TABLE tplus_business_documents ( + id BIGINT UNSIGNED NOT NULL COMMENT '主键ID' AUTO_INCREMENT, + source_database VARCHAR(128) NOT NULL COMMENT '源账套', + entity_type VARCHAR(32) NOT NULL COMMENT '源实体', + source_id VARCHAR(64) NOT NULL COMMENT '源单据或明细ID', + source_no VARCHAR(128) COMMENT '原始单号', + record_id BIGINT UNSIGNED NOT NULL COMMENT '当前观察到的源快照ID', + applied_record_id BIGINT UNSIGNED COMMENT '已接入的源快照ID', + fingerprint VARCHAR(64) COMMENT '已应用的业务字段指纹', + business_id BIGINT UNSIGNED COMMENT '关联业务ID', + target_id BIGINT UNSIGNED COMMENT '正式单据或明细ID', + status VARCHAR(24) NOT NULL COMMENT 'APPLIED/PENDING/CONFLICT/EXCLUDED', + reason VARCHAR(128) COMMENT '固定待补或差异编码', + data_json JSON NOT NULL COMMENT '最小必要来源与差异说明', + create_time DATETIME NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT pk_tplus_business_documents PRIMARY KEY (id), + CONSTRAINT uk_tplus_business_documents_source_entity_id UNIQUE (source_database, entity_type, source_id) +)ENGINE=InnoDB CHARSET=utf8mb4 COMMENT='T+业务单据接入与差异账本' COLLATE utf8mb4_general_ci; +CREATE INDEX idx_tplus_business_documents_business_entity ON tplus_business_documents (business_id, entity_type, id); +CREATE INDEX idx_tplus_business_documents_source_status ON tplus_business_documents (source_database, status, id); + +-- 运行控制不随开发初始化删除;已有设置保留,新库默认停止。 +CREATE TABLE IF NOT EXISTS tplus_runtime_controls ( + id BIGINT UNSIGNED NOT NULL, + desired_state VARCHAR(16) NOT NULL DEFAULT 'STOPPED', + actual_state VARCHAR(16) NOT NULL DEFAULT 'STOPPED', + worker_phase VARCHAR(16) NOT NULL DEFAULT 'STOPPED', + heartbeat_at DATETIME NULL, + last_error VARCHAR(255) NULL, + host VARCHAR(128) NULL, + platform VARCHAR(32) NULL, + owner VARCHAR(36) NULL, + version INTEGER NOT NULL DEFAULT 0, + updated_by BIGINT UNSIGNED NULL, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT pk_tplus_runtime_control PRIMARY KEY (id) +) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='T+读取服务运行控制'; +INSERT IGNORE INTO tplus_runtime_controls (id) VALUES (1); + +INSERT INTO alembic_version (version_num) VALUES ('33e4f5a6b7c8'); diff --git a/frontend/src/api/receipts.ts b/frontend/src/api/receipts.ts index 6b36b9c..4cc20d9 100644 --- a/frontend/src/api/receipts.ts +++ b/frontend/src/api/receipts.ts @@ -18,7 +18,7 @@ export interface ReceiptItem { receipt_date: string /** 回款金额(元),字符串直出 */ amount: string - /** 以物抵顶标记:1是 0否(绩效按比例折算) */ + /** 以物抵顶标记:1是 0否(特殊绩效线下核实,系统不自动折算) */ is_barter: number /** 核算状态:0未核算 1已核算(已核算禁改禁删) */ settlement_status: number diff --git a/scripts/windows/README.md b/scripts/windows/README.md index 27ee124..03c162f 100644 --- a/scripts/windows/README.md +++ b/scripts/windows/README.md @@ -1,6 +1,6 @@ -# Windows 任务计划托管 +# Windows T+ supervisor 托管 -本包托管后端、T+ worker 和每分钟周期检查。首次安装生成三个禁用任务并保留停止标记;自动周采集另外由应用配置启用。采集不会自动导入正式业务、核算或生成工资。 +本包说明旧 Windows 任务计划与新 supervisor 服务的迁移边界。新部署使用 `scripts/windows/PayrollTPlusSupervisor.xml.example` 由管理员选择并核验 WinSW 版本后注册 supervisor;旧的后端、worker 和每分钟维护任务不得与新 supervisor 并行运行。自动周采集与业务自动接入分别由应用配置启用。采集本身只写来源快照;启用业务自动接入后,成功采集会排队建档或追加单据,但不会自动核算或生成工资。业务自动接入默认关闭。 ## 配置及账号准备 @@ -12,7 +12,7 @@ - 脚本安装前和 Start 前检查关键入口、文件所有者和上层目录 ACL;不会自动改变 ACL。管理员仍需确保部署树全部内容和 Python 依赖都受相同保护。 - 安装清单保存服务账号 SID 和登录方式,Check/Start/Stop 均核验实际任务账号及 Limited 权限,发现漂移则拒绝操作。 -应用需先完成经授权的迁移;当前 head 为 `21f2a3b4c5d6`(V20 调度状态、V21 状态默认值修正)。本次仅在隔离库验证,未迁移客户业务库。 +应用需先完成经授权的迁移;部署时以本次交付代码的 `alembic heads` 和目标库的 `alembic current` 核对版本,不能沿用历史 head。客户现场外部 T+ 连接、证书和 Linux 部署方式见 [客户现场部署与 T+ 连接说明](../../docs/客户现场部署与T+连接说明.md)。 `backend/.env` 中的非敏感开关: @@ -26,9 +26,22 @@ T_PLUS_SCHEDULE_TIME=02:00 weekday 的 0~6 分别为周一至周日,时间固定按 `Asia/Shanghai` 解释。上线前保持自动化开关为 false。修改后重启托管进程,使后端与检查命令使用同一配置。 -## 预检查和安装 +## 新部署:WinSW supervisor -在 Windows PowerShell 5 中执行,使用实际部署路径。以下为示例路径,不含真实凭据。 +新安装不注册旧的三个任务。管理员应选择并核验适用于目标 Windows Server 的 WinSW 版本,将包装器与同名 XML 放到受保护目录,按现场路径修改 [PayrollTPlusSupervisor.xml.example](PayrollTPlusSupervisor.xml.example) 中的程序、工作目录和日志目录,再注册 `PayrollTPlusSupervisor` 服务。服务账号、ACL、证书和未登录状态下的配置读取必须在目标机验证;supervisor 只在 API、MySQL、Redis 和 T+ 只读连接健康时领取任务。 + +```powershell +# 仅示意:WinSW 版本、路径和服务账号由目标环境管理员核验 +winsw.exe install PayrollTPlusSupervisor.xml +winsw.exe start PayrollTPlusSupervisor +winsw.exe status PayrollTPlusSupervisor +``` + +停止时先在页面停止读取服务,等待当前安全步骤结束,再由管理员停止 Windows Service。不要同时启用旧 `PayrollDev-worker`、旧维护任务和新 supervisor;旧任务只做识别、停用和回退记录。 + +## 旧任务计划迁移检查(不用于新安装) + +以下脚本仅用于识别和迁移既有任务计划,不是新部署入口。在 Windows PowerShell 5 中执行,使用实际部署路径。以下为示例路径,不含真实凭据。 ```powershell $project = 'D:\Payroll App' @@ -60,17 +73,19 @@ $reader = 'D:\Payroll Secrets\reader.local.json' & "$project\scripts\windows\Manage-PayrollTasks.ps1" -Action Stop -RuntimeDirectory $runtime ``` -Start 的 `-Apply` 删除项目停止标记,启用并启动三项任务。Stop 的 `-Apply` 先写停止标记并禁用任务;worker 完成当前工作后退出,不再领取下一项。默认等待 30 秒,长查询未结束则返回 2,稍后 Check。仅在核实中断影响后,才使用 `-Force -Apply` 强制停止明确的项目任务;重启后遗留 RUNNING 批次标记中断,原数据及失败批次保留,由制作人核对后重试。 +以下 `Start`/`Stop` 和 `tplus-maintain` 说明仅适用于旧任务计划迁移检查,不是新 supervisor 的启动入口。新部署请按上方 WinSW supervisor 说明操作,不要执行这些旧任务的 `-Apply` 启动流程。 -每项任务使用 IgnoreNew,异常退出间隔 1 分钟重启,最多 3 次;启动器保留子进程退出码。后端及 worker 开机触发,维护命令开机及每分钟触发,长任务无强制执行时限。启动器子进程隐藏运行,后端绑定 `127.0.0.1`,对外访问应沿用现有网关。 +旧任务迁移时,`Start` 的 `-Apply` 只允许在回退窗口按原安装清单恢复;`Stop` 的 `-Apply` 先写停止标记并禁用旧任务。默认等待 30 秒,长查询未结束则返回 2,稍后 Check。仅在核实中断影响后,才使用 `-Force -Apply` 强制停止明确的旧项目任务;重启后遗留 RUNNING 批次标记中断,原数据及失败批次保留,由制作人核对后重试。旧任务不得与 `PayrollTPlusSupervisor` 并行。 + +旧任务使用 IgnoreNew,异常退出间隔 1 分钟重启,最多 3 次;启动器保留子进程退出码。后端、旧 worker 和旧维护命令的触发方式仅供迁移核对,不代表新部署架构。启动器子进程隐藏运行,后端绑定 `127.0.0.1`,对外访问应沿用现有网关。 ## 调度和异常处理 -`tplus-maintain --once` 只排队最近已到期的一周,同周期幂等;停机后补一次,不逐周补积压。手动任务/正式导入工作区正在运行时延后排队。人工重试结果接入该周期,不会在人工恢复成功后再次重读原失败批次。 +旧版 `tplus-maintain --once` 只用于迁移期间核对最近到期的一周,同周期幂等;新 supervisor 内部负责周期排队和 worker 执行。停机后补一次,不逐周补积压。手动任务/正式导入工作区正在运行时延后排队。人工重试结果接入该周期,不会在人工恢复成功后再次重读原失败批次。 仅定时采集且错误均为明确断连或超时,才在终止后 5 分钟、15 分钟重试,最多两次。中途断连保留已暂存页并标记失败,重新全量读取由哈希去重。行级错误/部分失败、配置错误、字段错误、进程中断和正式导入不自动重试。原批次、重试关系与错误记录一直保留。 -T+ 同步页显示自动化开关、心跳、最近成功和下次重试,以及失败/部分失败、排队超过 15 分钟、超过 8 天无成功、心跳中断提示。独立心跳每 30 秒写入,2 分钟无更新提示离线。日志按次写入 `runtime\logs`,不自动删除;不输出驱动连接文本。外部邮件及企业消息未启用。 +T+ 同步页显示自动化开关、心跳、最近成功和下次重试,以及失败/部分失败、排队超过 15 分钟、超过 8 天无成功、心跳中断提示。独立心跳每 30 秒写入,2 分钟无更新提示离线。日志固定保留当前与上一次两份(`runtime\logs\<角色>.out.log`、`<角色>.err.log` 及对应 `.prev.log`),不输出驱动连接文本。外部邮件及企业消息未启用。 ## 验证范围