소스 검색

chore: document blocked q-and-a migration

彭峰 1 개월 전
부모
커밋
c97c485a71

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

@@ -47,8 +47,9 @@
 - 旧内容附加功能 `TagKey/TagKey_Submit` 已迁移为 Angular 全局内容标签词库:保留管道符分隔、移除空白/引号/反斜杠和原顺序语义,改由 `saveContentTagKeys` 管理云函数写入 Parse Config,不再依赖旧 ASP.NET 服务器的 `/js/ICMS/tags.json` 本地文件。线上完整 Schema 核验不存在 `Comment/ContentAudit/ContentVerBak` 或等价类,因此 `Comment/Commont_API`、4 个内容审核流动作以及 `VerBak/VerBak_Del` 均由 `legacyContentAddonBlocker` 返回逐项数据阻塞原因,未用 `CommonModel` 状态伪造丢失的审批历史。
 - 旧商城供应商 `Vendor/Vendor_API/VendorAdd/VendorAdd_Submit` 全部依赖未迁入的 `ZL_Shop_Vendor`:线上 Schema 与签约数据库文档均没有 `Vendor/ShopVendor/Manufacturers`。现有 `Agency` 只有机构编号/名称/负责人,`DeliveryCenter` 只有交付中心编号/负责人,都缺供应商地址、手机、传真、邮箱和状态,不能等价映射。`legacyShopVendorBlocker` 已按动作返回数据阻塞原因,未擅自新建未签约数据类或污染机构数据。
 - 旧用户附加功能中,`PointGroup/PointGroupAdd/PointGroup_API/PointGroup_Submit` 已映射到 `UserLevel`:提供帐套隔离列表、关键词搜索、现有等级别名/积分阈值/图片/备注编辑和批量删除;旧 `id` 与 Parse 保留字段冲突,因此新增等级明确 501。`VMoneyLog` 已由只读 `UserMoneyLog` 虚拟资源统一聚合 `UserExpDomP/UserSIcon/UserExpHis/UserUserPoint/UserDummyPoint/UserCredit` 六类规范化账本,支持余额/银币/经验/积分/虚拟币/信用分类型、日期、用户名与摘要筛选,不允许篡改历史流水。旧 `ThirdInfo.config` 六个提供商和密钥未迁入,现有 `ThirdPlatInfo` 是不同业务平台且没有旧 OAuth/XML 消费者;`PrizeTaskLog(Global.SignIn)` 用户签到日志与 `UserRealNameAuth` 实名申请/奖励链也全部缺失。这 6 个动作由 `legacyUserAddonBlocker` 逐项返回数或基础设施阻塞,未把 `MisSign` 工作签到或 `Profile` 零散身份字段伪装成旧审批历史。
+- 旧问答后台 11 个动作无法从目标数据库等价重建:线上 Schema 没有 `Ask/GuestAnswer`、`ZL_Ask/ZL_GuestAnswer` 历史记录或 `QueId` 问题—回答关系。`Baike` 是百科词条,`Guestbook` 是留言/回复,`GuestBar` 是论坛帖子,`DesignQuestion/DesignAnswer` 是问卷题目/答卷,都不能伪冒问答主从表。`Default/AskInfo/AskAdd/AskAdd_Submit/Ask_API/AnswerList/AnswerAdd/AnswerAdd_Submit/Answer_API` 已按数据阻塞登记;`Config/Config_Submit` 依赖已丢失的 `GuestConfig.WDOption` 问答运行时、用户组权限与积分消费者,按基础设施阻塞登记。`legacyAskBlocker` 会对每个请求返回具体原因。
 - `_User`、`CommonModel`、`Node` 与 `Special` 表格已补齐当前页全选和批量工具栏,可直接批量停用/解锁/移动用户组,审核、回收、恢复、移动内容节点,回收、恢复、移动栏目,以及移动专题;翻页或重新查询会清空选择,避免误操作隐藏页记录。
-- 迁移状态页已接入从反编译基线自动生成的 1,042 项旧后台动作台账,可按模块、action、原因与状态搜索筛选;当前保守登记为 94 项已实现、61 项部分实现、144 项数据阻塞、204 项基础设施阻塞、539 项待逐项核验。生成器和 `admin-coverage:check` 会阻止台账与源码基线漂移。
+- 迁移状态页已接入从反编译基线自动生成的 1,042 项旧后台动作台账,可按模块、action、原因与状态搜索筛选;当前保守登记为 94 项已实现、61 项部分实现、153 项数据阻塞、206 项基础设施阻塞、528 项待逐项核验。生成器和 `admin-coverage:check` 会阻止台账与源码基线漂移。
 - Parse 登录只在浏览器保存当前 `sessionToken`;`masterKey` 只在部署进程中使用。
 - 云函数网关强制校验管理员身份、`company` 帐套、类白名单、字段白名单和敏感字段过滤。
 - `_Session` 不开放,`Function` 源码不开放通用查询或编辑;用户密码只允许专用重置操作。

