# -*- coding: utf-8 -*- """票 01 收尾落盘:票据翻 resolved + Answer 存证;map.md Decisions 补行。 二进制读写保留原行尾,写后校验 BOM/锚点/关键标记。""" import io, sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') # ---------- 1) ticket 01: Status: open -> resolved + Answer ---------- ticket = r'.scratch/a7-heavy/issues/01-sync-task-module.md' raw = open(ticket, 'rb').read() assert not raw.startswith(b'\xef\xbb\xbf'), 'ticket has BOM' nl = b'\r\n' if b'\r\n' in raw else b'\n' assert raw.count(b'Status: open') == 1, 'status anchor count=%d' % raw.count(b'Status: open') raw = raw.replace(b'Status: open', b'Status: resolved', 1) ANSWER = """ ## Answer 2026-09-08 落地(resolved)。10 项需求全部实现,验收基线全过:`mvn test -pl crm-auth -am` 全量单测 EXIT=0、BOM 扫描 0、编译过。 **实现清单** - 表/实体:`dt_sync_task`(任务编号 `SYNC-MMdd-序号`/触发方式/执行状态/起止时间/部门·人员·异常三计数/进度/失败明细 JSON)+ `dt_sync_settings` 单例行(启停/每日执行时间/重试间隔·次数/并发/最近修改人·时间)。 - 端点 5 个(`SyncTaskController`,全 ADMIN,深 tag「A7 后台管理/用户管理/同步任务」):POST /sync/dingtalk(任务化)、POST /sync/tasks/page、GET /sync/tasks/detail?id=、GET /sync/settings、POST /sync/settings。 - 服务:`DtSyncTaskServiceImpl`(launch 并发占位→61023、单线程 daemon 池 dt-sync-task 异步执行、进度 5/45/90/100、settings 边界校验→61022、page 默认 startTime 倒序)+ `DtSyncScheduleJob`(@Scheduled 每分钟心跳,由 dt_sync_settings 派生判定发起,触发方式=auto)。 - **组织拉取 SPI(执行中用户新增拍板)**:`OrgSourcePort` 接口(fetchDepartments/fetchDeptUsers + 嵌套 Dept/User 值对象)+ `DingTalkOrgSource` 实现(SDK/HTTP/token 失效重试/解析/限流全迁入,DtErrorException 翻译为业务异常)+ 装配缝 `crm.auth.org-source`(缺省 dingtalk;后续接企微/自有 OA 新增实现类标注 @ConditionalOnProperty 即可);编排层 `DtOrgSyncServiceImpl` 平台无关化、零 SDK 依赖。 - lastSyncTime:`UserStatsDTO.lastSyncTime` 改由 dt_sync_task 最新成功行 endTime 推导。 - 错误码 61022 同步设置参数非法 / 61023 已有同步任务运行中 / 61024 同步任务不存在(README 已登记);权限种子同步任务菜单 @Order(25)。 - 测试:`DtSyncTaskServiceH2Test`(11 用例)/ `DtOrgSyncServiceImplTest`(5 用例)/ `DingTalkOrgSourceTest`(4 用例)。 - bruno 文档 5 份 → 文档仓 `A7 后台管理/用户管理/同步任务/`(钉钉组织同步改型 / 同步任务分页 / 同步任务详情 / 读取同步设置 / 保存同步设置)。 **拍板项实际取舍与偏差** | 票面 | 实际 | | --- | --- | | ③ 返任务编号,计数改走 detail | 采纳并加强:返 `DtSyncLaunchDTO{taskId, taskNo}` 两者都给(前端轮询 detail 用 id) | | ④ tasks/page 改 POST | 已落实(表单绑定筛选,ADR-0017) | | ⑧ lastSyncTime 直接切换 | 采纳;Redis 兼容读取未保留(一期无存量调用方,同步成功即有任务行) | | 进度「已处理/总数」逐条粒度 | 未做,阶段粒度 5/45/90/100(listener 回报 45/90);原型无逐条强诉求 | | partial_failed 状态 | 枚举保留对齐原型文案;一期同步粒度失败即整体 all_failed,不产生该状态 | | 重试规则 | 仅落设置 + 边界校验(61022),自动重试执行未实现(票面 2/6 项未显式要求执行行为) | | 组织拉取解耦(用户新增拍板) | 见实现清单 SPI 条目:独立接口多实现,不写死钉钉 | """ ans = ANSWER.encode('utf-8').replace(b'\n', nl) if not raw.endswith(nl): raw += nl raw += ans open(ticket, 'wb').write(raw) raw2 = open(ticket, 'rb').read() assert b'Status: resolved' in raw2 and b'## Answer' in raw2 and b'OrgSourcePort' in raw2 assert not raw2.startswith(b'\xef\xbb\xbf') print('ticket01 OK bytes=%d' % len(raw2)) # ---------- 2) map.md: Decisions so far 追加一行 ---------- mpath = r'.scratch/a7-heavy/map.md' raw = open(mpath, 'rb').read() assert not raw.startswith(b'\xef\xbb\xbf'), 'map has BOM' nl = b'\r\n' if b'\r\n' in raw else b'\n' anchor = '执行票 01–05 开放。'.encode('utf-8') assert raw.count(anchor) == 1, 'map anchor count=%d' % raw.count(anchor) DECISION = ('- 2026-09-08 票 01 执行落地(resolved):同步任务 5 端点任务化全量实现——契约改型返 ' '`DtSyncLaunchDTO{taskId, taskNo}`、launch 并发占位 61023、单线程池异步、进度 5/45/90/100;' '**组织拉取抽离 `OrgSourcePort` SPI(执行中用户拍板)**:`DingTalkOrgSource` 实现 + 装配缝 ' '`crm.auth.org-source`(缺省 dingtalk),编排层 `DtOrgSyncServiceImpl` 平台无关化零 SDK 依赖,' '后续企微/自有 OA 新增实现类即可;lastSyncTime 直接切换 dt_sync_task 最新成功行(Redis 兼容读取未做);' '重试规则仅落设置+边界校验不实现自动重试;partial_failed 枚举保留但不产生;' '错误码 61022-61024、权限种子 @Order(25)、bruno 5 份落文档仓;验证 crm-auth 全量单测 EXIT=0 + BOM 0。').encode('utf-8') raw = raw.replace(anchor, anchor + nl + DECISION, 1) open(mpath, 'wb').write(raw) raw2 = open(mpath, 'rb').read() assert '票 01 执行落地'.encode('utf-8') in raw2 and not raw2.startswith(b'\xef\xbb\xbf') print('map.md OK bytes=%d' % len(raw2))