Explorar el Código

feat: migrate exam administration reads

彭峰 hace 1 mes
padre
commit
a2eb61c60f

+ 1 - 0
docs/migration/admin-migration-status.md

@@ -26,6 +26,7 @@
 - `Design/Counter` 与 `Extend/Counter` 共 14 个访问统计动作已完成数据审计:目标全部 Schema 不含旧 `ZL_Com_VisitCount` 或等价访问明细,无法还原 InfoID、日期、地域、浏览器、系统和来源聚合;全站统计代码还缺少 `SiteInfo.AllSiteJS` 注入运行时。对应云函数统一返回细分 `migration_blocked`,不会以 `SysLog` 或问卷 `SurveyLog` 伪造访问统计。
 - CRM 中仍有真实数据的 `CType/CTypeAdd/CType_Add/CType_Del` 已迁为 `crmClientTypes/saveCrmClientType/crmClientTypeBatch` 与 Angular“CRM 客户类型”页面:只管理 `CRMSAttr.ztype=ctype`,按帐套隔离、名称防重并通过 PostgreSQL 事务锁原子分配旧字符串编号。旧 `CRMS_Client`、`CRMS_Contact`、`IServer`、经销商申请和 Licence/PDF 链均未进入目标 Schema;`ContactInfo` 是企业微信联系人,未被错误当作 CRM 联系人复用。
 - 旧 `ECharts/ECharts` 六个动作依赖 `Content_Chart` 的 TName/ChartTitle/ToolBox/Package/option/SType/Tag 数据、七类 .NET 配置生成器、`SiteInfo.SiteUrl` 与 `/Common/ShowM` 渲染页;目标 Schema 中不存在任何 chart/echart/graph 数据类。所有动作通过 `legacyEChartsBlocker` 返回逐项原因,不用问卷统计或普通 Angular 图表冒充旧图表编辑器。
+- Exam 模块已把仍有历史数据的 `ExamSysPapers/ExamSysQuestions/PaperQuestions/ExamType/ContentPublish/PublishNode` 接入 Angular 只读列表、搜索、分页和详情,并关闭通用写入,防止破坏试卷 `qids/questList`、复合答案和报刊父子结构。教材版本、学校、班级、班级学员、旧 Course、答卷评分数据均未迁入;对应 21 个动作由 `legacyExamBlocker` 返回数据或运行时阻塞。`CourseBinding` 明确不冒充旧考试 Course。
 - 旧 `NodeAdd_Submit`、`Node_API` 与 `UnionNode_Move` 的核心栏目生命周期已迁为专用 `saveNode/nodeBatch`:新增栏目以临时来源键完成 Parse 初始写入,再使用 PostgreSQL 事务锁原子分配 `nodeId/orderId` 并替换为旧 `sourceKey`;保存会校验同级名称/目录重复、父栏目帐套和回收状态,移动会阻断自身/下级循环并递归修正所有后代深度。回收、恢复和每批 1–100 个栏目迁移已接入 Angular;永久删除仅在没有内容、下级栏目、节点权限和模型模板引用时开放,通用保存与删除不能绕过。
 - 旧 `SpecialAdd_Save` 与 `UnionSpecial_Move` 已迁为专用 `saveSpecial/specialBatch`:新增专题使用临时来源键和 PostgreSQL 事务锁原子分配 `specId/orderId`,保存时校验全帐套名称/目录唯一、父专题归属与层级循环;删除会拒绝仍有下级专题的记录。旧 `UnionSpecial_Merge` 被明确列为数据阻塞:`CommonModel.specialId` 只存在于 Parse Schema、PostgreSQL 物理列缺失,商品主表也未迁移,无法安全改写内容/商品专题关系。
 - 旧 `ModelManage/Field/Order_Submit` 的安全部分已迁为 `saveModelMetadata/saveModelFieldMetadata/modelFieldOrder`:现有模型可编辑名称、条目文案、图标、说明和内容模板元数据,现有字段可编辑别名、提示、显示/搜索配置,并可在同模型内批量排序;`modelId/modelType/tableName/fieldId/modelId/fieldName/fieldType/sourceKey` 等结构字段通用只读。新增、复制、删除模型或字段会显式返回 501,因为旧实现同时执行 PostgreSQL 建表/改列、标签生成和模板文件写入,目标托管环境没有该 DDL/旧模板运行时。