+ 15 - 0
scripts/deploy-admin-functions.mjs

@@ -1245,6 +1245,21 @@ async function handler(request, response) {
         RealNameAuth_API: '目标缺少实名申请记录及 Global.RealNameAuth 任务奖励链,无法安全审核、取消审核或删除'
       }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧用户附加功能动作'); fail(501, 'migration_blocked: ' + blockers[action]);
     }
+    if (operation === 'legacyAskBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        Default: '目标 Schema 缺少旧 ZL_Ask 问题主表、回收站标记与历史问题数据',
+        AskInfo: '目标无可定位的问答问题详情;Baike/Guestbook/GuestBar/DesignQuestion 均非等价问题主表',
+        AskAdd: '目标无 Ask 数据类承载 QTitle/Qcontent 与问答状态、精华、回收站关系',
+        AskAdd_Submit: '按既定数据库设计不能把问题错写到百科、留言、论坛或问卷题目',
+        Ask_API: '目标缺少可回收、恢复、删除、审核或加精的旧问答问题记录',
+        AnswerList: '目标 Schema 缺少旧 ZL_GuestAnswer 回答主表及 QueId 问题外键',
+        AnswerAdd: '目标无可编辑或新增的问答回答记录与问题归属',
+        AnswerAdd_Submit: '目标缺少 GuestAnswer 的 QueId/UserId/UserName/Content 数据模型与历史关系',
+        Answer_API: '目标缺少可审核、取消审核或删除的旧问答回答记录',
+        Config: '目标没有 GuestConfig.WDOption 问答运行时和 SelGroup/ReplyGroup/QuestGroup 权限消费者',
+        Config_Submit: '即使存储积分与用户组参数,目标也没有问答发布/回复/推荐运行时执行这些规则,不能伪造保存生效'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧问答动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
     if (operation === 'saveExamClass') {
       const objectId = String(input.objectId || '');
       const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};

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

@@ -170,6 +170,17 @@ const explicit = new Map([
   ['User / Addon/RealNameAuth', ['blocked-data', '目标 Schema 缺少 UserRealNameAuth 申请主表、审核状态与历史数据']],
   ['User / Addon/RealNameAuthAdd', ['blocked-data', '目标 Profile 零散身份字段不等价于可审阅的实名认证申请']],
   ['User / Addon/RealNameAuth_API', ['blocked-data', '目标缺少实名申请记录及 Global.RealNameAuth 任务奖励链']],
+  ['Content / Ask/Default', ['blocked-data', '目标 Schema 缺少旧 ZL_Ask 问题主表、回收站标记与历史问题数据']],
+  ['Content / Ask/AskInfo', ['blocked-data', '目标无问答问题详情;Baike/Guestbook/GuestBar/DesignQuestion 均非等价主表']],
+  ['Content / Ask/AskAdd', ['blocked-data', '目标无 Ask 数据类承载 QTitle/Qcontent 与状态、精华、回收站关系']],
+  ['Content / Ask/AskAdd_Submit', ['blocked-data', '不能把旧问答问题错写到百科、留言、论坛或问卷题目']],
+  ['Content / Ask/Ask_API', ['blocked-data', '目标缺少可回收、恢复、删除、审核或加精的旧问答问题记录']],
+  ['Content / Ask/AnswerList', ['blocked-data', '目标 Schema 缺少旧 ZL_GuestAnswer 回答主表及 QueId 问题外键']],
+  ['Content / Ask/AnswerAdd', ['blocked-data', '目标无可编辑或新增的问答回答记录与问题归属']],
+  ['Content / Ask/AnswerAdd_Submit', ['blocked-data', '目标缺少 GuestAnswer QueId/UserId/UserName/Content 数据模型与历史关系']],
+  ['Content / Ask/Answer_API', ['blocked-data', '目标缺少可审核、取消审核或删除的旧问答回答记录']],
+  ['Content / Ask/Config', ['blocked-infrastructure', '目标没有 GuestConfig.WDOption 问答运行时和用户组权限消费者']],
+  ['Content / Ask/Config_Submit', ['blocked-infrastructure', '目标没有问答发布/回复/推荐运行时执行积分与用户组规则']],
   ['FontPic/Index', ['blocked-data', '目标 Schema 缺少旧 FontPic 成品字体图主表与历史数据']],
   ['FontPic/FontPicInfo', ['blocked-data', '目标 Schema 缺少旧 FontPic 成品字体图详情数据']],
   ['FontPic/FontPicInfo_Submit', ['blocked-data', '目标 Schema 缺少旧 FontPic 成品字体图持久化模型']],

+ 2 - 1
scripts/smoke-admin-functions.mjs

@@ -353,6 +353,7 @@ try {
   const directUserLevel = await jsonRequest(`${PARSE_URL}/classes/UserLevel`, { method: 'POST', body: JSON.stringify({ alias: '__user_level_delete_smoke__', pointRate: 999999, image: '', remark: 'temporary', sourceKey: `smoke-user-level-${Date.now()}`, company }) }, true); temporaryUserLevelId = directUserLevel.objectId; const readDirectUserLevel = await jsonRequest(`${PARSE_URL}/classes/UserLevel/${temporaryUserLevelId}`, {}, true); if (readDirectUserLevel.id != null) throw new Error('积分等级省略 id 的创建探测意外生成了旧编号'); const deletedUserLevel = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'userLevelBatch', className: 'UserLevel', action: 'delete', objectIds: [temporaryUserLevelId] }); if (deletedUserLevel.updated !== 1) throw new Error('积分等级批量删除失败'); temporaryUserLevelId = '';
   const userMoneySchema = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'schema', className: 'UserMoneyLog' }); if (userMoneySchema.writable !== false || userMoneySchema.creatable !== false || userMoneySchema.fields?.some((field) => field.writable)) throw new Error('资金积分流水虚拟 Schema 应全部只读'); const moneyPage = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'userMoneyLogs', className: 'UserMoneyLog', type: 1, page: 1, pageSize: 20, search: '', startDate: '', endDate: '' }); const sampleMoneyLog = moneyPage.results?.[0]; if (moneyPage.total < 1 || !String(sampleMoneyLog?.objectId || '').startsWith('UserExpDomP:') || sampleMoneyLog.moneyTypeLabel !== '余额') throw new Error('余额流水规范化聚合异常'); const moneyDetail = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'getUserMoneyLog', className: 'UserMoneyLog', objectId: sampleMoneyLog.objectId }); if (moneyDetail.objectId !== sampleMoneyLog.objectId || moneyDetail.recordObjectId !== sampleMoneyLog.recordObjectId) throw new Error('资金流水详情读取异常'); const moneyDate = String(sampleMoneyLog.hisTime?.iso || sampleMoneyLog.hisTime || '').slice(0,10); const filteredMoney = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'userMoneyLogs', className: 'UserMoneyLog', type: 1, page: 1, pageSize: 20, search: String(sampleMoneyLog.userId || ''), startDate: moneyDate, endDate: moneyDate }); if (filteredMoney.total < 1) throw new Error('资金流水用户/日期筛选失败'); for (const type of [2,3,4,5,6]) { const ledger = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'userMoneyLogs', className: 'UserMoneyLog', type, page: 1, pageSize: 1, search: '', startDate: '', endDate: '' }); if (ledger.className !== 'UserMoneyLog') throw new Error(`资金流水类型 ${type} 未正确路由`); }
   for (const action of ['ThirdInfo','ThirdInfo_Submit','SigninList','RealNameAuth','RealNameAuthAdd','RealNameAuth_API']) { const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyUserAddonBlocker', action }, 501); if (!String(blocked.message).startsWith('migration_blocked:')) throw new Error(`旧用户附加动作 ${action} 未返回明确阻塞原因`); }
+  for (const action of ['Default','AskInfo','AskAdd','AskAdd_Submit','Ask_API','AnswerList','AnswerAdd','AnswerAdd_Submit','Answer_API','Config','Config_Submit']) { const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyAskBlocker', action }, 501); if (!String(blocked.message).startsWith('migration_blocked:')) throw new Error(`旧问答动作 ${action} 未返回明确阻塞原因`); }
   await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'save', className: 'Node', fields: { nodeName: '禁止通用新增' } }, 400);
   const nodeSuffix = Date.now().toString(36);
   const rootNode = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'saveNode', className: 'Node', parentId: 0, fields: { nodeName: `云函数冒烟栏目-${nodeSuffix}`, nodeDir: `smoke-${nodeSuffix}`, nodeType: 1, contentModel: '54' } });
