generate-admin-coverage.mjs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #!/usr/bin/env node
  2. import { readFileSync, writeFileSync } from 'node:fs';
  3. import { resolve } from 'node:path';
  4. const root = resolve(import.meta.dirname, '..');
  5. const sourcePath = resolve(root, 'docs/migration/legacy-admin-inventory.generated.md');
  6. const outputPath = resolve(root, 'src/app/admin/legacy-admin-coverage.generated.ts');
  7. const checkOnly = process.argv.includes('--check');
  8. const explicit = new Map([
  9. ['User/GroupManage', ['implemented', '已由 Group 资源页与专用帐套组生命周期替代']],
  10. ['User/Group_API', ['implemented', '已覆盖帐套组列表与带引用保护的删除']],
  11. ['User/GroupAdd', ['implemented', '已由动态组表单与专用 saveGroup 替代']],
  12. ['User/GroupAdd_Submit', ['implemented', '已覆盖原子编号、父组校验与保存']],
  13. ['Content/ContentManage_Status', ['implemented', '已由 contentBatch 状态操作替代']],
  14. ['Content/ContentManage_Del', ['implemented', '已由 contentBatch 回收站操作替代']],
  15. ['Content/ContentMove', ['implemented', '已由内容详情与批量节点移动控件替代']],
  16. ['Content/ContentMove_Submit', ['implemented', '已由 contentBatch 帐套节点校验与移动替代']],
  17. ['Content/IHtmlToString', ['implemented', '已由 cms.content-normalizer 替代']],
  18. ['User/UserManage', ['partial', '已提供帐套用户列表、搜索、分页和批量操作;旧多条件筛选与导出仍待补齐']],
  19. ['User/UserAdd', ['partial', '已提供安全普通用户开户;旧扩展资料字段尚未全部复刻']],
  20. ['User/UserAdd_Submit', ['partial', '已覆盖开户、旧用户 ID 与默认账户初始化']],
  21. ['User/UserInfo', ['partial', '已提供动态详情、资料编辑、状态、分组和密码重置']],
  22. ['User/UserInfo_Submit', ['partial', '已覆盖安全字段更新;旧 UserBase 全字段仍待专用映射']],
  23. ['User/User_API', ['partial', '已覆盖锁定、解锁、移动分组;标注、归属管理员和手机归属地尚未迁移']],
  24. ['User/UserExp', ['partial', '现有账本可查询,管理员余额变更表单与事务规则尚未补齐']],
  25. ['Content/ContentManage', ['partial', '已提供帐套内容列表、搜索、分页、审核和批量操作']],
  26. ['Content/EditContent', ['partial', '可编辑安全主表字段;模型附表专用表单尚未全部生成']],
  27. ['Content/Content_Update', ['partial', '可更新安全主表字段;模型附表更新仅覆盖已迁移学习模型']],
  28. ['Content/ContentRecycle', ['partial', '可回收和恢复内容;独立回收站筛选页尚未提供']],
  29. ['Content/ContentManage_API', ['partial', '已覆盖恢复;永久删除、清空、专题移除和排序交换尚未迁移']],
  30. ['Content / Node/NodeManage', ['implemented', '已由 Node 资源列表、分页与专用结构操作替代']],
  31. ['Content / Node/NodeSearch', ['implemented', '已覆盖按栏目名称搜索并保持帐套隔离']],
  32. ['Content / Node/NodeAdd', ['implemented', '已由 Node 动态表单与父节点结构控件替代']],
  33. ['Content / Node/NodeAdd_Submit', ['implemented', '已覆盖原子栏目编号、同级防重、父节点与层级校验']],
  34. ['Content / Node/UnionNode_Move', ['implemented', '已由 nodeBatch 批量移动与递归深度同步替代']],
  35. ['Content / Node/NodeRecycle', ['partial', '已覆盖回收和恢复;独立回收站筛选页尚未提供']],
  36. ['Content / Node/SPageAdd', ['partial', '节点类型与外链字段可编辑;旧单页专用布局尚未逐字段复刻']],
  37. ['Content / Node/OutLinkAdd', ['partial', '节点类型与 URL 字段可编辑;旧外链专用布局尚未逐字段复刻']],
  38. ['Content / Node/Node_API', ['partial', '已覆盖回收、恢复与安全永久删除;清空全部节点被明确禁止']],
  39. ['Content / Node/BatchNode', ['partial', '已提供栏目批量回收、恢复和结构迁移']],
  40. ['Content / Node/BatchNode_Submit', ['partial', '已覆盖结构批处理;旧批量模板、权限与计费字段仍待专用实现']],
  41. ['Content / Node/UnionNode', ['partial', '已提供栏目批量迁移;栏目合并仍待实现']],
  42. ['Content / Node/Special_API', ['partial', '已覆盖删除引用保护与保存时名称/目录校验;旧独立校验响应未保留']],
  43. ['Content / Node/Special', ['implemented', '已由 Special 资源列表、搜索与分页替代']],
  44. ['Content / Node/SpecialAdd', ['implemented', '已由专题动态表单与父专题结构控件替代']],
  45. ['Content / Node/SpecialAdd_Save', ['implemented', '已覆盖原子专题编号、全帐套防重、父专题与循环校验']],
  46. ['Content / Node/UnionSpecial', ['partial', '已提供专题批量迁移;关系合并因目标数据缺失被显式阻塞']],
  47. ['Content / Node/UnionSpecial_Move', ['implemented', '已由 specialBatch 帐套隔离批量移动替代']],
  48. ['Content / Node/UnionSpecial_Merge', ['blocked-data', 'CommonModel.specialId 物理列缺失且商品主表未迁移,无法安全重写专题关系']],
  49. ['Model/Index', ['partial', '已由全部数据类与 Model/ModelField 资源页替代;旧模型类型入口布局未逐项复刻']],
  50. ['Model/ModelManage', ['implemented', '已提供帐套模型列表、搜索、分页与安全元数据编辑']],
  51. ['Model/ModelAdd', ['blocked-infrastructure', '新增模型需要 PostgreSQL 建表、生成标签与写入旧模板文件']],
  52. ['Model/ModelAdd_Submit', ['blocked-infrastructure', '托管云函数未开放模型 DDL 与旧 Zoomla 模板文件运行时']],
  53. ['Model/Model_API', ['partial', '已覆盖安全模板/展示元数据更新;复制和删除模型因 DDL 被阻塞']],
  54. ['Model/ModelData', ['partial', '规范化目标类可从资源页管理;任意旧附表名到 Parse 类的映射尚未完整恢复']],
  55. ['Model/Field', ['implemented', '已提供模型字段列表、搜索、分页与安全展示元数据编辑']],
  56. ['Model/Order_Submit', ['partial', '已提供同模型字段批量排序云函数;专用拖拽界面尚未复刻']],
  57. ['Model/FieldAdd', ['blocked-infrastructure', '新增字段需要 PostgreSQL ALTER TABLE 与旧字段控件模板']],
  58. ['Model/FieldAdd_Submit', ['blocked-infrastructure', '目标托管环境未开放任意物理列 DDL 与旧控件生成器']],
  59. ['Model/Field_API', ['partial', '已覆盖字段顺序更新;字段删除因历史数据与物理列 DDL 被阻塞']],
  60. ['Content / Guest/Default', ['implemented', '已由 Guestbook 资源页与后台导航替代']],
  61. ['Content / Guest/MsgList', ['implemented', '已提供帐套留言列表、标题搜索、分页与批量审核工作流']],
  62. ['Content / Guest/ReplyGuest', ['implemented', '已由留言详情中的管理员回复面板替代']],
  63. ['Content / Guest/EBtnSubmit', ['implemented', '已由 guestbookReply 原子分配回复 GID 并继承分类/父留言关系']],
  64. ['Content / Guest/MsgShow', ['implemented', '已由留言详情抽屉替代']],
  65. ['Content / Guest/MsgShow_Submit', ['implemented', '已由 saveGuestbook 白名单正文编辑替代']],
  66. ['Content / Guest/Msg_API', ['implemented', '已覆盖审核、取消审核、回收、恢复与带回复引用保护的永久删除']],
  67. ['Content / Guest/CateList', ['implemented', '已由 Guestcate 资源列表、搜索和分类结构抽屉替代']],
  68. ['Content / Guest/CateAdd', ['implemented', '已由留言分类专用表单替代']],
  69. ['Content / Guest/CateAdd_Submit', ['implemented', '已覆盖原子 cateid、留言类型锁定与名称防重']],
  70. ['Content / Guest/Cate_API', ['implemented', '已覆盖下级分类、留言与帖子引用保护的删除']],
  71. ['Content / Bar/Default', ['implemented', '已由 Guestcate 贴吧类型列表入口替代']],
  72. ['Content / Bar/CateList', ['implemented', '已提供贴吧分类列表、搜索、分页与父子结构编辑']],
  73. ['Content / Bar/CateAdd', ['partial', '已提供贴吧分类表单;旧版发帖频率文件配置尚未迁移']],
  74. ['Content / Bar/CateAdd_Submit', ['partial', '已覆盖原子编号、防重、父子循环和分类元数据;文件配置尚缺']],
  75. ['Content / Bar/Cate_API', ['partial', '已覆盖删除引用保护及推荐/取消推荐;清空版块帖子尚未开放']],
  76. ['Content / Bar/BarAuthSet', ['blocked-data', '目标 Schema 缺少 ZL_Guest_BarAuth 用户版块权限关系,无法读取逐用户浏览、发帖和回复权限']],
  77. ['Content / Bar/BarAuthSet_Submit', ['blocked-data', '目标 Schema 缺少 Uid、BarID、Look、Send、Reply 权限关系,无法安全保存旧授权']],
  78. ['Content / Bar/TieList', ['implemented', '已提供帐套帖子列表、标题搜索、分页与批量工作流']],
  79. ['Content / Bar/TieDetail', ['implemented', '已由只读帖子详情抽屉和专用工作流面板替代']],
  80. ['Content / Bar/TieMove', ['implemented', '已由 barBatch 目标贴吧分类校验与移动替代']],
  81. ['Content / Bar/Tie_API', ['partial', '已覆盖审核、回收恢复、置顶/沉底、精华和移动;勋章关系未迁移']],
  82. ['Exam/Question_Class_Manage', ['partial', '已提供帐套分类列表、搜索和父分类编号;旧展开树布局尚未逐像素复刻']],
  83. ['Exam/AddQuestion_Class', ['implemented', '已由 ExamClass 专用结构表单替代']],
  84. ['Exam/Question_Class_GetChild', ['implemented', '已由 examClassChildren 帐套隔离子分类接口替代']],
  85. ['Exam/Question_Class_Add', ['implemented', '已由 saveExamClass 原子编号、防重、父级与循环校验替代']],
  86. ['Exam/ExamPointManage', ['implemented', '已提供帐套考点列表、搜索、分页和父子结构字段']],
  87. ['Exam/AddExamPoint', ['implemented', '已由 ExamPoint 专用考点结构表单替代']],
  88. ['Exam/ExamPoint_Add', ['implemented', '已由 saveExamPoint 原子编号、防重、父级与循环校验替代']],
  89. ['Exam/KnowledgeManage', ['partial', '已提供帐套知识点列表、搜索和父级/科目结构字段;旧展开树布局尚未逐像素复刻']],
  90. ['Exam/AddKnowledge', ['implemented', '已由 QuestionsKnowledge 专用知识点结构表单替代']],
  91. ['Exam/Knowledge_Add', ['implemented', '已由 saveKnowledge 原子编号、科目防重、父级与循环校验替代']],
  92. ['Exam/Knowledge_GetChild', ['implemented', '已由 knowledgeChildren 帐套及科目隔离子级接口替代']],
  93. ['Exam/ExTeacherManage', ['implemented', '已提供帐套考试教师列表、搜索、分页与删除']],
  94. ['Exam/AddExTeacher', ['implemented', '已由 ExTeacher 专用教师资料表单替代']],
  95. ['Exam/Teacher_Add', ['implemented', '已由 saveExamTeacher 原子编号和任教分类归属校验替代']],
  96. ['Dict/DicCateList', ['implemented', '已提供帐套字典分类列表、搜索、分页及启用状态管理']],
  97. ['Dict/DictCate_API', ['implemented', '已由字典分类专用保存、批量启停和带字典项引用保护的删除替代']],
  98. ['Dict/DicCateSel', ['implemented', '已由 dictionaryCategories 帐套隔离分类选择接口和 Angular 下拉控件替代']],
  99. ['Dict/DicItemList', ['implemented', '已提供帐套字典项列表、搜索、分页和所属分类选择']],
  100. ['Dict/DicItem_API', ['implemented', '已由字典项专用保存、分类归属校验、批量启停和删除替代']],
  101. ['Dict/GradeCateManage', ['implemented', '已提供多级字典分类列表、搜索、分页和引用保护删除']],
  102. ['Dict/GCateAdd', ['implemented', '已由 GradeCate 专用分类表单与层级别名输入替代']],
  103. ['Dict/GCateAdd_Submit', ['implemented', '已由 saveGradeCategory 编号、防重和层级别名校验替代']],
  104. ['Dict/GCate_API', ['implemented', '已由 gradeBatch 带选项引用保护的批量删除替代']],
  105. ['Dict/GradeOption', ['implemented', '已由按分类和父选项筛选的 gradeOptions 列表替代']],
  106. ['Dict/GOptionAdd', ['implemented', '已由 Grade 专用分类、父级选项表单替代']],
  107. ['Dict/GOptionAdd_Submit', ['implemented', '已由 saveGradeOption 自动层级、同级防重和父级归属校验替代']],
  108. ['Dict/GOption_API', ['implemented', '已由 gradeBatch 带下级引用保护的批量删除替代']],
  109. ['Shop / Exp/Currency', ['implemented', '已提供帐套货币列表、搜索、分页与批量删除']],
  110. ['Shop / Exp/CurrencyAdd', ['blocked-infrastructure', 'Currency.id 与 Parse 保留 id 冲突,物理表又缺少可写旧 ID 列,Angular 已隐藏新增入口']],
  111. ['Shop / Exp/CurrencyAdd_Submit', ['blocked-infrastructure', '无法为新货币持久化旧数字 ID;saveCurrency 对新增明确返回 migration_blocked']],
  112. ['Shop / Exp/Currency_API', ['implemented', '已由 currencyBatch 帐套隔离批量删除替代']],
  113. ['Config/Holiday', ['implemented', '已提供帐套节假日列表、搜索、分页、编辑与批量删除']],
  114. ['Config/Holiday_API', ['implemented', '已由 saveHoliday 白名单更新和 holidayBatch 帐套隔离删除替代']],
  115. ['Config/HolidayAdd', ['blocked-infrastructure', 'SysHoliday.id 与 Parse 保留 id 冲突,物理表又缺少可写旧 ID 列,Angular 已隐藏新增入口']],
  116. ['Config/HolidayAdd_Submit', ['blocked-infrastructure', '无法为新节假日持久化旧数字 ID;saveHoliday 对新增明确返回 migration_blocked']],
  117. ]);
  118. const blockedDataModules = new Map([
  119. ['Order', '目标库缺少订单、订单商品、支付与物流主表'],
  120. ['Pay', '目标库缺少支付流水、订单支付明细及可验证的交易数据'],
  121. ['Sale', '目标库缺少订单销售明细与可复算的销售事实表'],
  122. ['Shop / Coupon', '目标库无优惠券、促销实例与用户领取关系'],
  123. ['Shop / Refund', '目标库无订单退款与退款流水'],
  124. ['Product', '目标 Product 仅为旧商品附表,缺少 ZL_Commodities 主表'],
  125. ]);
  126. const blockedInfrastructureModules = new Map([
  127. ['Extend / DBTool', '依赖数据库备份恢复、建表或服务器文件系统,不适用于托管 Parse 运行时'],
  128. ['Files', '依赖旧站点文件系统与文件云配置,尚无新对象存储方案'],
  129. ['FontManager', '依赖服务器字体文件安装与删除权限'],
  130. ['Label / PowerShell', '依赖 Windows PowerShell 与服务器命令执行,云函数明确禁止'],
  131. ['Label / SiteDeploy', '依赖站点发布、远程命令与文件下载基础设施'],
  132. ['Site', '依赖 IIS 站点、虚拟目录、主机池与数据库服务器管理'],
  133. ['Template', '依赖旧模板/CSS 文件系统和 Zoomla 标签运行时'],
  134. ['Extend / MyCss', '依赖旧站点 CSS 文件读写与下载'],
  135. ['Mobile / WeiXin', '依赖新版微信、支付、模板消息和服务通知凭据'],
  136. ['User / Mail', '依赖邮件与短信服务商配置及发送凭据'],
  137. ]);
  138. const markdown = readFileSync(sourcePath, 'utf8');
  139. const entries = [];
  140. const occurrences = new Map();
  141. for (const line of markdown.split(/\r?\n/)) {
  142. const match = line.match(/^\| ([^|]+) \| (\d+) \| (.+) \|$/);
  143. if (!match || match[1].trim() === '模块 / 控制器') continue;
  144. const moduleName = match[1].trim();
  145. const actions = [...match[3].matchAll(/`([^`]+)`/g)].map((item) => item[1]);
  146. if (actions.length !== Number(match[2])) throw new Error(`${moduleName}: 动作数量不一致`);
  147. for (const action of actions) {
  148. const key = `${moduleName}/${action}`;
  149. const occurrence = (occurrences.get(key) || 0) + 1;
  150. occurrences.set(key, occurrence);
  151. let status = 'pending';
  152. let reason = '尚未逐项迁移或完成等价性核验';
  153. if (explicit.has(key)) [status, reason] = explicit.get(key);
  154. else if (blockedDataModules.has(moduleName)) { status = 'blocked-data'; reason = blockedDataModules.get(moduleName); }
  155. else if (blockedInfrastructureModules.has(moduleName)) { status = 'blocked-infrastructure'; reason = blockedInfrastructureModules.get(moduleName); }
  156. entries.push({ module: moduleName, action, occurrence, status, reason });
  157. }
  158. }
  159. if (entries.length !== 1042) throw new Error(`预期 1042 个后台动作,实际 ${entries.length}`);
  160. const generated = `// Generated by scripts/generate-admin-coverage.mjs. Do not edit by hand.\n` +
  161. `export type LegacyAdminCoverageStatus = 'implemented' | 'partial' | 'blocked-data' | 'blocked-infrastructure' | 'pending';\n` +
  162. `export interface LegacyAdminCoverageEntry { module: string; action: string; occurrence: number; status: LegacyAdminCoverageStatus; reason: string; }\n` +
  163. `export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = ${JSON.stringify(entries, null, 2)};\n`;
  164. if (checkOnly) {
  165. const current = readFileSync(outputPath, 'utf8');
  166. if (current !== generated) throw new Error('后台迁移覆盖台账已过期,请运行 npm run admin-coverage:generate');
  167. console.log(`Admin coverage is current: ${entries.length} actions.`);
  168. } else {
  169. writeFileSync(outputPath, generated);
  170. console.log(`Generated ${outputPath}: ${entries.length} actions.`);
  171. }