“项目重新编排与数据接入”是我最近一个月实际操作的一条完整链路,准确说是我独自维护的一套批处理系统改造记录。旧系统长期依赖人工跑脚本,每天凌晨靠 crontab 串起十几个任务,最头疼的就是上游数据一旦变化,后面所有环节全部白跑。这块工作的意义不在于把代码重写一遍,而在于把散落的任务状态统一起来,把数据接入从“能跑”变成“可重放、可排查、可监控”。如果你也在处理定时调度、外部数据同步、多个系统之间的流转协作,这篇记录应该能给你一些直接可用的思路。
从外部看,这只是一次日常项目重构;从实际内容看,它至少包含了调度建模、状态管理、数据接入规范、异常定位几个层面。这篇博客我按真实工作发生顺序来写,既有踩坑实录,也有最后的沉淀结果,希望能让同类项目的维护者少走几步弯路。
1. 为什么“重新编排”比“优化代码”更紧迫
1.1 旧流程到底乱在哪
我不是一开始就打算做“流程编排”这个概念的,真正推动我动手的是连续几周凌晨的告警电话。旧系统的任务依赖关系是写死在各种脚本里的,比如 A 脚本处理完供应商文件后,直接把数据塞到一张业务表,B 脚本靠 sleep 等几分钟再去读这张表。表面上看没人干预也能跑,但一旦 A 脚本因为上游文件格式变化失败,B 脚本照样会启动,它读取到上一批旧数据后照样生成报告,业务同事第二天早上拿到的很可能是一份带有昨日残留数据的错误结果。
这类问题归纳起来有三个明显毛病。
第一,任务之间没有清晰的中间产物和状态记录。处理到哪一步、成功没有、影响多少行,全部依赖日志文件里的 print 输出。日志一滚动,想定位某个环节的历史执行情况几乎靠猜。
第二,失败后的重试是裸奔式的。脚本挂了就重新执行一遍,但重新执行时不会判断之前有没有写进去半截数据,结果就是重复插入、数据翻倍。而这种翻倍因为当时缺少校验,可能要过一两天才被下游对账发现。
第三,数据接入环节是整个链路里最不可控的。上游发来的文件可能晚到,可能字段顺序调整,可能同一批数据传了两遍。这些情况如果不在入口堵住,后边再严谨的业务逻辑都会被脏数据带偏。
所以这次动手第一步不是去优化某个查询,而是先把“流程边界”画清楚。我把整条链路重新编排成四个独立阶段:数据接入、质量校验、业务处理、结果回写。只允许阶段之间通过可控的入库表和批次号衔接,不允许业务代码直接去读上游原始文件。
1.2 把“脚本串联”升级成“有状态的任务链”
在重新编排之前,我首先定了一个规矩:任何一环执行都必须能回答三个问题——现在跑到哪个环节、上一次为什么停、重新启动从哪里续跑。
传统写脚本的方式很难回答这些问题,所以我参考常见的任务调度思路,引入了一张非常简单的状态表。我用它记录每个批次的执行位置,状态字段只有 pending、running、success、failed、skipped 五种。每次跑批开始时写入一条记录,后面每一步更新这条记录。
这张表的设计很简单,但对整个项目的帮助非常大。它让“重新编排”这件事从逻辑层面落到了数据层面,我不用再通过 ps 命令到处看进程,SQL 一查就知道当天任务卡在哪一步。
sql复制CREATE TABLE pipeline_task (
task_id BIGINT AUTO_INCREMENT PRIMARY KEY,
pipeline_name VARCHAR(64) NOT NULL,
stage_name VARCHAR(64) NOT NULL,
biz_date VARCHAR(10) NOT NULL,
run_seq INT NOT NULL DEFAULT 1,
status VARCHAR(16) NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
context_json TEXT,
error_message TEXT,
started_at DATETIME,
finished_at DATETIME,
UNIQUE KEY uk_pipeline_stage (pipeline_name, stage_name, biz_date, run_seq)
);
context_json 字段是我特别留出来的,用来存每个任务执行时需要恢复的现场信息。比如数据接入阶段执行到一半网络中断,重新拉起时要知道已经处理到哪个文件、读到了哪个偏移量。把这些信息放到状态表里,比写在一个临时文件里靠谱得多,临时文件很容易被系统清理任务误删。
实际编写时我并没有引入一套重量级工作流引擎,而是自己维护了一个小的顺序推进逻辑。原因很现实:一是整条链路只有十几步,复杂度可控;二是重引擎会引入额外的部署和维护成本,一台机器就能跑的批处理没有必要做成分布式服务。顺序推进逻辑本身很简单,读取当天任务列表,按照配置的 stage 顺序执行,前一个 success 才执行下一个。
用这种“半自动编排”的方式,我获得了一个非常大的好处:排错和重跑不需要所有环节一起做。以前文件解析失败,修复后常常把已经完成的数据处理任务再跑一遍,既耗时又容易覆盖人工修正过的结果。现在可以直接把状态改成 pending 后从指定 stage 重跑,前面成功的环节不会再去碰。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据接入:上游数据进库前必须对齐的几件事
2.1 先对齐字段语义,再谈技术实现
数据接入在这个项目里占了相当大的工作量,但我发现真正难的不是写下载脚本或解析代码,而是和上游把口径对齐。以前我犯过一个错误,拿到对方提供的接口文档后,想当然认为“order_time”就是订单创建时间,结果数据接入完成后才发现那是订单最后修改时间,而且有时区标记,有时没有,直接导致下游统计全部失真。
这次我在接入前专门做了一份字段核对表,把所有要接的字段按业务含义、类型、取值样例、是否允许为空、更新频率逐一列出来,然后逐条和上游确认。以下是我使用的简版对齐表,你可以直接参考:
| 字段名 | 业务含义 | 上游类型 | 入库目标类型 | 允许为空 | 备注 |
|---|---|---|---|---|---|
| order_id | 订单唯一标识 | string(32) | varchar(32) | 否 | 全链路唯一键 |
| order_time | 订单创建时间 | datetime,东八区 | datetime | 否 | 统一按东八区存储 |
| amount | 订单金额 | 数值,单位元 | decimal(12,2) | 否 | 上游有个别超长字符串 |
| channel_code | 渠道编码 | string(8) | varchar(8) | 是 | 空值用空字符串,不填 NULL |
| raw_remark | 原始备注 | text | text | 是 | 长度可能超过 1000,不能用 varchar |
这张表最大的价值是让双方在接入前就把常识性差异暴露出来。比如上游的空值处理方式,有的地方写 NULL,有的地方会用空字符串,有的会用“N/A”这种业务占位符。这些不落到字面上,写解析代码时很容易只处理一种情况。
字段对齐之后还要确定“业务主键”和“文件批量标识”。我遇到过同一个订单在文件里出现多行的情况,如果没有唯一键约束,数据接入层就会放行重复数据,到最后数据处理阶段才会突然报唯一键冲突,那时再去排查是哪一层引入的重复,成本会高出很多。所以我在数据接入阶段就按业务主键做分组去重,异常重复的行直接写入错误队列。
2.2 不要直接把上游文件写入正式业务表
上一版系统最大的问题在于原始文件和正式表之间没有隔离区。上游文件一旦开始解析,解析出来的记录就直接修改业务表,中途失败的话,业务表里可能残留着一部分新数据,而另一部分还是旧数据,整个“半新半旧”的状态非常难清理。
这次我强制划分了三个区域:原始文件区、接入暂存区、正式业务区。
原始文件区按数据源名称和业务日期组织目录,每天一个文件夹,文件夹内存放原样收到的文件以及对应的 .done 标记文件和 md5 校验文件。这样做的目的不是存档,而是为了重复排查时有据可查。以后有人说“某一天的数据有问题”,我可以直接翻出那天的原始文件,看看它的真实格式和内容。
接入暂存区是一组中间表,命名规范上我统一加了 tmp_ 前缀,表结构比上游字段多出几个控制字段,包括批次号、文件路径、原始行号、接入时间。所有上游数据必须先落到这些暂存表,经过校验后,再通过一条明确的 INSERT ... SELECT 语句进入正式业务区。
这种方式虽然多了一步写表动作,但换来了两个非常关键的好处:第一,数据接入任务可以反复执行而不污染正式数据,如果解析失败,只需要清空当前批次对应的暂存表,重新读取即可;第二,正式业务表里永远只存在完整批次的数据,不存在半截数据,下游任务可以放心消费。
2.3 接入暂存表的时间分区设计
暂存表我统一按 biz_date 做分区,每天的数据进当天的分区。这样的设计在数据量上来之后非常重要。曾有一段时间我为了省事,把一个月的数据都放在同一张表里,跑了 20 天后发现即便加了索引,数据处理速度也明显下降。改成按天分区后,每次数据处理只需要扫描一个分区,速度能快一到两个数量级。
以下是一个典型的原始数据暂存表结构示例:
sql复制CREATE TABLE tmp_order_source (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
batch_id VARCHAR(64) NOT NULL,
src_line_no INT NOT NULL,
order_id VARCHAR(32) NOT NULL,
order_time DATETIME NOT NULL,
amount DECIMAL(12,2) NOT NULL,
channel_code VARCHAR(8) DEFAULT '',
raw_remark TEXT,
valid_flag TINYINT NOT NULL DEFAULT 1,
invalid_reason VARCHAR(512) DEFAULT NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
biz_date VARCHAR(10) NOT NULL,
KEY idx_biz_date_id (biz_date, order_id),
KEY idx_batch_id (batch_id)
) PARTITION BY RANGE COLUMNS(biz_date) (
PARTITION p20250420 VALUES LESS THAN ('2025-04-21'),
PARTITION p20250421 VALUES LESS THAN ('2025-04-22')
);
biz_date 在这里是业务日期,不是文件到达日期。这两个日期一定要区分清楚,否则跑到凌晨 0 点才收到的昨天数据,你会不知道该算在哪一天。这个区分是整个思路里容易被忽视但很重要的细节。
3. 数据接入全流程实现:一个完整批处理环节的拆分
3.1 总体流程设计
做完前面的准备后,我开始实现数据接入流程。这个流程在编排里是第一个 stage,整体结构如下:
- 检查上游文件是否完整,判断标志是目录下是否生成 .done 文件。
- 计算文件的 md5,和上一个批次比对,如果一致视为重复文件,直接跳过。
- 读取文件头部信息,和配置中的表头字段做一致性检查。
- 创建 import_batch 记录,进入暂存表写入阶段。
- 逐行解析,完成基础清洗后写入暂存表。
- 对暂存表数据做完整性校验,错误行更新 valid_flag 和 invalid_reason。
- 把校验通过的数据按业务主键统一合并进正式表。
- 更新 import_batch 状态,返回成功行数和失败行数。
第一步的 .done 文件检查是我踩过坑以后才加上的。之前上游文件传输方式是直接往共享目录放文件,没有时间戳也没有完成标志,我的程序可能在上游文件写入到一半时就开始读取,自然得到不完整的数据。后来我和上游约定好,文件写完后再生成一个同名的 .done 文件,我这边只处理带 .done 标记的批次,基本消除了半文件问题。
校验文件头这步也很有必要。前两周上游在文件里增加了一列备注信息,如果我没有检查表头,解析时就会因为列数不匹配直接报错,整个接入流程停在那里,而这类问题本可以在读取前通过一次字符串比对就发现。
3.2 数据接入主流程的代码骨架
整个流程我用 Python 实现,因为是单机批处理,代码结构上不需要很复杂的框架。下面这段代码是简化的核心流程,保留了我在实际项目里的关键判断逻辑。
python复制def run_daily_import(pipeline_meta, pipeline_ctx):
"""
pipeline_meta: 数据源配置,如源目录、目标表、业务日期等
pipeline_ctx: 编排上下文,里面包含 biz_date、run_seq
"""
biz_date = pipeline_ctx["biz_date"]
src_dir = pipeline_meta["src_dir"] + biz_date + "/"
# 1. 检查 .done 文件,防止读到未写完的文件
if not Path(src_dir + ".done").exists():
return {
"status": "skipped",
"reason": "upstream file not ready"
}
# 2. 计算文件 MD5,若和本日已完成批次重复,直接视为重复
file_path = src_dir + pipeline_meta["file_name"]
file_md5 = calc_md5(file_path)
if batch_already_success(pipeline_meta["source_name"], biz_date, file_md5):
return {"status": "success", "rows": 0}
# 3. 检查表头与配置是否一致
expected_headers = pipeline_meta["headers"]
actual_headers = read_first_line(file_path)
if actual_headers != expected_headers:
raise DataImportError(
f"header mismatch, expected={expected_headers}, actual={actual_headers}"
)
# 4. 创建批次记录并逐行写入暂存表
batch_id = create_import_batch(
source_name=pipeline_meta["source_name"],
biz_date=biz_date,
file_md5=file_md5,
file_name=pipeline_meta["file_name"],
)
valid_count = 0
try:
# 这里使用流式读取,避免一次性把大文件载入内存
with open_stream_and_parse(file_path) as reader:
for row in reader:
clean_row = clean_field(row, pipeline_meta["field_rules"])
insert_tmp_table(batch_id, clean_row, pipeline_meta["tmp_table"])
valid_count += 1
# 5. 数据完整性与唯一性校验
validate_tmp_data(batch_id, pipeline_meta)
# 6. 合并进正式表
merge_tmp_to_target(batch_id, pipeline_meta)
# 7. 更新状态
mark_batch_success(batch_id, valid_count)
return {"status": "success", "rows": valid_count}
except Exception as exc:
mark_batch_failed(batch_id, str(exc))
# 这里不直接吞异常,交给上层编排状态机处理重试
raise
有朋友可能会问 batch_already_success 为什么要加上 MD5 比较,而不直接查数据库有没有当天数据。
因为上游偶尔会在当天下午修正数据后重新发送同一个业务日期的文件,如果我不比较 MD5,系统就会认为当天数据已经接入完成,直接跳过修正文件。加了 MD5 之后,只有当文件的 MD5 完全相同才会判定为重复;如果同一天来了两个 MD5 不同的文件,我规定以后一份为最终版本,同时把前一份批次标记为 superseded,这样既能保证数据最新,不会无休止地让下游重复计算,也保留了历史的接入痕迹。
3.3 写入暂存表时的清洗细节
清洗逻辑并不复杂,但一定要覆盖实际数据里出现过的各种状况。我整理了字段清洗规则,放在一个 JSON 配置里,这样日常调整不需要改代码,只要改配置就能让不同数据源复用同一套接入程序。
json复制{
"source_name": "order",
"file_name": "order_detail_20250420.csv",
"headers": ["order_id", "order_time", "amount", "channel_code", "raw_remark"],
"field_rules": {
"order_id": {"type": "string", "required": true, "max_len": 32},
"order_time": {"type": "datetime", "required": true, "tz": "+08:00"},
"amount": {"type": "decimal", "required": true, "min": 0},
"channel_code": {"type": "string", "required": false, "default": ""},
"raw_remark": {"type": "string", "required": false, "max_len": 4000}
}
}
在清洗环节遇到不合规的数据时,大部分情况我选择把行记录下来并标记为 invalid 而不是直接抛异常中断整个文件。批处理项目里,上游一两行脏数据是比较常见的,如果因为一行坏数据让整批任务失败,会严重影响时效性。但是像表头对不上、文件编码错误、文件内容为空这类结构化问题,我会立刻中断,因为它说明整个文件都不可信,继续处理没有意义。
不同数据源的“可容忍错误率”也要单独配置。有的核心财务数据我会要求零容忍,只要有错误行就整批失败;有的日志分析数据可以容忍 1% 以内的错误行。我把这个开关也放到了配置里,避免每个数据源单独复制一套接入代码。
3.4 接入结果如何反馈给编排层
数据接入完成后,编排层不直接判断成功与否,而是查 import_batch 状态表。我把批次表和前面的 pipeline_task 表联合起来使用,pipeline_task 负责“流程跑到哪一步”,import_batch 负责“这次数据文件到底接得怎么样”。
import_batch 表的核心字段包括:batch_id、source_name、biz_date、file_name、file_md5、total_rows、valid_rows、error_rows、status、error_message。status 我用了几个固定值:pending、running、success、failed、superseded、duplicate。
这里面特意加了 superseded 状态。刚开始我并没有这个状态,遇到文件修正重推,直接把原来的成功记录改成 failed 然后重跑,结果历史报表里那一天的数据丢失了,事后复盘少了一个可追溯的中间状态。加了这个状态后,每天的数据接入历史就变得完整:第一版文件是成功的但被更新版替代了,第二版文件是当前生效版本。这些看似很小的状态位,在排查“当时为什么这样出数”时会给你省很多精力和时间。
编排层拿到状态后,会执行这段逻辑:如果 import 状态是 success,则继续跑下一个 stage;如果是 failed,则重试或停住等人工介入;如果是 duplicate,说明文件当天已经处理过,直接跳过;如果是 superseded,则把当前批次的数据处理任务整体重新执行一遍,而不是只接入新文件,以免正式表里出现新旧两个版本混在一起的情况。
4. 实际操作中遇到的坑与排查方法
4.1 排障实录:一桩桩问题怎么定位
整个改造过程中我遇到了不少问题,单凭印象记容易漏,所以我专门维护了一个“批处理踩坑速查表”。下面挑几个最典型的记录分享,这些问题基本覆盖了批处理数据接入会遇到的常见问题类型。
| 问题现象 | 真正的原因 | 定位方法 | 解决办法 |
|---|---|---|---|
| 每天入库量比预期多一倍 | 上游重复推送了同一天的文件,且文件 MD5 相同 | 对比 import_batch 中同一 biz_date 的 MD5 | 增加 MD5 去重,重复文件直接标记 duplicate |
| 偶尔出现凌晨任务白跑 | 文件还在传输过程中,读取到半截文件 | 查看原始文件大小和最终文件大小不一致 | 引入 .done 标记文件和文件大小等待机制 |
| 部分记录落地后成为 NULL | 上游用空字符串表示空值,解析时 split 后直接取值 | 抽样检查原始文件的脏行 | 清洗规则里对空字符串统一填充默认值 |
| 同一订单重复执行多次 | 正式表缺少唯一键约束,重复 merge 导致叠加 | 按 order_id 分组统计出现次数 | 在 merge 时使用 INSERT ... ON DUPLICATE KEY UPDATE |
| 跑批日期与源日期错位 | 上游字段带时区,没有统一转换成东八区 | 对比 order_time 的小时数,发现相差 8 个小时 | 字段对齐表里明确时区,并在解析时将 UTC 转换为本地时间 |
| 某一天接入突然全部报错 | 上游表头增加一列,列数不一致 | 查看报错日志,错误信息是“header mismatch” | 表头自动检测并发送变更告警,后续自动适配 |
这里我重点说一下时区问题。当时订单时间字段一直显示成 UTC,但因为大部分订单发生在白天,差 8 小时之后只是落在了凌晨,对日报影响不太明显,这个问题硬是拖了一个月才暴露。后来对账时发现当天单量始终少了一部分,排查才发现上游把时间转换逻辑做在了业务库层,而文件导出时忘记转换,导出结果是 UTC 时间。从那以后我把所有 datetime 字段的标准都定为“入库即东八区”,任何接入源必须在对接文档里写清楚自己的时区,否则默认按东八区处理,如果后续发现偏差再一起修正。
4.2 快速定位问题的一套标准流程
同类问题发生多了,我总结出一套简洁的问题定位顺序,打算分享给你。这套顺序看起来简单,但往往能让排查时间缩短一半。
第一,先查 pipeline_task 状态表,确认当前任务卡在哪个 stage。这一步能快速把范围缩小到“数据接入”还是“业务处理”。很多新手上来直接看日志,日志分布在十几个文件里,很容易淹没在细节里。
第二,如果是数据接入阶段失败,查 import_batch 表。看看文件是否真的读到了、校验通过多少行、错误信息是什么。此时基本能区分问题是出在上游文件本身,还是出在我的解析代码。
第三,去原始文件区检查文件是否完整,看一眼 .done 文件和文件修正时间。如果上传时间比任务启动时间晚,说明是上游延迟,不是程序 bug。
第四,再回头看日志。此时目标已经非常明确,直接搜 batch_id 或者订单号,比漫无目的翻日志高效得多。
这套流程跑顺以后,我对告警的恐惧感明显下降了。以前收到任务失败提醒,要花半小时定位问题;现在大概率五分钟内能定位到具体原因,剩下的事要么是等上游传文件,要么是修改某个配置后重跑。
4.3 幂等设计:避免“重试”变成“灾难”
重试机制是批处理系统非常重要的设计点,但也最容易埋雷。我的原则很简单:数据接入阶段至少要保证“重复执行同一批次”不会产生重复数据。
最简单也最有效的方法是利用暂存表和批次号。在数据接入写入暂存表时,每行都记录 batch_id。如果任务重跑,第一步就是检查该 batch_id 是否已经有数据。如果已经存在,就先删除该批次在暂存表中的记录,再重新写入。这样即使清洗逻辑执行到一半挂了,修复后重新执行,暂存表里也不会累积两套同批次的数据。
对于正式表的合并动作,我把它单独抽成一个方法,而且保证方法本身是幂等的。合并逻辑核心就是按业务主键判断记录是否存在,存在则更新,不存在则插入。使用 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE 能够很方便地实现这一点,关键是要提前在 order_id 上建立唯一索引。
注意:不要只在应用层先 SELECT 再判断是否 INSERT,批处理并发执行时容易产生竞态条件。数据库层面的唯一索引才是最终防线。
4.4 错误行隔离与人工处理机制
刚开始我的做法是只要有错误行,整个文件就失败,全部不落正式表。结果业务方经常因为一行脏数据拿不到当天数据,意见非常大。后来我改成“结构性错误整批失败,数据性错误行级隔离”,配合一个可视化的错误行查询页面,让业务可以快速看到哪些订单因为什么原因没进来。
被隔离的错误行并不删除,保留在暂存表里,valid_flag 置为 0,invalid_reason 写明异常原因。上游修正数据后,会重推一个文件,系统识别到新版文件后会把旧版文件对应的正式表数据回滚,再按新文件执行完整接入。整个过程里,错误行数据始终有记录,没有发生过“数据凭空消失”的争议。
错误原因尽量写得通俗且可操作,不要只抛一个 Python traceback。比如“金额字段格式错误:abc”比“invalid decimal literal”对业务同事友好得多。批处理系统最终是要和人协作的,错误信息写得好不好,直接影响每周要接多少个电话。
5. 数据接入规范化后,带给整个项目的改变
5.1 三个维度的提升
改造完成至今运行了大约一个半月,这套“重新编排 + 数据接入规范化”带来的收益主要体现在三个方面。
最直接的改变是“重跑不再心惊胆战”。以前每天凌晨失败一次,负责处理的人要花大量精力去确认哪些步骤已经执行过、哪些需要重跑。现在状态表里一目了然,该从哪一步开始执行,由编排逻辑决定,不需要人工在心里推算依赖关系。这在只有一个人维护、没有完整交接文档的情况下尤其重要。
第二个改变是“数据可追溯”。每一条入库数据都能关联到原始文件、批次号、解析时间和当时的校验结果。业务同事问“为什么今天这个订单金额不对”,我可以快速追溯到原始文件里的那行内容,而不是支支吾吾地从数据库里反查。数据接入的规范性让整个团队从“谁改的”的扯皮中走了出来。
第三个改变是“跨系统协作顺畅了”。上游字段发生变化时,系统会主动在接入阶段报错并提示差异,而不是一路跑到最后让业务报表出现异常。这种提前暴露问题的设计,让上游也能及时感知他们的变更会影响到哪些下游系统。
5.2 个人工作方式的变化
这次项目之后,我自己也沉淀了一套更适用的个人工作记录方法。以前我喜欢把每天做的事一股脑写在一个长文档里,感觉很努力,但复盘根本找不到重点。现在我只记录这几类问题:流程上哪里会卡住、数据接入阶段出现过哪些异常、哪个经验可以复用,以及哪些问题其实可以靠配置避免。
比如再把数据接入一个新数据源时,我会先列字段对齐表,再写解析代码,而不是先写代码再发现问题。这个次序看起来简单,却能省掉后续大量的沟通成本。每次上游说“接口文档更新了”,我要做的第一件事不是打开代码改字段,而是去改配置和字段对齐表,并顺带检查历史数据是否需要重新补录。这套个人工作记录方式已经成了我常用的标准化动作,也让我从整天救火的状态里慢慢解脱出来。
说到底,重新编排一个老项目并不需要用什么高深技术,真正需要的是先承认旧流程的混乱,再用一张状态表、一套接入规范,把混乱从过程中抽离出来。这个项目让我感触最深的一点是:代码层面能解决的问题往往不是最难的,最难的是没有统一的数据契约和流程状态认知。好在这些都可以通过一步步的梳理和记录逐渐改善,而这次数据接入改造,就是整个改善过程里迈出的最扎实的一步。