@@ -788,7 +789,7 @@ try {
   const productBlocked = await callLegacyFunction('', { action: 'product_list' }, 501);
   if (!String(productBlocked.retmsg).includes('ZL_Commodities')) throw new Error('缺商品主表的读取接口未明确阻塞');
 
-  console.log('Cloud smoke passed: admin auth/tenant/account lifecycle/group sync/node/special/guest-category/bar/exam-class/exam-point/knowledge/exam-teacher/dictionary/grade-dictionary/currency/holiday/search-navigation/advertising/font-shapes/surveys/customer-service/stores/content-tags/shop-vendor-blockers/user-level/money-ledgers/user-addon-blockers/api-catalog/config-blockers/model/guestbook lifecycle/content workflow/CRUD reads, CMS projections, normalized learning joins, app action coverage, public content, session scope, explicit blocked APIs.');
+  console.log('Cloud smoke passed: admin auth/tenant/account lifecycle/group sync/node/special/guest-category/bar/exam-class/exam-point/knowledge/exam-teacher/dictionary/grade-dictionary/currency/holiday/search-navigation/advertising/font-shapes/surveys/customer-service/stores/content-tags/shop-vendor-blockers/user-level/money-ledgers/user-addon-blockers/ask-blockers/api-catalog/config-blockers/model/guestbook lifecycle/content workflow/CRUD reads, CMS projections, normalized learning joins, app action coverage, public content, session scope, explicit blocked APIs.');
 } finally {
   if (adZoneRestore) await jsonRequest(`${PARSE_URL}/classes/AdZone/${adZoneRestore.objectId}`, { method: 'PUT', body: JSON.stringify({ remind: adZoneRestore.remind, zstatus: adZoneRestore.zstatus }) }, true).catch((error) => { console.error(`广告位冒烟数据恢复失败:${error.message}`); process.exitCode = 1; });
   if (adInfoRestore) await jsonRequest(`${PARSE_URL}/classes/AdInfo/${adInfoRestore.objectId}`, { method: 'PUT', body: JSON.stringify({ remind: adInfoRestore.remind, zstatus: adInfoRestore.zstatus }) }, true).catch((error) => { console.error(`广告内容冒烟数据恢复失败:${error.message}`); process.exitCode = 1; });

+ 6 - 0
src/app/admin/legacy-admin-coverage.generated.spec.ts

@@ -94,6 +94,12 @@ describe('legacy admin coverage inventory', () => {
     expect(byKey.get('User / Addon/ThirdInfo_Submit')?.status).toBe('blocked-infrastructure');
     expect(byKey.get('User / Addon/SigninList')?.status).toBe('blocked-data');
     expect(byKey.get('User / Addon/RealNameAuth_API')?.status).toBe('blocked-data');
+    expect(byKey.get('Content / Ask/Default')?.status).toBe('blocked-data');
+    expect(byKey.get('Content / Ask/Ask_API')?.status).toBe('blocked-data');
+    expect(byKey.get('Content / Ask/AnswerList')?.status).toBe('blocked-data');
+    expect(byKey.get('Content / Ask/Answer_API')?.status).toBe('blocked-data');
+    expect(byKey.get('Content / Ask/Config')?.status).toBe('blocked-infrastructure');
+    expect(byKey.get('Content / Ask/Config_Submit')?.status).toBe('blocked-infrastructure');
     expect(byKey.get('Exam/Teacher_Add')?.status).toBe('implemented');
     expect(byKey.get('User/User_API')?.status).toBe('partial');
     expect(byKey.get('Order/OrderList')?.status).toBe('blocked-data');

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

@@ -251,78 +251,78 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Content / Ask",
     "action": "Default",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 ZL_Ask 问题主表、回收站标记与历史问题数据"
   },
   {
     "module": "Content / Ask",
     "action": "AskInfo",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无问答问题详情;Baike/Guestbook/GuestBar/DesignQuestion 均非等价主表"
   },
   {
     "module": "Content / Ask",
     "action": "AskAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无 Ask 数据类承载 QTitle/Qcontent 与状态、精华、回收站关系"
   },
   {
     "module": "Content / Ask",
     "action": "AskAdd_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "不能把旧问答问题错写到百科、留言、论坛或问卷题目"
   },
   {
     "module": "Content / Ask",
     "action": "Ask_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少可回收、恢复、删除、审核或加精的旧问答问题记录"
   },
   {
     "module": "Content / Ask",
     "action": "AnswerList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 ZL_GuestAnswer 回答主表及 QueId 问题外键"
   },
   {
     "module": "Content / Ask",
     "action": "AnswerAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无可编辑或新增的问答回答记录与问题归属"
   },
   {
     "module": "Content / Ask",
     "action": "AnswerAdd_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少 GuestAnswer QueId/UserId/UserName/Content 数据模型与历史关系"
   },
   {
     "module": "Content / Ask",
     "action": "Answer_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少可审核、取消审核或删除的旧问答回答记录"
   },
   {
     "module": "Content / Ask",
     "action": "Config",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有 GuestConfig.WDOption 问答运行时和用户组权限消费者"
   },
   {
     "module": "Content / Ask",
     "action": "Config_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有问答发布/回复/推荐运行时执行积分与用户组规则"
   },
   {
     "module": "Content / Baike",