+ 22 - 4
scripts/deploy-admin-functions.mjs

@@ -9,8 +9,8 @@ const adminGatewayCode = String.raw`
 const CLASS_LABELS = {
   _User: '用户管理', Company: '帐套管理', Profile: '员工档案', Group: '用户组', Role: '角色', Permission: '权限', Department: '组织部门',
   CourseBinding: '课程绑定', CourseAppointment: '课程预约', LessonRecord: '上课记录', DailyStudyRecord: '每日学习记录', PracticeRecord: '练习记录', MemoryPracticeRecord: '抗遗忘记录',
-  VocabularyWord: '词库', CommonModel: '通用内容', ContentArticle: '文章内容', ContentPublish: '内容发布', Node: '栏目节点', NodeAuth: '节点权限', Model: '内容模型', ModelField: '模型字段',
-  ExamClass: '试题分类', ExamSysQuestions: '考试题库', ExamSysPapers: '试卷', ExamType: '考试类型', ExamPoint: '知识点', PaperQuestions: '试卷题目',
+  VocabularyWord: '词库', CommonModel: '通用内容', ContentArticle: '文章内容', ContentPublish: '报刊内容', Node: '栏目节点', NodeAuth: '节点权限', Model: '内容模型', ModelField: '模型字段', PublishNode: '报刊发布节点',
+  ExamClass: '试题分类', ExamSysQuestions: '考试题库', ExamSysPapers: '考试试卷', ExamType: '考试类型', ExamPoint: '知识点', PaperQuestions: '试卷大题分组',
   Datadiccategory: '数据字典分类', Datadic: '数据字典项',
   GradeCate: '多级字典分类', Grade: '多级字典选项',
   Currency: '货币管理', SysHoliday: '节假日管理',
@@ -20,7 +20,7 @@ const CLASS_LABELS = {
 const ALLOWED_CLASSES = new Set([
   'PageTemplate','PaperQuestions','PayPlat','Permission','PlatComp','App','DesignAnswer','DesignAsk','Attachment','DesignPage','DesignQuestion','DesignRes','Feedback','DesignScence','StudentAchieve','ContentArticle','DesignSiteInfo','Profile','AdInfo','AdZone','ARoleAuth','Baike','ExamSysQuestions','ContactInfo','VocabularyWord','AssessmentProfile','Agency','MemoryPracticeRecord','DeliveryCenter','CourseBinding','PracticeRecord','CourseAppointment','Account','SurveyItem','SurveyLog','LessonRecord','DailyStudyRecord','CommonModel','ContentPublish','Company','_Role','_User','CRMSAttr','Currency','Datadic','Datadiccategory','DesignTlp','DocModel','DocPermission','ExamClass','ExamSysPapers','ExamType','ExamPoint','ExTeacher','FontPicShape','FontPicShapeType','Grade','GradeCate','Group','GroupModel','GuestBar','Guestbook','Guestcate','MailTemp','Manager','MisProcedure','MisProLevel','MisSign','MisType','PageStyle','Model','ModelField','Node','NodeAuth','NodeModelTemplate','Product','PlatUserRole','Pub','PubTw','PubWTHD','PubZXDC','PublishNode','QuestionsKnowledge','Role','StoreProduct','SafeMobile','Search','SenTask','ServiceSeat','ShopFareTlp','ShopMoneyRegular','Special','StoreApplication','StoreStyle','SysCSSManage','SysHoliday','SysLog','Temp','ThirdPlatInfo','UserCredit','UserDummyPoint','UserFriendGroup','UserLevel','UserSIcon','UserUserPoint','UserExpDomP','UserExpHis'
 ]);
-const READ_ONLY_CLASSES = new Set(['_Role','Permission','PayPlat','ThirdPlatInfo','GuestBar','DesignAnswer','Baike']);
+const READ_ONLY_CLASSES = new Set(['_Role','Permission','PayPlat','ThirdPlatInfo','GuestBar','DesignAnswer','Baike','ExamSysPapers','ExamSysQuestions','PaperQuestions','ExamType','ContentPublish','PublishNode']);
 const SYSTEM_FIELDS = new Set(['objectId','createdAt','updatedAt','ACL','company','password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','adminRoleKey','isAdmin','roles','role','isDisabled','legacyUserId','legacyGroupId','legacyUserData','groupId']);
 const HIDDEN_FIELDS = new Set(['password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','apiKey','appSecret','secret','token']);
 const GENERIC_WRITE_TYPES = new Set(['String','Number','Boolean','Date','Pointer','Object','Array']);
@@ -1327,6 +1327,24 @@ async function handler(request, response) {
         Default: '目标没有可按 SType 筛选分页的旧 Content_Chart 记录'
       }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧 ECharts 动作'); fail(501, 'migration_blocked: ' + blockers[action]);
     }
+    if (operation === 'legacyExamBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        AddEngLishQuestion: '旧英语试题页依赖复合小题、音频/图片上传和答案解析器;目标仅保留只读 ExamSysQuestions 记录',
+        Setting: '目标 Schema 缺少旧 Exam_Sys_Answer 答卷明细,无法按最近一周统计并重算试题难度',
+        VersionList: '目标 Schema 缺少旧 Exam_Version 教材版本/册/章节树与历史数据', AddVersion: '目标无可定位或新建的 Exam_Version 版本树记录',
+        SchoolManage: '目标 Schema 缺少旧 School 学校主表与省市县数据', AddSchool: '目标无可编辑或新增的 School 记录',
+        ClassRoomManage: '目标 Schema 缺少旧 ClassRoom 班级主表与学校、年级、教师关系', AddClassRoom: '目标无可编辑或新增的 ClassRoom 记录',
+        StudentList: '目标 Schema 缺少 ClassRoomStudent 班级学员关系、审核状态与学员类型',
+        CourseManage: '目标 Schema 缺少旧 Course 课程主表;CourseBinding 是小树课程绑定,语义不等价', AddCourse: '目标无旧 Course 名称、科目、教师与课程结构记录',
+        ToScore: '目标 Schema 缺少旧 Exam_Sys_Answer 主观题答卷与评分状态',
+        Papers_Add: '试卷写入必须原子维护 qids/questList、分类、计时、价格与题目分值;当前只读迁移不允许通用表单破坏结构',
+        Question_Add: '旧试题写入依赖题型答案解析、复合小题、知识点和上传资源;当前只读迁移不能安全保存',
+        Version_GetList: '目标缺少可按 pid 查询的 Exam_Version 版本树', Version_Add: '目标缺少 Exam_Version 版本、册、章节持久层',
+        School_Add: '目标缺少 School 主表,无法保存学校类型、地区、校徽和状态', ClassRoom_Add: '目标缺少 ClassRoom 与学校、教师、年级关系持久层', Course_Add: '目标缺少旧 Course 主表和科目树关系',
+        Setting_Update: '目标缺少 Exam_Sys_Answer 作答统计,不能执行 CountDiffcult 难度重算',
+        PublishDesign: '旧报刊可视化设计器的前端资产、布局协议与渲染运行时未迁入'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧考试动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
     if (operation === 'legacyChatBlocker') {
       const action = String(input.action || ''); const blockers = {
         Default: '目标 Schema 缺少 ChatMsg 消息表,无法还原用户消息数列表',
@@ -1714,7 +1732,7 @@ async function handler(request, response) {
       const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20));
       let query = new Parse.Query(className); applyTenant(query, fields, context, input.companyId);
       const search = String(input.search || '').trim();
-      if (search) { const searchField = ['name','title','nodeName','specName','modelName','fieldName','fieldAlias','groupName','categoryname','dicname','cateName','gradeName','cClassName','testPoint','kName','tname','username','realName','mobile','sourceKey'].find((name) => fields[name] && fields[name].type === 'String'); if (searchField) query.matches(searchField, escapeRegex(search), 'i'); }
+      if (search) { const searchField = ['name','title','pName','pTitle','questionTitle','tName','newsName','nodeName','specName','modelName','fieldName','fieldAlias','groupName','categoryname','dicname','cateName','gradeName','cClassName','testPoint','kName','tname','username','realName','mobile','sourceKey'].find((name) => fields[name] && fields[name].type === 'String'); if (searchField) query.matches(searchField, escapeRegex(search), 'i'); }
       const sort = fields[input.sort] ? String(input.sort) : fields.updatedAt ? 'updatedAt' : 'createdAt'; if (input.order === 'asc') query.ascending(sort); else query.descending(sort);
       const total = await query.count({ useMasterKey: true }); query.skip((page - 1) * pageSize); query.limit(pageSize);
       const results = (await query.find({ useMasterKey: true })).filter(isVisible);

+ 42 - 0
scripts/generate-admin-coverage.mjs

@@ -99,6 +99,48 @@ const explicit = new Map([
   ['ECharts / ECharts/AddChart', ['blocked-infrastructure', '目标缺少 Content_Chart 持久层和旧七类图表配置生成运行时']],
   ['ECharts / ECharts/AddChart_Submit', ['blocked-data', '目标无法持久化 TName、ChartTitle、ToolBox、Package、option、SType 与 Tag 图表定义']],
   ['ECharts / ECharts/Default', ['blocked-data', '目标没有可按 SType 筛选分页的旧 Content_Chart 记录']],
+  ['Exam/Papers_System_Manage', ['partial', '已由只读 ExamSysPapers 帐套列表、试卷名搜索、分页与详情替代;旧分类筛选尚未复刻']],
+  ['Exam/Papers_System_Add', ['partial', '现有试卷详情可查看;qids/questList、计时、价格与分值需专用写入链后才能编辑或新增']],
+  ['Exam/Paper_QuestionManage', ['partial', '已提供 PaperQuestions 大题分组与 ExamSysQuestions 历史数据只读页;按单张试卷联合编排尚未复刻']],
+  ['Exam/ViewPaperCenter', ['partial', '试卷与试题详情均可查看;旧按题型分组、答案和分数排版预览尚未复刻']],
+  ['Exam/QuestList', ['partial', '已由 ExamSysQuestions 帐套隔离列表、标题搜索、分页和详情替代;年级/难度/版本组合筛选待补']],
+  ['Exam/AddEngLishQuestion', ['blocked-infrastructure', '旧英语试题页依赖复合小题、音频/图片上传和答案解析器;目标仅保留只读试题记录']],
+  ['Exam/QuestShow', ['partial', '已提供 ExamSysQuestions 主记录只读详情;旧复合小题递归展示和答案排版尚未复刻']],
+  ['Exam/Setting', ['blocked-data', '目标 Schema 缺少旧 Exam_Sys_Answer 答卷明细,无法按最近一周统计并重算试题难度']],
+  ['Exam/QuestGrade', ['partial', '已由 GradeCate/Grade 多级字典页维护旧题型、难度、年级和教材版本选项;考试专用分类快捷筛选待补']],
+  ['Exam/VersionList', ['blocked-data', '目标 Schema 缺少旧 Exam_Version 教材版本/册/章节树与历史数据']],
+  ['Exam/AddVersion', ['blocked-data', '目标无可定位或新建的 Exam_Version 版本树记录']],
+  ['Exam/SchoolManage', ['blocked-data', '目标 Schema 缺少旧 School 学校主表与省市县数据']],
+  ['Exam/AddSchool', ['blocked-data', '目标无可编辑或新增的 School 记录']],
+  ['Exam/ClassRoomManage', ['blocked-data', '目标 Schema 缺少旧 ClassRoom 班级主表与学校、年级、教师关系']],
+  ['Exam/AddClassRoom', ['blocked-data', '目标无可编辑或新增的 ClassRoom 记录']],
+  ['Exam/StudentList', ['blocked-data', '目标 Schema 缺少 ClassRoomStudent 班级学员关系、审核状态与学员类型']],
+  ['Exam/CourseManage', ['blocked-data', '目标 Schema 缺少旧 Course 课程主表;CourseBinding 是小树课程绑定,语义不等价']],
+  ['Exam/AddCourse', ['blocked-data', '目标无旧 Course 名称、科目、教师与课程结构记录']],
+  ['Exam/order', ['partial', '现有 ExamPoint 由 saveExamPoint 维护父级与 orderBy;旧打开页面即重写全表连续排序的副作用未保留']],
+  ['Exam/ToScore', ['blocked-data', '目标 Schema 缺少旧 Exam_Sys_Answer 主观题答卷与评分状态']],
+  ['Exam/SelQuestion', ['partial', '已由只读 ExamSysQuestions 搜索与选择列表提供数据;旧 issmall/qtype 多条件弹窗布局待补']],
+  ['Exam/Paper_AddLarge', ['partial', '现有 PaperQuestions 与试题详情可查询;按选中题目即时聚合大题 JSON 的专用接口待补']],
+  ['Exam/Papers_Add', ['blocked-infrastructure', '试卷写入必须原子维护 qids/questList、分类、计时、价格与题目分值;当前只读迁移不允许通用表单破坏结构']],
+  ['Exam/Question_Add', ['blocked-infrastructure', '旧试题写入依赖题型答案解析、复合小题、知识点和上传资源;当前只读迁移不能安全保存']],
+  ['Exam/QuestGrade_Add', ['partial', '现有 Grade 专用云函数可新增和编辑多级选项;考试页的 cate=4/5/6/7 快捷入口待补']],
+  ['Exam/Version_GetList', ['blocked-data', '目标缺少可按 pid 查询的 Exam_Version 版本树']],
+  ['Exam/Version_Add', ['blocked-data', '目标缺少 Exam_Version 版本、册、章节持久层']],
+  ['Exam/School_Add', ['blocked-data', '目标缺少 School 主表,无法保存学校类型、地区、校徽和状态']],
+  ['Exam/ClassRoom_Add', ['blocked-data', '目标缺少 ClassRoom 与学校、教师、年级关系持久层']],
+  ['Exam/Course_Add', ['blocked-data', '目标缺少旧 Course 主表和科目树关系']],
+  ['Exam/Setting_Update', ['blocked-data', '目标缺少 Exam_Sys_Answer 作答统计,不能执行 CountDiffcult 难度重算']],
+  ['Exam/News', ['partial', '已由只读 ContentPublish 报刊内容列表、搜索、分页与详情替代;旧专用树状布局待补']],
+  ['Exam/NewsAdd', ['partial', '现有报刊记录可查看;新建/编辑需专用旧编号和父子关系写入链']],
+  ['Exam/NewsAdd_Submit', ['partial', 'ContentPublish 数据已迁入但当前只读,尚未开放 newsName/nid 的专用原子保存']],
+  ['Exam/News_API', ['partial', '现有报刊详情可安全读取;删除与 publish_GetModel 组合响应尚未开放']],
+  ['Exam/NewsDiv', ['partial', 'ContentPublish 历史子项可查看;按 pid 的专用分区列表待补']],
+  ['Exam/PublishDesc', ['partial', 'ContentPublish 与 PublishNode 历史数据均可查看;按 nid 联合展示待补']],
+  ['Exam/PubNodeAdd', ['partial', '现有 PublishNode 详情可查看;父节点选择与安全新增/编辑尚未开放']],
+  ['Exam/PubNodeAdd_Submit', ['partial', 'PublishNode 数据已迁入但当前只读,等待父级循环校验和旧编号原子分配']],
+  ['Exam/PublishAdd', ['partial', '现有 ContentPublish 详情可查看;报刊父项上下文与专用编辑器尚未复刻']],
+  ['Exam/PublishAdd_Submit', ['partial', 'ContentPublish title/imgPath/json/pid/cdate/attachFile 写入尚未开放专用校验流程']],
+  ['Exam/PublishDesign', ['blocked-infrastructure', '旧报刊可视化设计器的前端资产、布局协议与渲染运行时未迁入']],
   ['Content / Node/NodeManage', ['implemented', '已由 Node 资源列表、分页与专用结构操作替代']],
   ['Content / Node/NodeSearch', ['implemented', '已覆盖按栏目名称搜索并保持帐套隔离']],
   ['Content / Node/NodeAdd', ['implemented', '已由 Node 动态表单与父节点结构控件替代']],

+ 3 - 0
scripts/smoke-admin-functions.mjs

@@ -650,6 +650,9 @@ try {
   for (const action of ['EChartList','ChartCite','ShowM','AddChart','AddChart_Submit','Default']) {
     const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyEChartsBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧 ECharts 动作 ${action} 未返回明确迁移阻塞`);
   }
