瀏覽代碼

chore: classify pwa and quartz migrations

彭峰 1 月之前
父節點
當前提交
6eca1ed960

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

@@ -41,6 +41,8 @@
 - `User/Promo.PromoList` 已迁为只读 `UserPromotion` 虚拟资源与 `userPromotions/getUserPromotion`:从 `_User.legacyUserData.ParentUserID` 实时还原用户、邀请人、直属推广人数,支持账号/昵称、邀请人 ID、“仅推广人”和推广成员注册日期筛选,日期语义保持旧版只约束直属成员统计。旧 `Cash` 提现申请、预扣退款账本和 `User_Bank` 提现账户均未迁入,提现首页、审核/驳回及账户维护等 10 个动作由 `legacyUserPromoBlocker` 明确阻塞,发票银行字段不会被误作提现账户。
 - 旧 `Pub` 互动模块新增链会同时创建动态 SQL 表、Model/ModelField、XML 调用标签与两套服务器 HTML 模板;目标虽签约了 `Pub/PubTw/PubWTHD/PubZXDC` 四类旧字段 Schema,但线上四类真实记录均为 0,也没有 `ZL_Pub_BDQYSH` 企业入驻表或旧模板/标签运行时。四类已从通用写入锁为只读,12 个模块定义、提交审核、企业入驻和回复动作由 `legacyPubBlocker` 返回逐项数据或基础设施原因,避免空 Schema 被误报为已迁移能力。
 - 旧 `Push` 后台依赖 `Mobile_PushAPI/Mobile_PushTlp/Mobile_PushMsg` 三张表、极光 AppKey/AppSecret、服务端 JPush SDK 和 `ZL_UserBase.UC` 设备注册号。签约文档与线上 110 个 Schema 均没有 push/notification/device/message 数据类,当前客户端也没有等价设备注册消费者;12 个配置、模板、发送和历史入口由 `legacyPushBlocker` 返回逐项数据或基础设施原因,不存储无消费者的密钥,也不伪报消息已发送。
