Compare commits
2 Commits
0d63359acb
...
8a171a572c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a171a572c | ||
|
|
651341b36c |
@ -37,7 +37,10 @@ T_PLUS_CONNECTION_FILE=
|
||||
# 必须填写在客户 SQL Server 2008 R2 上验证过的驱动名称。
|
||||
# 本地 PoC 可复用已安装的 SQL Server 驱动;不自动安装或升级客户驱动。
|
||||
T_PLUS_ODBC_DRIVER=
|
||||
T_PLUS_ENCRYPT=false
|
||||
# 默认启用传输加密。旧实例 TLS 不兼容时,仅隔离开发环境可显式设为 false,
|
||||
# 记录原因并重新验收;程序不会自动降级。真实环境值后续按验收结果填写。
|
||||
T_PLUS_ENCRYPT=true
|
||||
# 内网自签证书可暂用 true;它不校验证书身份。部署可信证书后改为 false。
|
||||
T_PLUS_TRUST_SERVER_CERTIFICATE=true
|
||||
T_PLUS_CONNECT_TIMEOUT=10
|
||||
T_PLUS_QUERY_TIMEOUT=120
|
||||
|
||||
@ -69,7 +69,7 @@ class Settings(BaseSettings):
|
||||
t_plus_connection_file: str = ""
|
||||
# 部署前必须指定已在 SQL Server 2008 R2 实测的驱动,不自动选择最新版。
|
||||
t_plus_odbc_driver: str = ""
|
||||
t_plus_encrypt: bool = False
|
||||
t_plus_encrypt: bool = True
|
||||
t_plus_trust_server_certificate: bool = True
|
||||
t_plus_connect_timeout: int = 10
|
||||
t_plus_query_timeout: int = Field(default=120, ge=1, le=600)
|
||||
@ -115,6 +115,7 @@ class Settings(BaseSettings):
|
||||
|
||||
连接 URL 只在启用 T+ 适配器时使用;密码由 SQLAlchemy URL 对象保存,
|
||||
日志中调用 ``render_as_string()`` 时仍会默认隐藏密码。
|
||||
ODBC 的分号和花括号由方言统一转义,调用方不得再手工拼接连接串。
|
||||
"""
|
||||
query = {
|
||||
"driver": self.t_plus_odbc_driver,
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
|
||||
连接配置、启动命令和异常处理参见项目 `docs/T+本地恢复与同步开发手册.md`。后端与 worker 必须指向同一 MySQL 业务库和 T+ 账套,查询文件随部署交付。
|
||||
|
||||
应用与兼容性脚本共用 `Settings.t_plus_database_url` 和连接文件校验;分号、花括号由 SQLAlchemy ODBC 方言转义。默认启用加密,失败不自动降级;既有私有配置需要按运行手册核对。管理页面不返回原始数据,但暂存 JSON 的生产敏感字段保护仍待 S1 决策。
|
||||
|
||||
## 验收限制
|
||||
|
||||
当前只写 MySQL 暂存与管理表,不写工资业务表、不推进时间水位。关联、成本、日期和负向事件的业务规则待客户确认;SQL Server 2022 的实测不能替代客户 2008 R2(10.50.x)的兼容验收。
|
||||
当前只写 MySQL 暂存与管理表,不写工资业务表、不推进时间水位。关联、成本、日期和负向事件的业务规则待客户确认。开发验收先以备份源 10.50.1600 为基准,生产完整版本后补;2022 副本结果不能替代该基准的实测。
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
# 后端测试
|
||||
|
||||
> AI 摘要:本轮 T+ 管理已完成真实 MySQL、只读源库与浏览器验证。最终单元测试 602 项通过;管理/并发/结构定点 18 项通过。真正的 2008 R2 验收仍未执行。
|
||||
> AI 摘要:2026-10-28 的 P0.5 连接修复已通过 19 项定点测试和 1001 项完整回归;使用本轮独立 MySQL/Redis 容器,未连接客户 T+ 源库。真实备份源 10.50.1600 验收仍未执行。
|
||||
|
||||
## 连接策略修复核验(2026-10-28)
|
||||
|
||||
- Ruff(含兼容性脚本)通过;定点命令 `uv run pytest tests/unit/test_tplus_config.py tests/unit/test_tplus_connection_file.py tests/unit/test_tplus_compatibility_script.py -q`:19 passed。
|
||||
- 新增覆盖:数据库名/用户名/密码中的分号、花括号和中文;默认加密与显式回退;脚本复用连接文件校验;10.50.1600 与其他补丁、2022 版本区分;连接失败不自动降级且报告不带驱动原始异常。
|
||||
- 完整回归:1001 passed,1 warning,869.47 秒。使用已安装的 `mysql:8.0`、`redis:7-alpine` 镜像启动独立容器、随机宿主端口和命名卷,以 `db/init/init-dev.sql` 初始化;未连接现有开发业务库或 T+ 源库。
|
||||
- 完整测试入口为 `uv run python ../.ai/run_isolated_regression.py`,内部调用 `pytest.main(["-q", ...])`,不筛选或跳过用例。运行期配置指向临时隔离实例;单测的 `_env_file=None` 仍保持默认值测试语义。结束后已清理本轮容器、卷与临时凭据配置。
|
||||
- 兼容性脚本、worker 的 `--help` 可运行;保留现有 Starlette TestClient 弃用提醒。本轮未改前端,未重复前端构建。
|
||||
- 版本判断与失败分支是单元模拟结果,不能据此宣称真实 2008 R2 的驱动、TLS、断连和权限已验收。下方 2026-10-27 的结果保留为历史记录。
|
||||
|
||||
## T+ 本轮核验(2026-10-27)
|
||||
|
||||
|
||||
92
backend/tests/unit/test_tplus_compatibility_script.py
Normal file
92
backend/tests/unit/test_tplus_compatibility_script.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""兼容性脚本与应用使用相同连接策略,失败报告不泄露连接数据。"""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.config import Settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def script(monkeypatch):
|
||||
# 纯单元测试不需要安装 ODBC 驱动或连接源库。
|
||||
monkeypatch.setitem(sys.modules, "pyodbc", MagicMock())
|
||||
path = Path(__file__).resolve().parents[3] / "scripts/tplus/check_compatibility.py"
|
||||
spec = importlib.util.spec_from_file_location("compatibility_script", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
settings = Settings(_env_file=None, app_secret_key="x" * 32, t_plus_encrypt=True)
|
||||
monkeypatch.setattr(module, "get_settings", lambda: settings)
|
||||
return module
|
||||
|
||||
|
||||
def test_脚本复用连接文件校验与加密设置(script, tmp_path):
|
||||
path = tmp_path / "reader.json"
|
||||
payload = {
|
||||
"host": "localhost",
|
||||
"port": 1433,
|
||||
"database": "data;x}",
|
||||
"user": "reader;other}",
|
||||
"password": "test-only;x}",
|
||||
"driver": "TestDriver",
|
||||
}
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
resolved = script.compatibility_settings(path)
|
||||
assert resolved.t_plus_database_url.database == payload["database"]
|
||||
assert resolved.t_plus_database_url.username == payload["user"]
|
||||
assert resolved.t_plus_database_url.query["Encrypt"] == "yes"
|
||||
assert script.get_settings().t_plus_enabled is False
|
||||
payload["user"] = "sa"
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
with pytest.raises(RuntimeError, match="专用只读"):
|
||||
script.compatibility_settings(path)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"version,passed",
|
||||
[
|
||||
("10.50.1600", True),
|
||||
("10.50.1600.0", True),
|
||||
("10.50.6000", False),
|
||||
("16.0.4165.4", False),
|
||||
],
|
||||
)
|
||||
def test_同主版本或2022副本不能冒充备份源同版本(script, version, passed):
|
||||
connection = MagicMock()
|
||||
cursor = connection.cursor.return_value
|
||||
cursor.execute.return_value.fetchone.side_effect = [
|
||||
(version,),
|
||||
("中文核验", Decimal("12345.6789"), datetime(2008, 1, 2, 3, 4, 5)),
|
||||
(0, 0, 0),
|
||||
]
|
||||
connection.getinfo.return_value = "test-driver"
|
||||
report = script._inspect_connection(connection, "10.50.1600")
|
||||
assert report["compatible"] is passed
|
||||
cursor.close.assert_called_once()
|
||||
|
||||
|
||||
def test_连接失败不降级且报告不带驱动异常原文(script, tmp_path, monkeypatch, capsys):
|
||||
report_path = tmp_path / "report.json"
|
||||
monkeypatch.setattr(
|
||||
sys, "argv", ["check", "--connection-file", "unused.json", "--report", str(report_path)]
|
||||
)
|
||||
monkeypatch.setattr(script, "compatibility_settings", lambda path: script.get_settings())
|
||||
engine = MagicMock()
|
||||
engine.raw_connection.side_effect = RuntimeError("PWD=secret-test-only;手机号")
|
||||
create = MagicMock(return_value=engine)
|
||||
monkeypatch.setattr(script, "create_engine", create)
|
||||
assert script.main() == 2
|
||||
create.assert_called_once()
|
||||
engine.raw_connection.assert_called_once()
|
||||
engine.dispose.assert_called_once()
|
||||
assert json.loads(report_path.read_text(encoding="utf-8")) == {
|
||||
"compatible": False,
|
||||
"error_type": "RuntimeError",
|
||||
}
|
||||
assert "secret-test-only" not in capsys.readouterr().out
|
||||
@ -3,6 +3,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.dialects.mssql.pyodbc import MSDialect_pyodbc
|
||||
|
||||
from app.core.config import Settings
|
||||
from app.services.tplus.mapping import MappingStatus, get_mapping
|
||||
@ -33,6 +34,34 @@ def test_T加未启用时读取器不建立连接() -> None:
|
||||
assert settings.t_plus_enabled is False
|
||||
|
||||
|
||||
def test_T加默认加密且允许显式隔离环境回退(monkeypatch) -> None:
|
||||
monkeypatch.delenv("T_PLUS_ENCRYPT", raising=False)
|
||||
monkeypatch.delenv("T_PLUS_TRUST_SERVER_CERTIFICATE", raising=False)
|
||||
assert _settings().t_plus_database_url.query["Encrypt"] == "yes"
|
||||
assert _settings().t_plus_database_url.query["TrustServerCertificate"] == "yes"
|
||||
url = _settings(t_plus_encrypt=False, t_plus_trust_server_certificate=False).t_plus_database_url
|
||||
assert url.query["Encrypt"] == "no"
|
||||
assert url.query["TrustServerCertificate"] == "no"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["a;b", "a}b", "{prefix", "中文;UID=other}x"])
|
||||
def test_ODBC特殊字符保持字段边界且不泄露密码(value) -> None:
|
||||
settings = _settings(
|
||||
t_plus_odbc_driver="Driver}Name",
|
||||
t_plus_database=value,
|
||||
t_plus_user=value,
|
||||
t_plus_password=value,
|
||||
t_plus_encrypt=True,
|
||||
)
|
||||
url = settings.t_plus_database_url
|
||||
args, _ = MSDialect_pyodbc().create_connect_args(url)
|
||||
escaped = "{" + value.replace("}", "}}") + "}"
|
||||
assert f"Database={escaped};UID={escaped};PWD={escaped};" in args[0]
|
||||
assert "DRIVER={Driver}}Name}" in args[0]
|
||||
assert ";Encrypt=yes;" in args[0]
|
||||
assert url.render_as_string().split("@", 1)[0].endswith(":***")
|
||||
|
||||
|
||||
def test_T加查询文件覆盖六类实体() -> None:
|
||||
reader = TPlusReader.__new__(TPlusReader)
|
||||
reader.query_dir = Path(__file__).resolve().parents[3] / "db" / "queries" / "tplus"
|
||||
|
||||
@ -4,49 +4,53 @@
|
||||
|
||||
## 1. 背景与当前状态(2026-10-28)
|
||||
|
||||
- 客户源备份 `ceshi20261026.bak` 版本为 10.50.1600;生产库具体版本待客户提供(确认单 B3)。
|
||||
- 客户源备份 `ceshi20261026.bak` 版本为 10.50.1600。按用户 2026-10-28 决定,先以该版本搭建开发验收环境;生产完整版本号后补,有差异时再核验(客户版 E01/E03)。
|
||||
- 本地 Docker 已用 SQL Server 2022 副本完成恢复 PoC:完整 CHECKDB 通过、只读账号可用、六类查询两轮核验 933,203 行无失败。该副本只用于结构核验与开发,**不能替代同版本验收**。
|
||||
- Hyper-V 虚拟机外壳已建于 `D:\work\app\tplus-vm`,处于关机、断网状态;Windows 与 SQL Server 2008 R2 安装介质尚未到位(确认单 B5)。
|
||||
- Hyper-V 虚拟机外壳已建于 `D:\work\app\tplus-vm`,处于关机、断网状态;Windows 与 SQL Server 2008 R2 安装介质尚未到位(客户版 E03)。
|
||||
|
||||
## 2. 环境约定
|
||||
|
||||
| 项目 | 约定 |
|
||||
| --- | --- |
|
||||
| 虚拟机目录 | `D:\work\app\tplus-vm` |
|
||||
| SQL Server 数据与日志 | `D:\work\app\tplus-sqlserver`(绑定目录,不入项目仓库) |
|
||||
| 备份文件 | `D:\work\app\tplus-backup`(先校验 SHA-256 再使用) |
|
||||
| SQL Server 数据与日志 | 2022 PoC 使用 `D:\work\app\tplus-sqlserver\data` 和 `log`;2008 R2 使用 D 盘虚拟磁盘内的来宾目录,恢复时确认实际路径 |
|
||||
| 备份文件 | 现有脚本使用 `D:\work\app\tplus-sqlserver\backup`;复制到虚拟机前后核对 SHA-256 |
|
||||
| 虚拟机网络 | 安装与验收期间保持仅主机或内网隔离,不接入公网 |
|
||||
| 凭据 | 只读账号凭据走双方认可的私密渠道交接;不写入本文档、代码或 Git |
|
||||
| 项目依赖脚本 | `scripts/tplus/`(工作区准备、备份检查、恢复、只读账号、结构盘点) |
|
||||
|
||||
客户版 E01 先使用本地恢复实例的连接信息和专用只读账号,E02 先用现有开发主机与网络,生产信息后补。E03 的目标版本已确定,但合法介质尚未到位,不能据此把安装或验收标为完成。
|
||||
|
||||
## 3. 装机步骤(依赖介质,介质到位后执行)
|
||||
|
||||
1. 运行 `scripts/tplus/prepare_sql2008r2_vm.ps1` 核对虚拟机外壳与磁盘位置;确认网卡保持断开。
|
||||
2. 挂载 Windows 安装介质,安装操作系统;完成后安装集成服务与全部安全更新,重新启用受限网络。
|
||||
3. 挂载 SQL Server 2008 R2 安装介质,安装数据库引擎实例;版本不低于 10.50.1600(RTM 即可还原源备份),实例排序规则与源备份一致(默认 Chinese_PRC_CI_AS)。
|
||||
4. 安装 SQL Server 2008 R2 的 SP3 与后续补丁(可选);补丁不影响 10.50 主版本兼容性。
|
||||
5. 在虚拟机内安装 `ODBC Driver 17 for SQL Server`(pyodbc 依赖;Driver 17 支持连接 2008 R2)。
|
||||
1. 通过 Hyper-V 管理器核对已有虚拟机与 D 盘磁盘位置,保持网络隔离。`prepare_sql2008r2_vm.ps1` 是创建脚本,已有同名虚拟机时拒绝覆盖,不用于状态检查。
|
||||
2. 准备适配 SQL Server 2008 R2 的合法 Windows 介质,安装来宾系统。记录系统版本及补丁,不把来宾系统版本当作未来工资部署主机的要求。
|
||||
3. 安装 SQL Server 2008 R2,基准锁定为 `10.50.1600`。排序规则从备份元数据或恢复库核验,不能凭文档默认值猜测。
|
||||
4. 首轮保留该基准;如后续需要测试 SP3 等其他补丁,另记版本和结果,不覆盖基准验收记录。
|
||||
5. 在运行后端/worker 的电脑上准备 ODBC 驱动。驱动、Windows 和 TLS 的实际组合须实测后记录,不能只根据驱动名称认定兼容。默认启用加密,隔离环境的显式回退步骤见本地开发手册。
|
||||
|
||||
## 4. 备份恢复步骤
|
||||
|
||||
1. `scripts/tplus/inspect_sqlserver_backup.ps1`:对 `ceshi20261026.bak` 执行 VERIFYONLY 与 FILELISTONLY,确认备份完整并列出数据/日志文件逻辑名。
|
||||
2. `scripts/tplus/restore_sqlserver_backup.ps1`:执行 RESTORE DATABASE,文件落位 `D:\work\app\tplus-sqlserver`;恢复模式按脚本默认。
|
||||
3. 恢复完成后执行 `DBCC CHECKDB` 全库检查,结果留档。
|
||||
4. `scripts/tplus/create_tplus_readonly_login.ps1`:创建专用只读登录与数据库用户,仅授予 `db_datareader`;凭据按第 2 节约定交接。
|
||||
5. `scripts/tplus/inventory_tplus_database.ps1`:盘点核心表结构与行数,与 Docker 副本的结构核验结果比对。
|
||||
现有备份检查、恢复、创建只读账号及盘点脚本调用 Docker 容器,使用容器内路径,仅适用于 2022 PoC。2008 R2 在虚拟机中按以下步骤操作,不直接套用容器脚本。
|
||||
|
||||
1. 在来宾系统中复制原始备份,核对文件哈希;由测试环境管理员通过数据库管理工具执行备份验证并读取文件列表,记录实际数据/日志逻辑名。
|
||||
2. 恢复到新的测试数据库,在恢复界面为每个逻辑文件指定来宾磁盘路径;确认目标库不存在,不覆盖其他数据库。
|
||||
3. 恢复后执行完整数据库一致性检查并留档;记录版本、排序规则和恢复数据库名。
|
||||
4. 创建仅用于该测试库的登录和用户,授予读取权限,核对角色、显式授权和禁止写入的结果。恢复/建账号使用管理员,应用始终使用专用只读账号。
|
||||
5. 对照 `db/queries/tplus/` 的九类实体查询和关联核验查询盘点结构、行数及汇总金额;保留差异原因。报告不包含凭据或个人明细。
|
||||
|
||||
## 5. 同版本验收清单(逐项通过后关闭 PENDING)
|
||||
|
||||
| # | 验收项 | 通过标准 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 版本确认 | `SELECT @@VERSION` 返回 10.50.x,与源备份主版本一致 |
|
||||
| 2 | 驱动连接 | 后端配置指向该实例,pyodbc + ODBC Driver 17 连接成功 |
|
||||
| 1 | 版本确认 | `SERVERPROPERTY('ProductVersion')` 为 10.50.1600(末尾 .0 等价);其他 10.50 补丁版本单独记录,不能冒充基准通过 |
|
||||
| 2 | 驱动连接 | 从实际运行后端的电脑连接成功,记录驱动名称、版本与加密/证书设置 |
|
||||
| 3 | 只读权限 | 用只读账号执行六类查询成功;执行任何写操作被拒绝 |
|
||||
| 4 | 六类查询 | 人员、客户、产品、订单、发货、收付款九个白名单 SQL 全部返回,行数与 Docker 副本一致 |
|
||||
| 5 | 中文数据 | 姓名、客户名、产品名等中文字段读取无乱码(只读账号仅验证读取) |
|
||||
| 6 | 日期字段 | 凭证日期、审核日期、到账日期等候选字段格式与空值表现正常 |
|
||||
| 7 | 金额精度 | 金额字段以 Decimal 读取,小数位与源数据一致 |
|
||||
| 8 | 断连超时 | 人为断网/停实例,同步错误进入 `tplus_sync_errors`,不静默丢数据 |
|
||||
| 8 | 断连超时 | 仅在隔离测试实例模拟断连;批次不得误报成功,批次失败或行级错误可追溯,恢复后可重试 |
|
||||
| 9 | 重复同步 | 同一备份重复 staging,相同行哈希全部去重跳过,行数不变 |
|
||||
| 10 | 关联核验 | `relationship_audit` 数量统计与 Docker 副本结果一致 |
|
||||
|
||||
@ -54,10 +58,11 @@
|
||||
|
||||
- 验收通过后产出报告(含上表逐项结果与 `@@VERSION` 输出),归档至 `.ai/archive/ai-aux/`;项目文档更新结论为「2008 R2 验收通过」。
|
||||
- 同步更新 `.ai/AI_CONTEXT.md` 与 `.ai/PROJECT_PROGRESS.md`。
|
||||
- 验收中发现的字段差异回到 `docs/待确认事项清单_2026-10-28_v2.xlsx` 对应编号,不直接改代码。
|
||||
- `check_compatibility.py` 默认核对 10.50.1600,复用应用连接策略;它的脚本检查通过不能替代本表中的断连、重复同步及完整权限验收。
|
||||
- 验收中发现的字段差异回到 `docs/客户待确认事项表_2026-10-28_客户版.xlsx` 对应编号,不直接改代码。
|
||||
|
||||
## 7. 明确边界
|
||||
|
||||
- 本环境仅验收 T+ 源库只读访问;工资系统生产部署(确认单 B4)另行确认。
|
||||
- 本环境仅验收 T+ 源库只读访问;工资系统生产部署(客户版 E02)另行确认。
|
||||
- 客户生产 T+ 原库不升级、不改动;2022 副本不回迁。
|
||||
- 时间增量(确认单 #13)不在本手册范围内,待 rowversion 验证方案另行评审。
|
||||
|
||||
40
docs/T+交付部署设计_2026-10-28.md
Normal file
40
docs/T+交付部署设计_2026-10-28.md
Normal file
@ -0,0 +1,40 @@
|
||||
# T+ 交付部署设计(2026-10-28)
|
||||
|
||||
状态:设计稿。本文约定后续 D1/D2 的实现方向;本轮没有安装服务、创建计划任务或发送告警。
|
||||
|
||||
## 托管方案
|
||||
|
||||
| 方案 | 适用方式 | 本项目取舍 |
|
||||
| --- | --- | --- |
|
||||
| Windows 任务计划程序 | 启动时运行现有 worker,设置失败重启和禁止重复实例 | 首期推荐;复用现有 CLI,不增加第三方服务包装依赖 |
|
||||
| NSSM | 将长驻进程包装为 Windows 服务 | 如客户要求服务管理器统一管理,再评审安装来源与版本;本轮不引入 |
|
||||
| 原生 Windows 服务包装 | 开发服务生命周期、停止信号和恢复逻辑 | 当前缺少此实现,首期维护成本较高,暂不选用 |
|
||||
|
||||
开发环境先围绕备份源 `10.50.1600` 验证连接;工资后端继续使用现有开发主机与 Python 环境。数据库来宾 Windows 的版本不能用来推断工资部署主机的版本。生产主机、服务账号与目录由 客户版 E02 回填后确定。
|
||||
|
||||
后续托管任务采用启动触发,工作目录指向部署包 `backend/`,直接运行部署虚拟环境中的 `python.exe -m app.cli tplus-worker --watch`。不要在托管任务启动时执行依赖升级。账号仅授予所需目录、日志和私有配置访问权限;凭据不作为任务命令行参数。
|
||||
|
||||
建议恢复参数:进程异常退出后间隔 1 分钟重启,最多 3 次;同一任务已有实例时忽略重复启动;持续运行的 worker 不设通用执行时长上限。三次失败后转人工处理。以上是设计默认值,未写入客户环境。
|
||||
|
||||
## 每周排队与重试
|
||||
|
||||
拟新增单独的排队命令,调用现有 `task_service.create_task`。每周一 02:00(Asia/Shanghai)排队一次全量暂存,时间待业务方确认;尚未验收的时间增量保持关闭。worker 仍只消费队列,两者职责分开。
|
||||
|
||||
排队命令按“源账套 + ISO 周 + 任务类型”生成稳定 UUID 请求键,重复触发复用同一任务;排队时若同账套有活动任务,先记录本周待补状态,结束后再补排,避免队列堆积。停机后补跑一次,不逐周补发过期任务。
|
||||
|
||||
业务任务失败与进程退出分别处理。暂定仅对可确认的断连、连接超时重试,间隔 5 分钟、15 分钟,最多 2 次;每次调用现有重试接口,保留父批次。配置错误、版本不符、字段缺失、类型未确认及 PARTIAL 转人工处理。当前错误码可能合并为 `EXECUTION_ERROR`,D2 实现时先补可测试的错误分类,不能据此宽泛自动重试。
|
||||
|
||||
## 告警与观察
|
||||
|
||||
管理员同步页面与脱敏日志作为首期查看入口。拟对 FAILED/PARTIAL、连续 3 次进程重启失败、QUEUED 超过 15 分钟以及超过 8 天没有成功同步提示异常。运行时长阈值需在同版本首轮全量耗时实测后确定。
|
||||
|
||||
外部邮件或企业消息通道、接收人、网络出口待 客户版 E02 与客户答复,当前不配置也不发送。通知内容限批次号、状态、时间、错误码和处理入口,不包含凭据、手机号或原始 JSON。相同根批次和错误码 30 分钟内合并通知,恢复成功发一次恢复通知;投递失败保留记录,不改变同步批次结果。
|
||||
|
||||
## 实施与验收顺序
|
||||
|
||||
1. 先完成备份源同版本 A3;确认业务规则并完成 C2–C5 后,再启用正式托管和自动排队。开发阶段可用隔离数据库模拟调度行为。
|
||||
2. D1 验证重启后启动、异常退出恢复、重复触发单实例、受限账号读配置、停止后不领取新任务、升级与回退均能重新取得锁。
|
||||
3. D2 验证重复周任务、停机补跑、断连重试次数、PARTIAL 人工处理、通知去重与投递失败。不得用模拟成功代替真实同版本验收。
|
||||
4. 发布时先手动单批运行并对账,再启用周任务。异常时停用排队与 worker,保留批次和原始数据,回退应用版本;不清空业务数据。
|
||||
|
||||
待填内容:客户版 E01 的生产连接、账号和版本差异,E02 的部署主机、网络、同步时段和通知方式,E03 的同版本介质,E04 的工资验收例单,以及 S1 的生产敏感字段策略。没有这些答复时继续文档与隔离开发验证,不虚构生产配置。
|
||||
@ -1,6 +1,6 @@
|
||||
# T+ 数据导入设计:staging → 工资业务表(2026-10-28)
|
||||
|
||||
> 状态:设计稿,未实现。客户确认单(`docs/待确认事项清单_2026-10-28_v2.xlsx`)13 项业务口径闭环前,不启动本文档对应代码开发。文中「预留参数」按编号挂到系统设置模块,客户答复后先更新清单、再回填参数、最后实现导入。
|
||||
> 状态:设计稿,未实现。客户确认单(`docs/客户待确认事项表_2026-10-28_客户版.xlsx`)业务口径答复齐全并完成设计评审后,再实现导入。「预留参数」在本稿中表示待确认决策,不代表已建系统设置项;字段映射、流程规则、部署配置须分别回填。
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
@ -55,6 +55,8 @@ staging PENDING
|
||||
|
||||
## 7. 预留参数清单(等待客户答复)
|
||||
|
||||
本节保留旧编号供开发追溯。当前对外文件为《客户待确认事项表_2026-10-28_客户版.xlsx》,旧表已按用户决定删除;回填时按第 10 节对应关系查找,不重复询问已经确定的规则。
|
||||
|
||||
| 编号 | 预留参数 | 等待内容 |
|
||||
| --- | --- | --- |
|
||||
| #1 | 成本取值 | 订单 LatestCost 与销货 costPrice/costAmount 的取舍(含税 L1 一并确认) |
|
||||
@ -70,9 +72,11 @@ staging PENDING
|
||||
| #11 | 产品口径 | 单位换算、价格类型、计提分类维护方 |
|
||||
| #12 | 收款方式 | 结算方式枚举与以物抵顶差异 |
|
||||
| #13 | 初始化与增量 | 起始日期、历史已结算款识别、rowversion 验证 |
|
||||
| L2–L5 | 二次分配三项 | 追认后回填分摊参数 |
|
||||
| L2 | 特殊款项 | 预收、保证金、代收,与 #3/#6 一并确认 |
|
||||
| L3–L5 | 二次分配三项 | 追认后回填分配标的、余额归属、协助关系 |
|
||||
| L7 | 三级角色职权 | 页面验收时核对 |
|
||||
| B1–B5 | 环境接入与介质 | 主机地址、只读账号、版本号、部署条件、合法安装介质(写到部署配置,不进代码) |
|
||||
| B1–B5 | 环境接入与介质 | 先用本地备份恢复环境,版本基准 10.50.1600;生产信息后补,介质仍需落实,凭据不入文档和 Git |
|
||||
| S1 | 敏感字段策略 | 当前开发阶段暂接受未单独加密的风险;生产保留、保护和备份范围仍待确认 |
|
||||
|
||||
## 8. 字段核验备注
|
||||
|
||||
@ -85,6 +89,60 @@ staging PENDING
|
||||
|
||||
## 9. 实现触发条件
|
||||
|
||||
1. 确认单 13 项 + 材料 5 项 + L1–L7 全部闭环(检查点 A)。
|
||||
1. 当前客户版的业务答复足以确定导入规则,并完成对应设计评审(检查点 A);生产地址等资料可在正式联调前补齐。
|
||||
2. 本设计按答复修订并评审定稿。
|
||||
3. 先实现主数据导入并通过可回滚验收(检查点 B),再进入交易数据与负向事件。
|
||||
|
||||
开发设计与隔离测试不等待生产地址/账号。备份源同版本环境仍须实际安装和验收;正式上线须补齐生产环境信息并完成 S1 决策。
|
||||
|
||||
## 10. 口径答复回填清单
|
||||
|
||||
所有答复先写入当前客户版 F 列,记录答复人、日期、例单或来源,再修订本稿。技术字段存在只能证明可取数,不能代替业务方选择计提口径。旧表问题已归并为下表编号,旧编号仅供内部追溯。
|
||||
|
||||
| 原编号 | 当前客户版编号 |
|
||||
| --- | --- |
|
||||
| #1、L1 | B02 成本取法、B01 含税口径 |
|
||||
| #2、#3 | T01 发货日、T02 到账日 |
|
||||
| #4、#5 | T03 回款归属与跨单分摊 |
|
||||
| #6、L2 | B08 特殊款项 |
|
||||
| #7、#8、#12 | T04 状态、T05 负向事件、T06 抵顶方式 |
|
||||
| #9、#10、#11 | T07 人员、T08 客户、T09 产品 |
|
||||
| #13 | B07 启用与历史承接;增量发现属于开发设计 |
|
||||
| L3–L5 | B03 分配规则;修改窗口另见 B04 |
|
||||
| L6、L7 | B05 经理本人业务、B06 岗位权限 |
|
||||
| B1/M1、B2/M2、B3/M3 | E01 生产连接,E03 备份源版本验收 |
|
||||
| B4/M4、B5/M5 | E02 部署与同步,E03 同版本介质 |
|
||||
| S1 | S1 敏感字段策略 |
|
||||
|
||||
客户版 E04 另列实际工资结果核对:先单笔、再个人与部门月结,复用已提供例单并逐笔解释差异。它是正式交付验收项,不能由技术测试代替。
|
||||
|
||||
| 编号 | 设计回填位置与核验重点 | 定稿后的实现顺序 |
|
||||
| --- | --- | --- |
|
||||
| #1 / L1 | 销售额、成本字段,含税口径;用成本调整例单核对 | C3 明细映射与金额校验 |
|
||||
| #2 | 发货日期及缺失处理,核对 SendDate 实际存在性 | C3 发货导入 |
|
||||
| #3 / L2 | 到账日期与预收转正式回款;arrivalDate 字典为到期日,不默认作到账日 | C3 回款导入 |
|
||||
| #4 | 无订单关联数据的人工补录或排除规则 | C3 关联异常处理 |
|
||||
| #5 | 多单分摊、金额合计、尾差规则 | C3 分摊与对账 |
|
||||
| #6 / L2 | 期初、保证金、代收和历史已核算款的纳入范围 | C3 回款过滤 |
|
||||
| #7 | 生效、核销、作废的真实状态对应关系 | C3 有效性校验 |
|
||||
| #8 | 原单追溯、冲减月份、已结算调整和防重复 | C4 负向事件 |
|
||||
| #9 | 人员工号、范围、经理关系,保护人工岗位字段 | C2 人员导入 |
|
||||
| #10 | 客户筛选、改名合并和源 ID 追溯 | C2 客户导入 |
|
||||
| #11 | 单位换算、价格、指导价与计提分类维护方 | C2 产品导入 |
|
||||
| #12 | 结算方式枚举与以物抵顶的核算差异 | C3 回款方式 |
|
||||
| #13 | 初始化起点、历史核算识别、变更/作废/删除发现 | C5 增量方案,验证前不启用时间水位 |
|
||||
| L3 | 二次分配标的与现有拆行实现的差异 | 校准已有分配逻辑并回归 |
|
||||
| L4 | 不足 100% 的余额归属与留存方式 | 与 L3 一并校准 |
|
||||
| L5 | 协助仅记录、不计提的追认 | 核对独立二次分配机制 |
|
||||
| L6 | 经理个人户客户确认结果 | 核对生效日期后调整既有参数并复核例单 |
|
||||
| L7 | 角色名称、人员和可操作项 | 权限配置及页面验收 |
|
||||
| B1/M1、B2/M2 | 开发先用本地只读连接;生产地址和凭据单独交接 | 部署前配置及最小权限检查 |
|
||||
| B3/M3 | 先按备份源 10.50.1600;生产完整版本后补 | A3 基准验收,再验证生产差异 |
|
||||
| B4/M4 | 开发现有主机先行;生产系统和网络后补 | D1/D2 托管与告警配置 |
|
||||
| B5/M5 | 合法介质来源、版本与可用性 | A2 安装后进入 A3 |
|
||||
| S1 | 人员表、暂存表和备份的保护、访问、保留期 | 全库策略确认后另行实施,上线前验收 |
|
||||
|
||||
- [ ] 核对每项答复与原始附件,冲突仍标待确认,不填猜测默认值。
|
||||
- [ ] 补齐源字段到目标字段、空值处理、人工字段保护和幂等映射,评审后定稿。
|
||||
- [ ] 按 C2 → C3 → C4/C5 顺序实施,先定点测试与对账,再做隔离回归。
|
||||
- [ ] 业务清单、设计、实现和验收结论同步更新;未答复项保留原编号。
|
||||
|
||||
64
docs/T+本地恢复与同步开发手册.md
Normal file
64
docs/T+本地恢复与同步开发手册.md
Normal file
@ -0,0 +1,64 @@
|
||||
# T+ 本地恢复与同步开发手册
|
||||
|
||||
日期:2026-10-28。当前程序把 T+ 原始数据写入 MySQL 暂存表,管理员可排队、查看结果和手动重试;工资业务表导入、自动周任务和告警投递尚未实现。
|
||||
|
||||
## 环境与版本
|
||||
|
||||
按用户本轮决定,先以备份源 SQL Server 2008 R2 `10.50.1600` 搭建开发验收环境。开发连接使用本地恢复实例的地址、端口和专用只读账号;客户生产信息到位后另行回填。现有 SQL Server 2022 恢复副本可继续用于开发,但不能证明备份源同版本兼容。安装和恢复步骤见 [2008 R2 测试环境](SQLServer2008R2测试环境.md)。
|
||||
|
||||
`scripts/tplus/inspect_sqlserver_backup.ps1`、`restore_sqlserver_backup.ps1`、`create_tplus_readonly_login.ps1` 使用 Docker 容器路径,只用于现有 2022 PoC。不能把这些脚本原样用于 Windows 虚拟机。已有虚拟机不重复运行创建脚本。
|
||||
|
||||
## 连接配置
|
||||
|
||||
在 `backend/` 运行应用与 worker,两者使用同一套 MySQL 和 T+ 账套配置。参考 `backend/.env.example` 填写本机私有配置:
|
||||
|
||||
| 配置项 | 用途 |
|
||||
| --- | --- |
|
||||
| `T_PLUS_ENABLED` | 默认 false;准备好只读数据源后显式启用 |
|
||||
| `T_PLUS_CONNECTION_FILE` | 项目外的本地 JSON 文件路径;管理页面不接收此路径 |
|
||||
| `T_PLUS_HOST/PORT/DATABASE/USER/PASSWORD/ODBC_DRIVER` | 未使用连接文件时填写;驱动名称必须与运行后端的电脑上已安装、实测过的驱动一致 |
|
||||
| `T_PLUS_ENCRYPT` | 默认 true;连接失败不会自动退回明文 |
|
||||
| `T_PLUS_TRUST_SERVER_CERTIFICATE` | 默认 true,适用于当前自签证书开发配置;不校验证书身份,部署可信证书后应设为 false |
|
||||
| `T_PLUS_CONNECT_TIMEOUT/QUERY_TIMEOUT` | 默认 10/120 秒;查询超时范围 1–600 秒 |
|
||||
| `T_PLUS_QUERY_DIR` | 留空使用仓库 `db/queries/tplus/`;部署时须交付查询文件 |
|
||||
|
||||
连接文件只接受 `host`、`port`(整数)、`database`、`user`、`password`、`driver` 六个键,最大 16 KB;拒绝 `sa` 和额外配置项。文件覆盖源库连接字段,TLS 和超时仍取应用配置。连接文件、真实密码与完整连接串不入 Git、不贴到确认单或报告。
|
||||
|
||||
默认加密可能使旧驱动或未配置 TLS 的实例连接失败。先核对驱动与服务端能力;确需临时回退时,仅在隔离开发环境显式设置 `T_PLUS_ENCRYPT=false`,记录原因、范围和复测结果。此次变更只更新代码默认值与模板,既有私有配置不会自动被覆盖。真实 10.50.1600 的 TLS 兼容仍待实测。
|
||||
|
||||
## 启动与排队
|
||||
|
||||
依赖安装使用项目锁文件;已有环境无需重新安装。需要 T+ 驱动依赖时,在 `backend/` 执行 `uv sync --extra tplus`,ODBC 系统驱动另按实际环境准备。
|
||||
|
||||
```powershell
|
||||
# 在 backend/ 目录执行,先完成本机私有配置
|
||||
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
# 另开终端,在 backend/ 目录消费已排队任务
|
||||
uv run python -m app.cli tplus-worker --watch
|
||||
# 单次消费队列中的一个任务
|
||||
uv run python -m app.cli tplus-worker --once
|
||||
```
|
||||
|
||||
管理员在 `/system/tplus` 创建读取或关联核验任务。HTTP 只入队,worker 负责执行;启动 worker 本身不会创建定时任务。`--connection-file` 可显式指定本地连接文件,后端仍需配置相同账套。
|
||||
|
||||
```powershell
|
||||
# 直接暂存一个实体;不会写工资业务表
|
||||
uv run python -m app.cli tplus-stage --entity personnel
|
||||
# 只读兼容检查;报告写入忽略目录,默认基准 10.50.1600
|
||||
uv run python ../scripts/tplus/check_compatibility.py --connection-file "D:\work\app\tplus-sqlserver\reader.local.json" --report "../.ai/tplus-compatibility.json"
|
||||
```
|
||||
|
||||
兼容脚本与应用共用配置入口。退出码 0 表示脚本覆盖的检查通过,2 表示不通过;完整验收还需权限审计、断连、重跑和对账。只有经确认需要验证其他补丁版本时才使用 `--expected-version`,报告中的版本不能冒充默认基准通过。
|
||||
|
||||
## 常见问题
|
||||
|
||||
| 现象 | 处理 |
|
||||
| --- | --- |
|
||||
| 页面显示未配置 | 核对开关、连接文件权限、只读账号与驱动;配置就绪不等于连通 |
|
||||
| 批次停在 QUEUED | 核对 worker 是否运行,以及它与后端的 MySQL 配置是否一致 |
|
||||
| 批次 FAILED/PARTIAL | 查看脱敏错误码;排除原因后在页面重试,保留原批次,勿直接改终态 |
|
||||
| 进程中断后留有 RUNNING | worker 下次取得执行锁后恢复中断状态;确认原因后重试 |
|
||||
| 加密连接失败 | 按上文核对 TLS;程序不自动关闭加密 |
|
||||
| 版本检查不通过 | 核对报告实际版本;2022 或其他补丁版本都不算 10.50.1600 同版本通过 |
|
||||
|
||||
当前暂存 JSON 可能包含手机号。本轮按用户批准的计划暂不加密或掩码,已在确认清单 S1 记录开发阶段风险接受;这不代表客户已批准生产保留。数据库访问、备份范围和全库敏感字段策略仍需统一确认。
|
||||
BIN
docs/客户待确认事项表_2026-10-28_客户版.xlsx
Normal file
BIN
docs/客户待确认事项表_2026-10-28_客户版.xlsx
Normal file
Binary file not shown.
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
"""只读核验真实 SQL Server 2008 R2;2022 PoC 不能获得兼容通过结果。"""
|
||||
"""只读核验备份源同版本 SQL Server;2022 PoC 不能获得兼容通过结果。"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
@ -8,41 +8,36 @@ from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pyodbc
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "backend"))
|
||||
|
||||
from sqlalchemy import URL, create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.core.tplus_db import resolve_tplus_settings
|
||||
from app.services.tplus.mapping import DETAIL_MAPPINGS, TPLUS_MAPPINGS
|
||||
from app.services.tplus.reader import TPlusReader
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""检查版本、中文、金额、日期和写权限,结果不包含凭据或客户记录。"""
|
||||
parser = argparse.ArgumentParser(description="SQL Server 2008 R2 兼容性验收")
|
||||
parser.add_argument("--connection-file", required=True, type=Path)
|
||||
parser.add_argument("--report", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
config = json.loads(args.connection_file.read_text(encoding="utf-8-sig"))
|
||||
connection_string = (
|
||||
"DRIVER={"
|
||||
+ config["driver"]
|
||||
+ "};SERVER="
|
||||
+ config["host"]
|
||||
+ ","
|
||||
+ str(config["port"])
|
||||
+ ";DATABASE="
|
||||
+ config["database"]
|
||||
+ ";UID="
|
||||
+ config["user"]
|
||||
+ ";PWD={"
|
||||
+ config["password"].replace("}", "}}")
|
||||
+ "};"
|
||||
def compatibility_settings(path: Path) -> Settings:
|
||||
"""复用应用的连接文件校验、URL 转义和 TLS 配置,不修改全局设置。"""
|
||||
settings = get_settings().model_copy(
|
||||
update={"t_plus_enabled": True, "t_plus_connection_file": str(path)}
|
||||
)
|
||||
with pyodbc.connect(connection_string, timeout=10) as connection:
|
||||
connection.timeout = 15
|
||||
cursor = connection.cursor()
|
||||
return resolve_tplus_settings(settings)
|
||||
|
||||
|
||||
def _version_matches(actual: str, expected: str) -> bool:
|
||||
def parts(value):
|
||||
values = value.split(".")
|
||||
return tuple(values + ["0"] * (4 - len(values)))
|
||||
|
||||
return parts(actual) == parts(expected)
|
||||
|
||||
|
||||
def _inspect_connection(connection, expected_version: str) -> dict:
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
version = str(
|
||||
cursor.execute(
|
||||
"SELECT CAST(SERVERPROPERTY('ProductVersion') AS varchar(80))"
|
||||
@ -57,45 +52,70 @@ def main() -> int:
|
||||
"HAS_PERMS_BY_NAME(DB_NAME(),'DATABASE','UPDATE'), "
|
||||
"HAS_PERMS_BY_NAME(DB_NAME(),'DATABASE','DELETE')"
|
||||
).fetchone()
|
||||
checks = {
|
||||
"sqlserver_2008r2": version.startswith("10.50."),
|
||||
"unicode_roundtrip": values[0] == "中文核验",
|
||||
"decimal_exact": values[1] == Decimal("12345.6789"),
|
||||
"datetime_exact": values[2] == datetime(2008, 1, 2, 3, 4, 5),
|
||||
"no_dml_permissions": all(x == 0 for x in permissions),
|
||||
}
|
||||
report = {
|
||||
"server_version": version,
|
||||
"driver_name": connection.getinfo(pyodbc.SQL_DRIVER_NAME),
|
||||
"driver_version": connection.getinfo(pyodbc.SQL_DRIVER_VER),
|
||||
"checks": checks,
|
||||
"compatible": all(checks.values()),
|
||||
}
|
||||
url = URL.create(
|
||||
"mssql+pyodbc",
|
||||
username=config["user"],
|
||||
password=config["password"],
|
||||
host=config["host"],
|
||||
port=config["port"],
|
||||
database=config["database"],
|
||||
query={"driver": config["driver"]},
|
||||
)
|
||||
engine = create_engine(url, hide_parameters=True)
|
||||
adapter_checks = {}
|
||||
try:
|
||||
with Session(engine) as session:
|
||||
reader = TPlusReader(session)
|
||||
for mapping in (*TPLUS_MAPPINGS, *DETAIL_MAPPINGS):
|
||||
try:
|
||||
reader.fetch_entity(mapping.entity_type, max_rows=1)
|
||||
adapter_checks[mapping.entity_type] = True
|
||||
except Exception as exc:
|
||||
adapter_checks[mapping.entity_type] = False
|
||||
report.setdefault("errors", {})[mapping.entity_type] = type(exc).__name__
|
||||
finally:
|
||||
engine.dispose()
|
||||
report["adapter_queries"] = adapter_checks
|
||||
report["compatible"] = report["compatible"] and all(adapter_checks.values())
|
||||
cursor.close()
|
||||
checks = {
|
||||
"sqlserver_2008r2": version.startswith("10.50."),
|
||||
"backup_source_version": _version_matches(version, expected_version),
|
||||
"unicode_roundtrip": values[0] == "中文核验",
|
||||
"decimal_exact": values[1] == Decimal("12345.6789"),
|
||||
"datetime_exact": values[2] == datetime(2008, 1, 2, 3, 4, 5),
|
||||
"no_dml_permissions": all(x == 0 for x in permissions),
|
||||
}
|
||||
return {
|
||||
"server_version": version,
|
||||
"expected_version": expected_version,
|
||||
"driver_name": connection.getinfo(pyodbc.SQL_DRIVER_NAME),
|
||||
"driver_version": connection.getinfo(pyodbc.SQL_DRIVER_VER),
|
||||
"checks": checks,
|
||||
"compatible": all(checks.values()),
|
||||
}
|
||||
|
||||
|
||||
def _inspect_adapter(engine, report: dict) -> None:
|
||||
checks = {}
|
||||
with Session(engine) as session:
|
||||
reader = TPlusReader(session)
|
||||
for mapping in (*TPLUS_MAPPINGS, *DETAIL_MAPPINGS):
|
||||
try:
|
||||
reader.fetch_entity(mapping.entity_type, max_rows=1)
|
||||
checks[mapping.entity_type] = True
|
||||
except Exception as exc:
|
||||
session.rollback()
|
||||
checks[mapping.entity_type] = False
|
||||
report.setdefault("errors", {})[mapping.entity_type] = type(exc).__name__
|
||||
report["adapter_queries"] = checks
|
||||
report["compatible"] = report["compatible"] and all(checks.values())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""检查版本、中文、金额、日期和写权限,报告不包含凭据或客户记录。"""
|
||||
parser = argparse.ArgumentParser(description="SQL Server 2008 R2 兼容性验收")
|
||||
parser.add_argument("--connection-file", required=True, type=Path)
|
||||
parser.add_argument("--report", required=True, type=Path)
|
||||
parser.add_argument("--expected-version", default="10.50.1600", help="备份源版本基准")
|
||||
args = parser.parse_args()
|
||||
engine = None
|
||||
try:
|
||||
settings = compatibility_settings(args.connection_file)
|
||||
engine = create_engine(
|
||||
settings.t_plus_database_url,
|
||||
hide_parameters=True,
|
||||
connect_args={"timeout": settings.t_plus_connect_timeout},
|
||||
)
|
||||
connection = engine.raw_connection()
|
||||
try:
|
||||
connection.driver_connection.timeout = settings.t_plus_query_timeout
|
||||
report = _inspect_connection(connection, args.expected_version)
|
||||
finally:
|
||||
connection.close()
|
||||
_inspect_adapter(engine, report)
|
||||
except Exception as exc:
|
||||
# 驱动异常可能包含完整连接串,报告仅保留类型。
|
||||
report = {"compatible": False, "error_type": type(exc).__name__}
|
||||
finally:
|
||||
if engine is not None:
|
||||
engine.dispose()
|
||||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(report, ensure_ascii=False))
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# 下一阶段开发计划:T+ 数据导入与同版本验收
|
||||
# 下一阶段开发计划:评审收口、交付设计与同版本验收
|
||||
|
||||
日期:2026-10-28。基于关联核验与同步管理轮次完成后的实际情况制定;上一轮计划与任务清单已归档至 `tasks/archive/2026-10-27-tplus-sync-*.md`。
|
||||
|
||||
@ -6,30 +6,43 @@
|
||||
|
||||
- 同步管理已完成:批次/异常/重试/锁/页面验收通过;staging 累计 933,203 行,六类工资业务表尚未接入写入。
|
||||
- 口径进展:澄清清单 24 项答复基本到齐;对照表 v2 已裁决经理个人户(#7)、协助 25%(#6/#15)、信息员(#9)、企业户 3‰(#20)等冲突。9.8 方案中蓝色字体标注的原方案算法作废,毛利润率按不含运费口径执行。
|
||||
- 仍未确认:含税取数、预收/保证金/代收、二次分配三项、三级角色职权,连同 T+ 口径 13 项与环境接入 5 项,已整理于 `docs/待确认事项清单_2026-10-28_v2.xlsx`(L1–L7)。
|
||||
- 当前确认入口:`docs/客户待确认事项表_2026-10-28_客户版.xlsx`。业务与对接问题按 B01–B08、T01–T09 归并,环境及验收为 E01–E04,本轮增加 S1;F 列保留客户答复。旧 v2 表按用户决定删除,旧编号对应关系见 C1 第 10 节。
|
||||
- 经理个人户:微信沟通确定按 9.8 方案执行、不计个人户部分,客户群确认中;系统参数已预留,确认后调整参数即可,无需开发。
|
||||
- 环境差距:合法 Windows 与 SQL Server 2008 R2 安装介质未到位,D 盘虚拟机仅有外壳;当前分支约 65 个文件未提交。
|
||||
- 工程收口:原有迁移、后端、前端与文档已拆分提交并推送;此前完整回归 990 项通过。
|
||||
- 环境基准:按用户最新决定,先以备份源 SQL Server 2008 R2 10.50.1600 推进开发验收,生产地址、账号和部署信息后补。合法介质与真实同版本实测仍待完成。
|
||||
|
||||
## 目标与顺序
|
||||
|
||||
先收口工程,再并行推进同版本环境验收与客户口径确认;口径闭环后实施 staging 到工资业务表的导入,最后交付托管与部署。
|
||||
先完成 P0.5 连接修复、运行手册与安全决策记录,再补 D1/D2 设计和 C1 回填清单。当前继续本地开发与隔离测试;业务答复回收后按清单更新设计,再实施导入。生产环境信息不阻塞本阶段设计工作。
|
||||
|
||||
### P0 工程收口(无外部依赖,立即执行)
|
||||
### P0 工程收口(已完成)
|
||||
|
||||
1. 将本轮 T+ 同步管理成果按迁移、后端服务、前端页面、文档拆分为规范提交;合入前本地构建与测试通过。
|
||||
|
||||
### P0.5 评审收口(本轮)
|
||||
|
||||
- 兼容性脚本复用应用的连接文件校验与 SQLAlchemy URL,消除手工连接串拼接;应用 URL 已有方言转义,不重复造一套转义器。
|
||||
- 默认启用加密;隔离开发环境仅允许显式回退并记录原因,不自动降级、不代填私有配置。真实旧版 TLS 兼容留待 A3。
|
||||
- 补齐 `docs/T+本地恢复与同步开发手册.md`,区分容器恢复脚本与 Windows 来宾恢复步骤。
|
||||
- 确认表新增 S1:用户已同意当前开发阶段暂不单独加密或掩码,生产保护、保留期与备份访问范围仍待统一确认;本轮不改敏感数据。
|
||||
- 定点验证连接特殊字符、默认 TLS、显式回退、错误脱敏和备份源版本判定,再在隔离 MySQL/Redis 执行完整回归。通过后分代码与文档提交推送。
|
||||
|
||||
本轮验证结果:19 项定点、1001 项完整回归与 Ruff 通过;临时测试容器及卷已清理。确认表保留原业务答复与引文,环境材料与 S1 已更新;D1/D2 为设计交付,实际托管和告警未启用。
|
||||
|
||||
### 轨道 A:同版本验收环境(外部依赖:安装介质)
|
||||
|
||||
- A1 先行细化 `docs/SQLServer2008R2测试环境.md`:装机步骤、恢复步骤,以及驱动、只读权限、六类查询、中文/日期/金额、断连超时、重复同步的验收清单。
|
||||
- A2 取得合法介质后,在 `D:\work\app\tplus-vm` 完成系统与数据库安装。
|
||||
- A2 取得合法介质后,在 `D:\work\app\tplus-vm` 完成备份源 10.50.1600 环境安装;当前不是已完成状态。
|
||||
- A3 恢复原始备份并执行完整验收,产出报告,关闭同版本兼容 PENDING 状态。
|
||||
|
||||
### 轨道 B:业务口径确认(外部依赖:客户填写)
|
||||
|
||||
- B1 发送 v2 清单:客户填「客户确认单-精简版」,IT/服务商填材料项;L1(含税取数)、L2(预收等)与确认单 #1/#3/#6 一并答复,避免重复提问。
|
||||
- B1 后续发送当前客户版:客户在 F 列填写业务答复,IT/服务商补充 E01–E03 的资料;同一例单可复用,已确认规则不重复提问。
|
||||
- B2 跟进客户群对经理个人户的反馈,回填 L6。
|
||||
- B3 答复回收后更新字段映射文档与暂定口径记录:二次分配三项(L3–L5)追认、三级角色职权(L7)在页面验收时核对。
|
||||
|
||||
任务 B1–B3 是工作编号;当前客户版使用 B01/T01/E01 等编号,C1 中的旧编号仅供追溯。发送确认单是后续人工事项,本轮不代发消息。
|
||||
|
||||
### 轨道 C:T+ 数据导入开发(依赖轨道 B 闭环;设计可先行)
|
||||
|
||||
- C1 先行起草「staging → 业务表写入」设计:表映射、金额精度(金额用 Decimal、比例和日斜率用 Fraction)、幂等与重算授权留痕、人工字段保护(回款/发货批次由系统人工指定,业务归属变更不回溯历史)、事务边界;明确标注等待确认的差异点,口径闭环后评审定稿。
|
||||
@ -40,7 +53,8 @@
|
||||
|
||||
### 轨道 D:交付收尾(依赖 A3 与轨道 C 完成)
|
||||
|
||||
- D1 `tplus-worker` 服务托管(Windows 服务方案)。
|
||||
- 本轮前置产出 `docs/T+交付部署设计_2026-10-28.md`:首期推荐任务计划程序,明确排队、进程恢复和业务重试的区别;周任务、阈值为设计默认,外部告警通道待确认。
|
||||
- D1 `tplus-worker` 托管(首期采用 Windows 任务计划程序)。
|
||||
- D2 每周同步任务编排与失败告警。
|
||||
- D3 客户内网独立部署与最终验收。
|
||||
|
||||
@ -50,6 +64,8 @@
|
||||
- 导入保护人工字段;每笔回款只核算一次;重算须授权并留痕。
|
||||
- 2008 R2 验收未完成前,2022 恢复副本的结果不作为同版本兼容证据。
|
||||
- 每个任务先完成定点测试再进入依赖任务;回归使用隔离的 MySQL/Redis。
|
||||
- C1 尾部按 #1–#13、L1–L7、B1–B5/M1–M5、S1 列出答复回填顺序。业务答复、字段存在、同版本实测和生产部署分别验收,不能互相代替。
|
||||
- S1 的暂缓加密是用户批准的当前开发阶段例外;生产敏感字段策略仍是上线前待确认项。
|
||||
|
||||
## 项目后续路线
|
||||
|
||||
|
||||
@ -6,14 +6,23 @@
|
||||
- [x] 拆分提交本轮 T+ 同步管理成果(迁移、后端、前端、文档);提交前 Ruff、单元测试、前端类型检查与构建通过。
|
||||
依赖:无。
|
||||
|
||||
## P0.5 评审收口与交付设计
|
||||
- [x] 兼容脚本复用应用 URL 与连接文件校验,补特殊字符转义回归。
|
||||
- [x] 默认启用 TLS,模板说明显式回退、自签证书限制和私有配置迁移。
|
||||
- [x] 补本地恢复与同步开发手册,修正 A1 的容器/虚拟机步骤。
|
||||
- [x] 在现有客户版新增 S1,环境材料说明先用本地备份源同版本、生产信息后补;按用户决定保留旧 v2 删除结果。
|
||||
- [x] 产出 D1/D2 托管、每周排队、重试与告警设计稿。
|
||||
- [x] C1 补全每项预留决策的回填顺序。
|
||||
- [x] 完成隔离全量回归(1001 项)、Ruff、文档链接与表格保留校验;分代码和文档提交交付。
|
||||
|
||||
## 轨道 A:同版本验收环境
|
||||
- [x] A1 细化 2008 R2 测试环境与验收手册(无介质可先做)。
|
||||
- [ ] A2 取得合法介质并完成虚拟机安装。
|
||||
- [ ] A2 取得合法介质并安装备份源 10.50.1600 同版本环境。
|
||||
- [ ] A3 恢复原始备份并执行完整验收,产出报告。
|
||||
依赖:A2 等介质到位;A3 依赖 A2。
|
||||
|
||||
## 轨道 B:业务口径确认
|
||||
- [ ] B1 发送 `docs/待确认事项清单_2026-10-28_v2.xlsx` 并跟进填写。
|
||||
- [ ] B1 发送 `docs/客户待确认事项表_2026-10-28_客户版.xlsx` 并跟进填写。
|
||||
- [ ] B2 回填经理个人户客户群反馈结果(L6)。
|
||||
- [ ] B3 答复回收后更新映射与暂定口径记录。
|
||||
依赖:无;B1 立即可发。
|
||||
@ -31,8 +40,10 @@
|
||||
- [ ] D2 每周同步任务与失败告警。
|
||||
- [ ] D3 客户内网部署验收。
|
||||
依赖:A3 与轨道 C 完成。
|
||||
说明:D1/D2 设计稿已产出,托管、周任务和外部告警尚未启用;生产主机条件与通道待确认。
|
||||
|
||||
## 检查点
|
||||
- [ ] 检查点 A:口径确认全部闭环(确认单 13 项 + 材料 5 项 + L1–L7)。
|
||||
- [ ] 检查点 A:客户版业务答复完成并回填 C1(B01–B08、T01–T09);E01–E04 按环境与正式验收节点补齐。
|
||||
- [ ] 检查点 B:主数据导入通过,业务表首次写入可回滚。
|
||||
- [ ] 检查点 C:全量导入与 2008 R2 验收通过,进入轨道 D。
|
||||
- [ ] 上线前确认 S1 的全库敏感字段保护、保留期与备份访问策略。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user