+  for (const action of ['AddEngLishQuestion','Setting','VersionList','AddVersion','SchoolManage','AddSchool','ClassRoomManage','AddClassRoom','StudentList','CourseManage','AddCourse','ToScore','Papers_Add','Question_Add','Version_GetList','Version_Add','School_Add','ClassRoom_Add','Course_Add','Setting_Update','PublishDesign']) {
+    const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyExamBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧考试动作 ${action} 未返回明确迁移阻塞`);
+  }
   await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'contentBatch', className: 'CommonModel', action: 'status', objectIds: [temporaryAppointmentCommonObjectId], status: 99 });
 
   const page = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'list', className: 'CourseAppointment', page: 1, pageSize: 2 });

+ 5 - 0
src/app/admin/admin.constants.ts

@@ -51,5 +51,10 @@ export const ADMIN_NAVIGATION = [
   { label: 'CRM 客户类型', icon: 'contact-round', route: '/admin/resources/CRMSAttr' },
   { label: '百科审核', icon: 'book-marked', route: '/admin/resources/Baike' },
   { label: '题库管理', icon: 'list-checks', route: '/admin/resources/ExamSysQuestions' },
+  { label: '考试试卷', icon: 'file-check-2', route: '/admin/resources/ExamSysPapers' },
+  { label: '试卷大题分组', icon: 'list-collapse', route: '/admin/resources/PaperQuestions' },
+  { label: '考试类型', icon: 'list-filter', route: '/admin/resources/ExamType' },
+  { label: '报刊内容', icon: 'newspaper', route: '/admin/resources/ContentPublish' },
+  { label: '报刊发布节点', icon: 'network', route: '/admin/resources/PublishNode' },
   { label: '迁移状态', icon: 'route', route: '/admin/migration' },
 ] as const;

+ 84 - 84
src/app/admin/legacy-admin-coverage.generated.ts

@@ -1910,57 +1910,57 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Exam",
     "action": "Papers_System_Manage",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "已由只读 ExamSysPapers 帐套列表、试卷名搜索、分页与详情替代;旧分类筛选尚未复刻"
   },
   {
     "module": "Exam",
     "action": "Papers_System_Add",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "现有试卷详情可查看;qids/questList、计时、价格与分值需专用写入链后才能编辑或新增"
   },
   {
     "module": "Exam",
     "action": "Paper_QuestionManage",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "已提供 PaperQuestions 大题分组与 ExamSysQuestions 历史数据只读页;按单张试卷联合编排尚未复刻"
   },
   {
     "module": "Exam",
     "action": "ViewPaperCenter",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "试卷与试题详情均可查看;旧按题型分组、答案和分数排版预览尚未复刻"
   },
   {
     "module": "Exam",
     "action": "QuestList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "已由 ExamSysQuestions 帐套隔离列表、标题搜索、分页和详情替代;年级/难度/版本组合筛选待补"
   },
   {
     "module": "Exam",
     "action": "AddEngLishQuestion",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "旧英语试题页依赖复合小题、音频/图片上传和答案解析器;目标仅保留只读试题记录"
   },
   {
     "module": "Exam",
     "action": "QuestShow",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "已提供 ExamSysQuestions 主记录只读详情;旧复合小题递归展示和答案排版尚未复刻"
   },
   {
     "module": "Exam",
     "action": "Setting",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 Exam_Sys_Answer 答卷明细,无法按最近一周统计并重算试题难度"
   },
   {
     "module": "Exam",
@@ -1980,57 +1980,57 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Exam",
     "action": "QuestGrade",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "已由 GradeCate/Grade 多级字典页维护旧题型、难度、年级和教材版本选项;考试专用分类快捷筛选待补"
   },
   {
     "module": "Exam",
     "action": "VersionList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 Exam_Version 教材版本/册/章节树与历史数据"
   },
   {
     "module": "Exam",
     "action": "AddVersion",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无可定位或新建的 Exam_Version 版本树记录"
   },
   {
     "module": "Exam",
     "action": "SchoolManage",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 School 学校主表与省市县数据"
   },
   {
     "module": "Exam",
     "action": "AddSchool",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无可编辑或新增的 School 记录"
   },
   {
     "module": "Exam",
     "action": "ClassRoomManage",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 ClassRoom 班级主表与学校、年级、教师关系"
   },
   {
     "module": "Exam",
     "action": "AddClassRoom",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无可编辑或新增的 ClassRoom 记录"
   },
   {
     "module": "Exam",
     "action": "StudentList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少 ClassRoomStudent 班级学员关系、审核状态与学员类型"
   },
   {
     "module": "Exam",
@@ -2050,15 +2050,15 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Exam",
     "action": "CourseManage",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 Course 课程主表;CourseBinding 是小树课程绑定,语义不等价"
   },
   {
     "module": "Exam",
     "action": "AddCourse",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无旧 Course 名称、科目、教师与课程结构记录"
   },
   {
     "module": "Exam",
@@ -2085,8 +2085,8 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Exam",
     "action": "order",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "现有 ExamPoint 由 saveExamPoint 维护父级与 orderBy;旧打开页面即重写全表连续排序的副作用未保留"
   },
   {
     "module": "Exam",
@@ -2099,36 +2099,36 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Exam",
     "action": "ToScore",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 Exam_Sys_Answer 主观题答卷与评分状态"
   },
   {
     "module": "Exam",
     "action": "SelQuestion",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "已由只读 ExamSysQuestions 搜索与选择列表提供数据;旧 issmall/qtype 多条件弹窗布局待补"
   },
   {
     "module": "Exam",
     "action": "Paper_AddLarge",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "现有 PaperQuestions 与试题详情可查询;按选中题目即时聚合大题 JSON 的专用接口待补"
   },
   {
     "module": "Exam",
     "action": "Papers_Add",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "试卷写入必须原子维护 qids/questList、分类、计时、价格与题目分值;当前只读迁移不允许通用表单破坏结构"
   },
   {
     "module": "Exam",
     "action": "Question_Add",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "旧试题写入依赖题型答案解析、复合小题、知识点和上传资源;当前只读迁移不能安全保存"
   },
   {
     "module": "Exam",
@@ -2148,22 +2148,22 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Exam",
     "action": "QuestGrade_Add",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "现有 Grade 专用云函数可新增和编辑多级选项;考试页的 cate=4/5/6/7 快捷入口待补"
   },
   {
     "module": "Exam",
     "action": "Version_GetList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少可按 pid 查询的 Exam_Version 版本树"
   },
   {
     "module": "Exam",
     "action": "Version_Add",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少 Exam_Version 版本、册、章节持久层"
   },
   {
     "module": "Exam",
@@ -2183,15 +2183,15 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Exam",
     "action": "School_Add",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少 School 主表,无法保存学校类型、地区、校徽和状态"
   },
   {
     "module": "Exam",
     "action": "ClassRoom_Add",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少 ClassRoom 与学校、教师、年级关系持久层"
   },
   {
     "module": "Exam",
@@ -2204,8 +2204,8 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Exam",
     "action": "Course_Add",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少旧 Course 主表和科目树关系"
   },
   {
     "module": "Exam",
@@ -2218,85 +2218,85 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Exam",
     "action": "Setting_Update",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少 Exam_Sys_Answer 作答统计,不能执行 CountDiffcult 难度重算"
   },
   {
     "module": "Exam",
     "action": "News",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "已由只读 ContentPublish 报刊内容列表、搜索、分页与详情替代;旧专用树状布局待补"
   },
   {
     "module": "Exam",
     "action": "NewsAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "现有报刊记录可查看;新建/编辑需专用旧编号和父子关系写入链"
   },
   {
     "module": "Exam",
     "action": "NewsAdd_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "ContentPublish 数据已迁入但当前只读,尚未开放 newsName/nid 的专用原子保存"
   },
   {
     "module": "Exam",
     "action": "News_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "现有报刊详情可安全读取;删除与 publish_GetModel 组合响应尚未开放"
   },
   {
     "module": "Exam",
     "action": "NewsDiv",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "ContentPublish 历史子项可查看;按 pid 的专用分区列表待补"
   },
   {
     "module": "Exam",
     "action": "PublishDesc",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "ContentPublish 与 PublishNode 历史数据均可查看;按 nid 联合展示待补"
   },
   {
     "module": "Exam",
     "action": "PubNodeAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "现有 PublishNode 详情可查看;父节点选择与安全新增/编辑尚未开放"
   },
   {
     "module": "Exam",
     "action": "PubNodeAdd_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "PublishNode 数据已迁入但当前只读,等待父级循环校验和旧编号原子分配"
   },
   {
     "module": "Exam",
     "action": "PublishAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "现有 ContentPublish 详情可查看;报刊父项上下文与专用编辑器尚未复刻"
   },
   {
     "module": "Exam",
     "action": "PublishAdd_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "ContentPublish title/imgPath/json/pid/cdate/attachFile 写入尚未开放专用校验流程"
   },
   {
     "module": "Exam",
     "action": "PublishDesign",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "旧报刊可视化设计器的前端资产、布局协议与渲染运行时未迁入"
   },
   {
     "module": "Shop / Exp",

+ 3 - 0
src/app/admin/pages/admin-resource.component.html

@@ -160,6 +160,9 @@
         @if (className() === 'CRMSAttr') {
           <section class="account-create-note"><strong>CRM 客户类型</strong><span>仅展示旧 ztype=ctype 记录;编号由云函数在帐套内原子分配,名称防重。客户、联系人、服务记录和经销商申请主表未迁入,不会以其他数据类冒充。</span></section>
         }
+        @if (['ExamSysPapers', 'ExamSysQuestions', 'PaperQuestions', 'ExamType', 'ContentPublish', 'PublishNode'].includes(className())) {
+          <section class="account-create-note"><strong>考试与报刊历史数据只读</strong><span>已保留帐套隔离列表、搜索和详情;试卷题目关系、答案解析、发布父子结构与旧数字 ID 不能通过通用表单修改,等待对应专用写入链完成。</span></section>
+        }
         @if (className() === 'UserMoneyLog') {
           <section class="account-create-note"><strong>资金积分流水只读</strong><span>旧单表已按余额、银币、经验、积分、虚拟币和信用分拆分为六个规范化账本;管理页只合并查看,不允许篡改历史流水。</span></section>
         }

+ 1 - 1
src/app/admin/pages/admin-resource.component.ts

@@ -148,7 +148,7 @@ export class AdminResourceComponent implements OnInit {
   readonly icons = { ChevronLeft, ChevronRight, Eye, KeyRound, LoaderCircle, Pencil, Plus, RefreshCw, Search, Trash2, X };
   readonly columns = computed(() => {
     const fields = this.schema()?.fields ?? [];
-    const preferred = ['objectId', 'name', 'title', 'value', 'remark', 'qtitle', 'sName', 'storeName', 'styleName', 'count', 'content', 'str1', 'categoryname', 'dicname', 'cateName', 'gradeName', 'cClassName', 'testPoint', 'kName', 'tname', 'username', 'userName', 'realName', 'sRemrk', 'askId', 'zoneId', 'typeId', 'shape', 'qtype', 'required', 'ztype', 'priority', 'zstatus', 'storeState', 'storeCommendState', 'storeStyleId', 'sDefault', 'sIndex', 'str4', 'sdate', 'edate', 'fileUrl', 'type', 'state', 'eliteLevel', 'orderId', 'mobile', 'sourceKey', 'status', 'createdAt', 'updatedAt'];
+    const preferred = ['objectId', 'name', 'title', 'pName', 'pTitle', 'questionTitle', 'tName', 'newsName', 'nodeName', 'value', 'remark', 'qtitle', 'sName', 'storeName', 'styleName', 'count', 'content', 'str1', 'categoryname', 'dicname', 'cateName', 'gradeName', 'cClassName', 'testPoint', 'kName', 'tname', 'username', 'userName', 'realName', 'sRemrk', 'askId', 'zoneId', 'typeId', 'shape', 'qtype', 'required', 'ztype', 'priority', 'zstatus', 'storeState', 'storeCommendState', 'storeStyleId', 'sDefault', 'sIndex', 'str4', 'sdate', 'edate', 'fileUrl', 'type', 'state', 'eliteLevel', 'orderId', 'mobile', 'sourceKey', 'status', 'createdAt', 'updatedAt'];
     const rank = (name: string): number => { const index = preferred.indexOf(name); return index < 0 ? preferred.length : index; };
     return fields.map((field, index) => ({ field, index })).sort((a, b) => rank(a.field.name) - rank(b.field.name) || a.index - b.index).map(({ field }) => field).slice(0, 7);
   });