+- 旧 `Extend/PWA` 会在 ASP.NET 站点上读写 `SiteConfig.SiteOption`,缩放八种图标,生成 `manifest.json`,注入页面 head,并从 `pwa_sw_js.tlp` 覆盖站点根目录 `/sw.js`。当前 Angular 构建没有 service-worker 发布链,云函数也不能修改已部署静态资产;6 个页面和提交动作由 `legacyPWABlocker` 明确为基础设施阻塞,不提供“保存成功但客户端不生效”的伪配置。
+- `Extend/QuartzTask` 管理的是 `QuartzNetTask` 任务记录、任务内日志及常驻进程中的 `IScheduler/ScheduleTaskHandler`,创建、更新、立即执行、暂停、恢复和删除都必须同步调度器。目标只存在语义不同且为空的 `SenTask`,云函数没有 Quartz 控制面;含 GET/POST 两个同名 `TaskAdd` 在内的 11 个动作由 `legacyQuartzTaskBlocker` 分别返回数据或基础设施阻塞原因。
 - 旧 `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/旧模板运行时。

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

@@ -1506,6 +1506,16 @@ async function handler(request, response) {
         Index: '旧入口只跳转到推送 API 列表,目标缺少 Mobile_PushAPI 配置数据', APIList: '目标 Schema 缺少 Mobile_PushAPI 的别名、平台、AppKey 与密钥记录', API_del: '目标没有可定位并删除的推送服务配置', AddAPI: '目标没有可编辑或新增的极光推送 API 配置模型', AddAPI_Submit: '签约数据库未提供可安全存储和轮换推送 AppKey/AppSecret 的配置类', PushMsg: '目标缺少可选择的推送 API 配置与设备受众数据', PushMsg_Submit: '目标没有 JPush 凭据、SDK 消费者、ZL_UserBase.UC 设备注册号或移动端注册链,无法真实发送并记录结果', PushTlp: '目标 Schema 缺少 Mobile_PushTlp 推送模板与状态数据', AddPushTlp: '目标没有可编辑或新增的推送模板记录', AddPushTlp_Submit: '目标缺少推送模板名称、标题、内容和参数持久层', PushTlp_API: '目标没有可启用、停用或删除的推送模板', MsgList: '目标 Schema 缺少 Mobile_PushMsg 发送历史、受众与结果记录'
       }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧推送动作'); fail(501, 'migration_blocked: ' + blockers[action]);
     }
+    if (operation === 'legacyPWABlocker') {
+      const action = String(input.action || ''); const blockers = {
+        Default: '旧入口跳转到离线缓存配置;目标没有可动态改写已部署前端的 PWA 控制面', Default_Submit: '旧提交为空操作,目标不伪造无效果的保存成功', Mobile: '目标缺少旧 SiteOption PWA 配置、manifest 文件和多尺寸图标资产', Mobile_Submit: '云函数不能缩放上传图标、写 manifest.json、注入站点 head 或持久化旧 SiteConfig', Offline: '目标没有旧 PWA_PC_Enable/Exts/IndexUrl/PreCache 配置与服务器模板', Offline_Submit: '云函数不能读取 pwa_sw_js.tlp 并覆盖已部署站点根目录 /sw.js'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧 PWA 动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
+    if (operation === 'legacyQuartzTaskBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        TaskList: '目标 Schema 缺少 QuartzNetTask 任务定义、类型、状态、次数和历史记录;空 SenTask 语义不等价', ViewLog: '目标没有 QuartzNetTask.Log 执行日志可按任务查看', TaskAdd: '目标没有 Quartz IScheduler、ScheduleTaskHandler 与任务持久层,无法创建一次或循环任务', TaskEdit: '目标没有可编辑的 Quartz 任务定义', TaskUpdate: '目标没有可同步更新持久层与 Quartz 调度器的控制面', ExecuteNow: '云函数环境没有已注册的 Quartz 作业可立即触发', TaskDelete: '目标没有可同时删除 Quartz 作业与任务记录的调度控制面', TaskDeleteRange: '目标没有可批量注销 Quartz 作业与删除任务记录的控制面', TaskPause: '云函数环境没有常驻 Quartz 调度器或可暂停作业', TaskResume: '云函数环境没有常驻 Quartz 调度器或可恢复作业'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧 Quartz 任务动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
     if (operation === 'legacyLogBlocker') {
       const action = String(input.action || ''); const blockers = {
         TxtLog: '云函数运行时没有旧 /Log/{LogType}/ 本地日志目录', TxtLogContent: '云函数不能按文件名读取旧服务器文本日志', TxtLog_Down: '云函数没有旧服务器日志文件可下载',

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

@@ -293,6 +293,22 @@ const explicit = new Map([
   ['Push/AddPushTlp_Submit', ['blocked-data', '目标缺少推送模板名称、标题、内容和参数持久层']],
   ['Push/PushTlp_API', ['blocked-data', '目标没有可启用、停用或删除的推送模板']],
   ['Push/MsgList', ['blocked-data', '目标 Schema 缺少 Mobile_PushMsg 发送历史、受众与结果记录']],
+  ['Extend / PWA/Default', ['blocked-infrastructure', '旧入口跳转到离线缓存配置;目标没有可动态改写已部署前端的 PWA 控制面']],
+  ['Extend / PWA/Default_Submit', ['blocked-infrastructure', '旧提交为空操作,目标不伪造无效果的保存成功']],
+  ['Extend / PWA/Mobile', ['blocked-infrastructure', '目标缺少旧 SiteOption PWA 配置、manifest 文件和多尺寸图标资产']],
+  ['Extend / PWA/Mobile_Submit', ['blocked-infrastructure', '云函数不能缩放上传图标、写 manifest.json、注入站点 head 或持久化旧 SiteConfig']],
+  ['Extend / PWA/Offline', ['blocked-infrastructure', '目标没有旧 PWA_PC_Enable/Exts/IndexUrl/PreCache 配置与服务器模板']],
+  ['Extend / PWA/Offline_Submit', ['blocked-infrastructure', '云函数不能读取 pwa_sw_js.tlp 并覆盖已部署站点根目录 /sw.js']],
+  ['Extend / QuartzTask/TaskList', ['blocked-data', '目标 Schema 缺少 QuartzNetTask 任务定义、类型、状态、次数和历史记录;空 SenTask 语义不等价']],
+  ['Extend / QuartzTask/ViewLog', ['blocked-data', '目标没有 QuartzNetTask.Log 执行日志可按任务查看']],
+  ['Extend / QuartzTask/TaskAdd', ['blocked-infrastructure', '目标没有 Quartz IScheduler、ScheduleTaskHandler 与任务持久层,无法创建一次或循环任务']],
+  ['Extend / QuartzTask/TaskEdit', ['blocked-data', '目标没有可编辑的 Quartz 任务定义']],
+  ['Extend / QuartzTask/TaskUpdate', ['blocked-infrastructure', '目标没有可同步更新持久层与 Quartz 调度器的控制面']],
+  ['Extend / QuartzTask/ExecuteNow', ['blocked-infrastructure', '云函数环境没有已注册的 Quartz 作业可立即触发']],
+  ['Extend / QuartzTask/TaskDelete', ['blocked-infrastructure', '目标没有可同时删除 Quartz 作业与任务记录的调度控制面']],
+  ['Extend / QuartzTask/TaskDeleteRange', ['blocked-infrastructure', '目标没有可批量注销 Quartz 作业与删除任务记录的控制面']],
+  ['Extend / QuartzTask/TaskPause', ['blocked-infrastructure', '云函数环境没有常驻 Quartz 调度器或可暂停作业']],
+  ['Extend / QuartzTask/TaskResume', ['blocked-infrastructure', '云函数环境没有常驻 Quartz 调度器或可恢复作业']],
   ['Design / Interactive/InteractiveList', ['blocked-data', '目标缺少旧 Design_Pub 交互提交表及 sceneid、uid、fname 数据;DesignAnswer/Pub 不等价']],
   ['Design / Interactive/Interactive_API', ['blocked-data', '目标没有可按旧 Design_Pub ID 删除的交互提交记录']],
   ['Office / iServer/Default', ['blocked-data', '目标 Schema 缺少旧 IServer 服务工单主表与回复表']],

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

@@ -717,6 +717,8 @@ try {
   for (const className of ['Pub','PubTw','PubWTHD','PubZXDC']) { const schema = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'schema', className }); if (schema.writable !== false || schema.fields?.some((field) => field.writable)) throw new Error(`${className} 空互动类必须保持只读`); }
   for (const action of ['PubManage','Pub_API','Pubinfo','PubAdd','PubAdd_Submit','Pubsinfo','PubsinfoAdd','PubsinfoAdd_Submit','PubInfo_API','PubInfo2_API','PubsinfoReply','PubsinfoReply_Submit']) { const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyPubBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧互动模块动作 ${action} 未返回明确迁移阻塞`); }
   for (const action of ['Index','APIList','API_del','AddAPI','AddAPI_Submit','PushMsg','PushMsg_Submit','PushTlp','AddPushTlp','AddPushTlp_Submit','PushTlp_API','MsgList']) { const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyPushBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧推送动作 ${action} 未返回明确迁移阻塞`); }
+  for (const action of ['Default','Default_Submit','Mobile','Mobile_Submit','Offline','Offline_Submit']) { const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyPWABlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧 PWA 动作 ${action} 未返回明确迁移阻塞`); }
+  for (const action of ['TaskList','ViewLog','TaskAdd','TaskEdit','TaskUpdate','ExecuteNow','TaskDelete','TaskDeleteRange','TaskPause','TaskResume']) { const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyQuartzTaskBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧 Quartz 任务动作 ${action} 未返回明确迁移阻塞`); }
   for (const action of ['TxtLog','TxtLogContent','TxtLog_Down','TaskList','TaskAdd','TaskAdd_API','TaskAdd_Submit','Task_API','TaskCenter']) {
     const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyLogBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧日志任务动作 ${action} 未返回明确迁移阻塞`);
   }

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

