generate-admin-coverage.mjs 6.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  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. ]);
  31. const blockedDataModules = new Map([
  32. ['Order', '目标库缺少订单、订单商品、支付与物流主表'],
  33. ['Pay', '目标库缺少支付流水、订单支付明细及可验证的交易数据'],
  34. ['Sale', '目标库缺少订单销售明细与可复算的销售事实表'],
  35. ['Shop / Coupon', '目标库无优惠券、促销实例与用户领取关系'],
  36. ['Shop / Refund', '目标库无订单退款与退款流水'],
  37. ['Product', '目标 Product 仅为旧商品附表,缺少 ZL_Commodities 主表'],
  38. ]);
  39. const blockedInfrastructureModules = new Map([
  40. ['Extend / DBTool', '依赖数据库备份恢复、建表或服务器文件系统,不适用于托管 Parse 运行时'],
  41. ['Files', '依赖旧站点文件系统与文件云配置,尚无新对象存储方案'],
  42. ['FontManager', '依赖服务器字体文件安装与删除权限'],
  43. ['Label / PowerShell', '依赖 Windows PowerShell 与服务器命令执行,云函数明确禁止'],
  44. ['Label / SiteDeploy', '依赖站点发布、远程命令与文件下载基础设施'],
  45. ['Site', '依赖 IIS 站点、虚拟目录、主机池与数据库服务器管理'],
  46. ['Template', '依赖旧模板/CSS 文件系统和 Zoomla 标签运行时'],
  47. ['Extend / MyCss', '依赖旧站点 CSS 文件读写与下载'],
  48. ['Mobile / WeiXin', '依赖新版微信、支付、模板消息和服务通知凭据'],
  49. ['User / Mail', '依赖邮件与短信服务商配置及发送凭据'],
  50. ]);
  51. const markdown = readFileSync(sourcePath, 'utf8');
  52. const entries = [];
  53. const occurrences = new Map();
  54. for (const line of markdown.split(/\r?\n/)) {
  55. const match = line.match(/^\| ([^|]+) \| (\d+) \| (.+) \|$/);
  56. if (!match || match[1].trim() === '模块 / 控制器') continue;
  57. const moduleName = match[1].trim();
  58. const actions = [...match[3].matchAll(/`([^`]+)`/g)].map((item) => item[1]);
  59. if (actions.length !== Number(match[2])) throw new Error(`${moduleName}: 动作数量不一致`);
  60. for (const action of actions) {
  61. const key = `${moduleName}/${action}`;
  62. const occurrence = (occurrences.get(key) || 0) + 1;
  63. occurrences.set(key, occurrence);
  64. let status = 'pending';
  65. let reason = '尚未逐项迁移或完成等价性核验';
  66. if (explicit.has(key)) [status, reason] = explicit.get(key);
  67. else if (blockedDataModules.has(moduleName)) { status = 'blocked-data'; reason = blockedDataModules.get(moduleName); }
  68. else if (blockedInfrastructureModules.has(moduleName)) { status = 'blocked-infrastructure'; reason = blockedInfrastructureModules.get(moduleName); }
  69. entries.push({ module: moduleName, action, occurrence, status, reason });
  70. }
  71. }
  72. if (entries.length !== 1042) throw new Error(`预期 1042 个后台动作,实际 ${entries.length}`);
  73. const generated = `// Generated by scripts/generate-admin-coverage.mjs. Do not edit by hand.\n` +
  74. `export type LegacyAdminCoverageStatus = 'implemented' | 'partial' | 'blocked-data' | 'blocked-infrastructure' | 'pending';\n` +
  75. `export interface LegacyAdminCoverageEntry { module: string; action: string; occurrence: number; status: LegacyAdminCoverageStatus; reason: string; }\n` +
  76. `export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = ${JSON.stringify(entries, null, 2)};\n`;
  77. if (checkOnly) {
  78. const current = readFileSync(outputPath, 'utf8');
  79. if (current !== generated) throw new Error('后台迁移覆盖台账已过期,请运行 npm run admin-coverage:generate');
  80. console.log(`Admin coverage is current: ${entries.length} actions.`);
  81. } else {
  82. writeFileSync(outputPath, generated);
  83. console.log(`Generated ${outputPath}: ${entries.length} actions.`);
  84. }