# Handoff: a7-parity 执行票 07→12 串行实施(2026-09-07) > 注:OS 临时目录不可写(沙箱限制),按 .scratch 临时区惯例落盘于此。 ## 会话状态 - effort:`.scratch/a7-parity/`(蓝湖 A7 后台管理原型-代码对齐) - **Map 决策阶段已收官**:票 01-06 全 resolved,拍板存档见 `issues/06-gap-verdict.md` + `gap-report.md` - 用户指令:**串行执行执行票 07→12**(每票 claim→实现→resolve;最后票 12 收官验收) - **票 07 已 claim、侦察完毕、设计定案、尚未写一行代码**——本文档核心就是把侦察成果完整传递 - Todo:t07 IN_PROGRESS;t08-t12 PENDING ## 不重复的既有素材(直接引用,勿重读) | 素材 | 路径 | |---|---| | 票面 07-12 | `.scratch/a7-parity/issues/07-a71-a72-light-endpoints.md` ~ `12-bruno-regen-baseline.md` | | 四张盘点矩阵 | 同目录 `matrix-a7-1-a7-2.md` / `matrix-a7-3-upper.md` / `matrix-a7-3-lower.md` / `matrix-a7-4-a7-5.md` | | 缺口总报告 | `gap-report.md`(缺接口 9 组/语义差异 6/tag 12 处) | | Map(含全部拍板行) | `map.md` | | 原型文本库 | `.scratch/opportunity-bugfix/lanhu-pages/A7-*.md`(34 文件) | | 票 08 素材 | matrix-a7-3-upper §推送规则;参照实现=`crm-rule` 的 OpportunitySchemeCardTemplate*(版本化 8 端点同构) | | 票 09 素材 | matrix-a7-3-lower §查重;原型 `A7-3-2-4-1_查重规则(详情页).md`(602 行 4 步骤向导) | | 票 10 素材 | 原型 `A7-3-4_项目规则.md`(264 行 5 区块);LeadPool 现状=crm-rule LeadPoolController + PoolMemberStore | | 票 11 素材 | tag 8 处清单在票面;种子参照 `crm-dict/.../config/DictPermissionInitializer.java`(permission-seeder seam 先例) | | AGENTS.md 纪律 | BOM 剥离脚本 / 孤立单大写字母 @Column 钉名 / ADR-0017 表单绑定 | ## 全局拍板(票 06 已定,勿再问用户) ① P0 全建,重接口(同步任务 5 端点、全局日志 5 端点+AOP)记账不建;② 新规则 Controller 全进 crm-rule;③ 日志形态 A;④ 数据共享不纳入。次级默认值:资源删除语义按原型改(级联→禁删)、LeadPool 部门成员=显式成员表 DEPT_MEMBER、查重「执行次数」=引用次数、幻影 3 端点保留挂运维 tag。全程不提交 git(后端仓+文档仓 E:\code\crm-api-docs 均留用户审查)。 ## 票 07 侦察结论(本会话独有,重读代价高,务必信任) ### 代码事实 - **crm-auth/pom.xml**:依赖 crm-base+crm-file;easyexcel 4.0.3 在 crm-base(compile scope 可传递);StringRedisTemplate 直接注入有先例(SessionStoreImpl/CorpAccessTokenProviderImpl) - **SystemController**(156 行):`/api/system` 下已有 menus/tree、depts/tree|save|delete、users/assign-roles(单用户全量替换,@RequestParam userId+roleIds 逗号串)、users/assign-depts、users/page、users/stats、sync/dingtalk、users/admin-role;底部有 `splitIds(String)` 私有工具 - **RoleController**(80 行):`/api/roles` page/saveOrUpdate/detail/assign-resources/delete;@PreAuthorize 用 `hasAuthority('crm:role:*')`;tag「A7 后台管理/权限管理/角色管理」✅ 不用动 - **ResourceController**(55 行):`/api/resources` list/saveOrUpdate/delete/icon/upload;**类级 @PreAuthorize("hasRole('ADMIN')")**;4 个端点 tag 均为「A7 后台管理/权限点管理」→ 待补「权限管理」一级 - **SysRole 实体**:仅 roleName/roleCode/sort/remark/builtin——**缺 roleType/status 两列** - **SysMenu 实体**:status 列已存在(varchar(16) default 'enabled'),但只有 button 类型写值;**catalog/menu 存量行 status=null → 一律视为 enabled** - **AuthUser 实体**:username/mobile/email/avatar/enabled/lastLoginTime/deptId/employmentStatus/title/account——筛选列全齐,无需 DDL - **UserPageParam**:deptId/employmentStatus/loginFrom/loginTo(+BaseParam keyword/current/size/orderBy/asc) - **RoleParam**:空壳(仅继承 BaseParam) - **UserStatsDTO**:@AllArgsConstructor **4 参**(deptCount/activeCount/resignedCount/unassignedCount)——加 3 字段后须同步改 UserQueryServiceImpl.stats() 的**两处 new 调用**(约 144 行空集分支、157 行正常分支) - **UserQueryServiceImpl**(216 行):pageUserList 的 scopedUserIds 子树预筛→wrapper→批量拼装部门/角色名模式照抄扩展;stats() 计数模式 - **AuthUserServiceImpl**(168 行):assignRoles=先删后插全量替换;**configureAdminRoles=增量先例**(查已有行,仅插缺失/仅删命中)——批量增量照此模式 - **SysRoleServiceImpl**(192 行):pageRoles/saveRole(builtin 保护+code 唯一)/saveRoleWithScopes/getRoleDetail/assignResources(祖先补全)/deleteRoleCascade(builtin 保护+级联三表)——deleteRoleCascade 需**补「在职使用人数=0 才可删」校验**(矩阵三#7) - **ResourceServiceImpl**(296 行):**delete 现状=子节点检查+级联清理 sys_role_menu(103-127 行,要改写)**;save 校验链完整;applyToEntity 只对 button 写 status - **RoleScopeStore**:load(roleId)/save(roleId, moduleScopes)/delete(roleId)——copy 角色复制数据范围档位用它 - **IDtOrgSyncService**:只有 syncOrg();**lastSyncTime 无任何现成存储**→定案见下 - **AuthConstants 错误码 61xxx**:已用至 61018(61009 是 ResourceServiceImpl 私有常量 CODE_RESOURCE_INVALID)→新码从 **61019** 起 - **权限码**:crm:role:list/detail/save/delete/assign 已由 DataInitializer 种子化(ButtonSeed×5)。**定案:新角色端点复用现有码**(stats→crm:role:list;copy/enable/disable→crm:role:save),不改 DataInitializer - **跨域 seam 先例**:PermissionSeeder 接口在 **crm-base.service**、各业务模块实现 → UserTransferPort 同位放 crm-base.service - **三域 owner 字段**:Lead.ownerUserId(pool_id/dept_id/owner_user_id 三层)、Customer.ownerUserId(+owner_dept_id)、Opportunity.ownerUserId(+owner_dept_id+owner_name_snapshot);**「项目」域=同表 oppStatus=5(已转项目)** - **crm-opportunity 已有 E9 移交 transition**(CAS,但只允许状态 2/3)→ **定案:port 实现直接批量 update 直改 owner(低频管理动作,覆盖所有状态),域内轨迹不做、依赖 sys_user_transfer_log 全局快照**(轻票定位,Answer 记录) - 业务模块全部依赖 crm-auth → port 实现可注入 AuthUserMapper(拿接收人 deptId,**owner_dept_id 跟人走**,Opportunity 注释明示「移交跟人走」)与 ISysDeptService - **导出无先例**(全仓无 export 端点);UserListDTO 有嵌套 List 不能直接喂 EasyExcel ### 设计定案(写码时照此执行) 1. **批量分配角色=增量语义**:⚠️ 票面写「多用户全量替换」与原型 u296 原文「提交后为 2 名员工增加"销售人员"角色;**不会覆盖其已有角色**,也不会修改钉钉资料」矛盾——**按原型增量实现**(对每个 userId 查已有 roleId 集,仅插缺失行;configureAdminRoles 同款),在票 07 Answer 记录此偏差及依据 2. **UserTransferPort**(crm-base 新建):`String domain()` / `long countOwned(Long userId)` / `int transferOwned(Long fromUserId, Long toUserId)`;4 个实现:crm-lead×1(lead)、crm-customer×1(customer)、crm-opportunity×2(opportunity=oppStatus!=5 / project=oppStatus=5) 3. **sys_user_transfer_log 新表**(SysUserTransferLog 实体+Mapper):fromUserId/toUserId/domainsJson(各域计数快照)/operatorUserId/transferTime;命名过孤立单大写检查(from_user_id/domains_json ✅) 4. **UserTransferService(crm-auth 新)**:注入 `List`(ObjectProvider 防空);`preview(userId)`→List(domain/domainName/count);`transfer(userId, receiverId)`→校验接收人存在在职启用+≠userId→逐 port transferOwned→写 transfer log(事务) 5. **角色 status/roleType**:SysRole 加 `roleType varchar(20)`(存 code:SYS/MANAGE/BIZ/FUNC,服务层校验 4 值)+ `status varchar(16) default 'enabled'`(对齐 SysMenu 先例);role_type ✅ 无命名坑 6. **copyRole**:复制 roleType/remark/功能权限(sys_role_menu 全套)/数据范围(RoleScopeStore.load→save);**不复制人员**;生成 **status=disabled 停用草稿**;roleCode 服务端生成(原码+后缀或时间戳,唯一即可) 7. **enableRole 校验**(原型 u163-u186):≥1 个 menu 类型授权(resourceIds 含 menu 节点)+ 四域(lead/opportunity/customer/project)数据范围档位齐全,否则报错;disableRole 直改 status 8. **userCount(在职使用人数)**:批量算——page 出参对当页 roleIds:selectList(sys_user_role in roleIds)→userIds→AuthUserMapper selectList(id in, employment_status='active')→按 role 归组计数;detail 同步填 9. **RoleStatsDTO**:roleCount/enabledCount/disabledCount/zeroUsageCount(active 使用人数=0 的角色数) 10. **资源 enable/disable/references**:disable 时查 sys_role_menu 中引用该资源且**角色 status=enabled** 的行>0→禁止停用(61020);references(id)→List(roleId/roleName/roleStatus/activeUserCount);enable/disable 后 apiPermissionCache.invalidate() 11. **资源 delete 语义改写**(拍板项):删掉级联清理 sys_role_menu 代码;校验链=①无子节点②**仅 status=disabled 可删**(null 视为 enabled)③**有任何 sys_role_menu 引用→禁止删除并提示引用方**;发现现网调用方依赖级联语义则停下报票 06 复议(票面原文) 12. **lastSyncTime**:IDtOrgSyncService 加 `LocalDateTime getLastSyncTime()`;impl 注入 StringRedisTemplate,syncOrg() 成功尾部写 key `crm:auth:sync:last-sync-time`(ISO 字符串),读时反解析;注释挂账「任务化时迁表」 13. **stats 三时间戳**:UserStatsDTO +lastSyncTime(来自 Redis)/lastTransferTime/lastTransferUser(查 sys_user_transfer_log 最新一条,transferUser=接收人名) 14. **导出**:新建 UserExportRow 平铺 DTO(@ExcelProperty:姓名/账号/职务/手机号/主部门/兼职部门逗号串/在职状态/角色逗号串/最后登录时间);EasyExcel 写 ByteArrayOutputStream→`FileApi.upload(bytes, "用户列表-yyyyMMddHHmmss.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "system")`→返回 `Result`,前端拿 fileId 走既有下载端点 15. **UserPageParam 4 筛选落地**:username/mobile/title=like;roleId=先 selectList(sys_user_role where role_id in)拿 userIds→wrapper.in(id, userIds)(空集短路返空页,同 scopedUserIds 模式) 16. **tag 修正(票 07 范围 5 处)**:ResourceController 4 处→「A7 后台管理/权限管理/权限点管理」;sync/dingtalk→「A7 后台管理/用户管理/同步任务」;**幻影 3 项**:depts/save、depts/delete 的 tag 从「部门管理」改挂「A7 后台管理/运维」,users/admin-role 已是「运维」不动;depts/tree 票面未列→保留「部门管理」不动 17. **新错误码**:61019 移交参数非法 / 61020 资源被启用角色引用禁操作 / 61021 角色启用校验不过(enable 校验失败) ### 待写文件清单(票 07 剩余工作) **新建**:crm-base `service/UserTransferPort.java`;crm-auth `domain/entity/SysUserTransferLog.java`、`mapper/SysUserTransferLogMapper.java`、`domain/dto/TransferDomainCountDTO.java`、`domain/dto/UserTransferResultDTO.java`(可并入 Service 返回 Map 或简化)、`domain/dto/RoleStatsDTO.java`、`domain/dto/ResourceReferenceDTO.java`、`domain/dto/UserExportRow.java`、`service/UserTransferService.java` + `service/impl/UserTransferServiceImpl.java` **修改**:UserPageParam(+4 筛选)、UserStatsDTO(+3 字段)、IUserQueryService/UserQueryServiceImpl(筛选落地+stats 时间戳+改两处 new)、IAuthUserService/AuthUserServiceImpl(assignRolesBatch 增量)、SysRole(+roleType/status)、RoleParam(+roleType/status)、RoleDTO(+roleType/status/userCount,fromEntity/toEntity 补映射)、ISysRoleService/SysRoleServiceImpl(roleStats/copyRole/enableRole/disableRole+delete 补人数校验+pageRoles 补筛选+userCount 批量)、IResourceService/ResourceServiceImpl(enable/disable/references+delete 改语义)、IDtOrgSyncService/DtOrgSyncServiceImpl(lastSyncTime)、AuthConstants(+61019/61020/61021)、SystemController(+4 端点+tag)、RoleController(+4 端点)、ResourceController(+3 端点+4 tag)、业务模块 4 个 port 实现(crm-lead×1/crm-customer×1/crm-opportunity×2,放各自 config 或 service 包,@Component 注册) ### 票 07 验收与收尾 - 全仓 *.java BOM 扫描 0(AGENTS.md 有 PowerShell 剥离脚本);`mvn compile` 过;单测全绿(若 crm-auth 既有测试受 SysRole 新列/构造签名变化影响需同步修) - SearchReplace 票 07:Status→resolved + Answer(含增量语义偏差记录、级联→禁删已改、记账项);map.md Decisions 追加票 07 行;TodoWrite t07 COMPLETE - 票 08→12 依次推进;票 12 收官时跑 bruno-sync 全量重生成(文档仓 E:\code\crm-api-docs,bare collection 布局)+对账双向零差+基线验收 ## 环境注意(踩过的坑) - PowerShell 环境:`python -c` 内联与脚本执行不可靠(ExitCode 1);文件名清单用 `Get-ChildItem | Out-File -Encoding utf8`;代码检索一律用 Grep/Glob/Read 工具(ripgrep,可靠) - mvn 编译若报 `illegal character: '\uFEFF'` = 文件带 BOM,按 AGENTS.md 脚本剥离 - 大段 CJK Markdown 编辑走 Write 新文件或小步 SearchReplace,避免超大单块替换 - 每票收工时对照 AGENTS.md「新模块接口接入 checklist」(URL 扁平动词/深 tag/bruno 配置) ## Suggested skills - **implement**(票 08-12 大块实现时可直接按票面+本文档定案实施,无需再侦察) - **bruno-sync**(票 12 收官必用:全量重生成+对账) - 无需 grilling(拍板已全部完成,遇到票面与原型冲突按原型+Answer 记录偏差处理;若发现现网依赖级联删除语义则停下报票 06 复议)