@@ -60,6 +60,9 @@ describe('legacy admin coverage inventory', () => {
     expect(byKey.get('Pub/PubAdd_Submit')?.status).toBe('blocked-infrastructure');
     expect(byKey.get('Push/APIList')?.status).toBe('blocked-data');
     expect(byKey.get('Push/PushMsg_Submit')?.status).toBe('blocked-infrastructure');
+    expect(byKey.get('Extend / PWA/Offline_Submit')?.status).toBe('blocked-infrastructure');
+    expect(byKey.get('Extend / QuartzTask/TaskAdd')?.status).toBe('blocked-infrastructure');
+    expect(byKey.get('Extend / QuartzTask/ViewLog')?.status).toBe('blocked-data');
     expect(byKey.get('Config/FontIcon')?.status).toBe('implemented');
     expect(byKey.get('Config/Hotkey_API')?.status).toBe('blocked-infrastructure');
     expect(byKey.get('Config/Prize_Submit')?.status).toBe('blocked-infrastructure');

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

@@ -5088,120 +5088,120 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Extend / PWA",
     "action": "Default",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "旧入口跳转到离线缓存配置;目标没有可动态改写已部署前端的 PWA 控制面"
   },
   {
     "module": "Extend / PWA",
     "action": "Default_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "旧提交为空操作,目标不伪造无效果的保存成功"
   },
   {
     "module": "Extend / PWA",
     "action": "Mobile",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标缺少旧 SiteOption PWA 配置、manifest 文件和多尺寸图标资产"
   },
   {
     "module": "Extend / PWA",
     "action": "Mobile_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "云函数不能缩放上传图标、写 manifest.json、注入站点 head 或持久化旧 SiteConfig"
   },
   {
     "module": "Extend / PWA",
     "action": "Offline",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有旧 PWA_PC_Enable/Exts/IndexUrl/PreCache 配置与服务器模板"
   },
   {
     "module": "Extend / PWA",
     "action": "Offline_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "云函数不能读取 pwa_sw_js.tlp 并覆盖已部署站点根目录 /sw.js"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "TaskList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少 QuartzNetTask 任务定义、类型、状态、次数和历史记录;空 SenTask 语义不等价"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "ViewLog",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有 QuartzNetTask.Log 执行日志可按任务查看"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "TaskAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有 Quartz IScheduler、ScheduleTaskHandler 与任务持久层,无法创建一次或循环任务"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "TaskAdd",
     "occurrence": 2,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有 Quartz IScheduler、ScheduleTaskHandler 与任务持久层,无法创建一次或循环任务"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "TaskEdit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可编辑的 Quartz 任务定义"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "TaskUpdate",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有可同步更新持久层与 Quartz 调度器的控制面"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "ExecuteNow",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "云函数环境没有已注册的 Quartz 作业可立即触发"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "TaskDelete",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有可同时删除 Quartz 作业与任务记录的调度控制面"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "TaskDeleteRange",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有可批量注销 Quartz 作业与删除任务记录的控制面"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "TaskPause",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "云函数环境没有常驻 Quartz 调度器或可暂停作业"
   },
   {
     "module": "Extend / QuartzTask",
     "action": "TaskResume",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "云函数环境没有常驻 Quartz 调度器或可恢复作业"
   },
   {
     "module": "Shop / Refund",