Просмотр исходного кода

chore: classify pub and push migrations

彭峰 2 месяцев назад
Родитель
Сommit
559c142f4a

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

@@ -39,6 +39,8 @@
 - `Printer/Index` 的控制器本身只是返回已丢失的 Razor 视图;旧程序集可确认其候选业务依赖 `ZL_Shop_PrintDevice/ZL_Shop_PrintTlp/ZL_Shop_PrintMessage` 和飞印客户端,但签约文档与线上 110 个 Schema 均没有这些设备、模板和消息数据,旧客户端还向 `my.feyin.net` 发送明文 HTTP 请求。无法在不知道页面、历史设备密钥和第三方协议可用性的情况下安全伪造 1:1 打印后台,已由 `legacyPrinterBlocker` 明确登记为基础设施阻塞。
 - `Printer/Index` 的控制器本身只是返回已丢失的 Razor 视图;旧程序集可确认其候选业务依赖 `ZL_Shop_PrintDevice/ZL_Shop_PrintTlp/ZL_Shop_PrintMessage` 和飞印客户端,但签约文档与线上 110 个 Schema 均没有这些设备、模板和消息数据,旧客户端还向 `my.feyin.net` 发送明文 HTTP 请求。无法在不知道页面、历史设备密钥和第三方协议可用性的情况下安全伪造 1:1 打印后台,已由 `legacyPrinterBlocker` 明确登记为基础设施阻塞。
 - `User/PrivacyPolicy` 管理的不是普通静态文案,而是 `ZL_LicenceFiles` 用户协议、强制签署、PDF 表单字段、限定用户/用户组以及 `ZL_LicenceUserSign` 逐用户签约历史。目标 Schema 与签约文档均无这两类数据或协议 PDF,5 个动作复用 `legacyLicenceBlocker` 返回逐项数据阻塞原因,不能用注销通道或任意新文本冒充已经签署过的法律记录。
 - `User/PrivacyPolicy` 管理的不是普通静态文案,而是 `ZL_LicenceFiles` 用户协议、强制签署、PDF 表单字段、限定用户/用户组以及 `ZL_LicenceUserSign` 逐用户签约历史。目标 Schema 与签约文档均无这两类数据或协议 PDF,5 个动作复用 `legacyLicenceBlocker` 返回逐项数据阻塞原因,不能用注销通道或任意新文本冒充已经签署过的法律记录。
 - `User/Promo.PromoList` 已迁为只读 `UserPromotion` 虚拟资源与 `userPromotions/getUserPromotion`:从 `_User.legacyUserData.ParentUserID` 实时还原用户、邀请人、直属推广人数,支持账号/昵称、邀请人 ID、“仅推广人”和推广成员注册日期筛选,日期语义保持旧版只约束直属成员统计。旧 `Cash` 提现申请、预扣退款账本和 `User_Bank` 提现账户均未迁入,提现首页、审核/驳回及账户维护等 10 个动作由 `legacyUserPromoBlocker` 明确阻塞,发票银行字段不会被误作提现账户。
 - `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` 返回逐项数据或基础设施原因,不存储无消费者的密钥,也不伪报消息已发送。
 - 旧 `NodeAdd_Submit`、`Node_API` 与 `UnionNode_Move` 的核心栏目生命周期已迁为专用 `saveNode/nodeBatch`:新增栏目以临时来源键完成 Parse 初始写入,再使用 PostgreSQL 事务锁原子分配 `nodeId/orderId` 并替换为旧 `sourceKey`;保存会校验同级名称/目录重复、父栏目帐套和回收状态,移动会阻断自身/下级循环并递归修正所有后代深度。回收、恢复和每批 1–100 个栏目迁移已接入 Angular;永久删除仅在没有内容、下级栏目、节点权限和模型模板引用时开放,通用保存与删除不能绕过。
 - 旧 `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 物理列缺失,商品主表也未迁移,无法安全改写内容/商品专题关系。
 - 旧 `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/旧模板运行时。
 - 旧 `ModelManage/Field/Order_Submit` 的安全部分已迁为 `saveModelMetadata/saveModelFieldMetadata/modelFieldOrder`:现有模型可编辑名称、条目文案、图标、说明和内容模板元数据,现有字段可编辑别名、提示、显示/搜索配置,并可在同模型内批量排序;`modelId/modelType/tableName/fieldId/modelId/fieldName/fieldType/sourceKey` 等结构字段通用只读。新增、复制、删除模型或字段会显式返回 501,因为旧实现同时执行 PostgreSQL 建表/改列、标签生成和模板文件写入,目标托管环境没有该 DDL/旧模板运行时。

+ 12 - 2
scripts/deploy-admin-functions.mjs

@@ -15,12 +15,12 @@ const CLASS_LABELS = {
   GradeCate: '多级字典分类', Grade: '多级字典选项',
   GradeCate: '多级字典分类', Grade: '多级字典选项',
   Currency: '货币管理', SysHoliday: '节假日管理',
   Currency: '货币管理', SysHoliday: '节假日管理',
   Product: '商品', StoreProduct: '门店商品', StoreApplication: '门店申请', ShopFareTlp: '商城运费模板', ShopMoneyRegular: '金额规则', PayPlat: '支付平台',
   Product: '商品', StoreProduct: '门店商品', StoreApplication: '门店申请', ShopFareTlp: '商城运费模板', ShopMoneyRegular: '金额规则', PayPlat: '支付平台',
-  GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科审核', Search: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', FontPicShape: '字体图形素材', FontPicShapeType: '字体图形分类', DesignAsk: '问卷调查', DesignQuestion: '问卷题目', DesignAnswer: '问卷答卷', ServiceSeat: '客服席位', Temp: '客服欢迎语', StoreApplication: '店铺管理', StoreStyle: '店铺样式', PageStyle: '黄页样式', PlatComp: '协同办公企业', ContentTagKey: '内容标签词库', UserLevel: '积分等级', UserMoneyLog: '资金积分流水', UserPromotion: '用户推广关系', CRMSAttr: 'CRM 客户类型', SysLog: '系统日志', MisType: 'OA 流程类型', Agency: '代理机构', DeliveryCenter: '交付中心'
+  GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科审核', Pub: '互动模块定义', PubTw: '提问互动提交', PubWTHD: '问题回答提交', PubZXDC: '在线调查提交', Search: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', FontPicShape: '字体图形素材', FontPicShapeType: '字体图形分类', DesignAsk: '问卷调查', DesignQuestion: '问卷题目', DesignAnswer: '问卷答卷', ServiceSeat: '客服席位', Temp: '客服欢迎语', StoreApplication: '店铺管理', StoreStyle: '店铺样式', PageStyle: '黄页样式', PlatComp: '协同办公企业', ContentTagKey: '内容标签词库', UserLevel: '积分等级', UserMoneyLog: '资金积分流水', UserPromotion: '用户推广关系', CRMSAttr: 'CRM 客户类型', SysLog: '系统日志', MisType: 'OA 流程类型', Agency: '代理机构', DeliveryCenter: '交付中心'
 };
 };
 const ALLOWED_CLASSES = new Set([
 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','UserMoneyLog','UserPromotion','UserSIcon','UserUserPoint','UserExpDomP','UserExpHis'
   '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','UserMoneyLog','UserPromotion','UserSIcon','UserUserPoint','UserExpDomP','UserExpHis'
 ]);
 ]);
-const READ_ONLY_CLASSES = new Set(['_Role','Permission','PayPlat','ThirdPlatInfo','GuestBar','DesignAnswer','Baike','ExamSysPapers','ExamSysQuestions','PaperQuestions','ExamType','ContentPublish','PublishNode','SysLog']);
+const READ_ONLY_CLASSES = new Set(['_Role','Permission','PayPlat','ThirdPlatInfo','GuestBar','DesignAnswer','Baike','ExamSysPapers','ExamSysQuestions','PaperQuestions','ExamType','ContentPublish','PublishNode','SysLog','Pub','PubTw','PubWTHD','PubZXDC']);
 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','legacyUserPlat','groupId']);
 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','legacyUserPlat','groupId']);
 const HIDDEN_FIELDS = new Set(['password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','apiKey','appSecret','secret','token']);
 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']);
 const GENERIC_WRITE_TYPES = new Set(['String','Number','Boolean','Date','Pointer','Object','Array']);
@@ -1496,6 +1496,16 @@ async function handler(request, response) {
         Index: '旧入口只重定向到提现申请页,目标缺少 Cash 提现主表,无法提供等价落点', WD: '目标 Schema 缺少 Cash 提现申请、手续费、结果、凭证与处理状态记录', WD_API: '目标没有可审核的提现申请记录', WDAudit: '目标缺少指定提现申请的金额、手续费、账户和处理详情', WDAudit_Accept: '目标缺少可原子审核的 Cash 提现事务记录', WDAudit_Reject: '目标缺少 Cash 提现申请及对应预扣余额账本,不能安全执行拒绝退款', UserBank: '目标 Schema 缺少 User_Bank 提现账户及审核状态数据', UserBankAdd: '目标没有可编辑的开户人、卡号、账户类型和开户行记录', UserBankAdd_Submit: '目标缺少 User_Bank 持久层,不能把提现账户错写到发票银行字段', UserBank_API: '目标没有可删除、审核或取消审核的提现账户记录'
         Index: '旧入口只重定向到提现申请页,目标缺少 Cash 提现主表,无法提供等价落点', WD: '目标 Schema 缺少 Cash 提现申请、手续费、结果、凭证与处理状态记录', WD_API: '目标没有可审核的提现申请记录', WDAudit: '目标缺少指定提现申请的金额、手续费、账户和处理详情', WDAudit_Accept: '目标缺少可原子审核的 Cash 提现事务记录', WDAudit_Reject: '目标缺少 Cash 提现申请及对应预扣余额账本,不能安全执行拒绝退款', UserBank: '目标 Schema 缺少 User_Bank 提现账户及审核状态数据', UserBankAdd: '目标没有可编辑的开户人、卡号、账户类型和开户行记录', UserBankAdd_Submit: '目标缺少 User_Bank 持久层,不能把提现账户错写到发票银行字段', UserBank_API: '目标没有可删除、审核或取消审核的提现账户记录'
       }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧推广提现动作'); fail(501, 'migration_blocked: ' + blockers[action]);
       }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧推广提现动作'); fail(501, 'migration_blocked: ' + blockers[action]);
     }
     }
+    if (operation === 'legacyPubBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        PubManage: '目标 Pub 模块定义与 PubTw/PubWTHD/PubZXDC 提交类均为空,无法还原互动模块列表', Pub_API: '旧删除、复制、恢复依赖 Pub 定义与动态表,modelname 还会检查 SQL 物理表;目标没有可操作记录或动态建表运行时', Pubinfo: '旧动作只跳转到指定 Pub 定义编辑页,目标 Pub 没有任何历史模块', PubAdd: '新增互动模块需要动态模型、表、标签与模板选择,目标云函数没有等价创建运行时', PubAdd_Submit: '旧提交会动态建 SQL 表、Model/ModelField、XML 标签及两套服务器 HTML 模板,云函数不能等价执行', Pubsinfo: '目标没有 Pub 定义可确定提交表,三个已签约 Pub 提交类也全部为空', PubsinfoAdd: '目标缺少互动模块定义、动态字段与可编辑的提交记录', PubsinfoAdd_Submit: '目标无法根据空的 Pub 定义选择动态提交表并保存系统字段与模型字段', PubInfo_API: '目标没有可删除、审核或取消审核的互动提交记录', PubInfo2_API: '旧企业入驻分支硬编码依赖缺失的 ZL_Pub_BDQYSH 提交表,不能直接改写企业与用户归属', PubsinfoReply: '目标没有互动父记录、动态字段和回复上下文', PubsinfoReply_Submit: '目标缺少可确定回复表的 Pub 定义及父提交记录'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧互动模块动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
+    if (operation === 'legacyPushBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        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 === 'legacyLogBlocker') {
     if (operation === 'legacyLogBlocker') {
       const action = String(input.action || ''); const blockers = {
       const action = String(input.action || ''); const blockers = {
         TxtLog: '云函数运行时没有旧 /Log/{LogType}/ 本地日志目录', TxtLogContent: '云函数不能按文件名读取旧服务器文本日志', TxtLog_Down: '云函数没有旧服务器日志文件可下载',
         TxtLog: '云函数运行时没有旧 /Log/{LogType}/ 本地日志目录', TxtLogContent: '云函数不能按文件名读取旧服务器文本日志', TxtLog_Down: '云函数没有旧服务器日志文件可下载',

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

@@ -269,6 +269,30 @@ const explicit = new Map([
   ['User / Promo/UserBankAdd', ['blocked-data', '目标没有可编辑的开户人、卡号、账户类型和开户行记录']],
   ['User / Promo/UserBankAdd', ['blocked-data', '目标没有可编辑的开户人、卡号、账户类型和开户行记录']],
   ['User / Promo/UserBankAdd_Submit', ['blocked-data', '目标缺少 User_Bank 持久层,不能把提现账户错写到发票银行字段']],
   ['User / Promo/UserBankAdd_Submit', ['blocked-data', '目标缺少 User_Bank 持久层,不能把提现账户错写到发票银行字段']],
   ['User / Promo/UserBank_API', ['blocked-data', '目标没有可删除、审核或取消审核的提现账户记录']],
   ['User / Promo/UserBank_API', ['blocked-data', '目标没有可删除、审核或取消审核的提现账户记录']],
+  ['Pub/PubManage', ['blocked-data', '目标 Pub 模块定义与 PubTw/PubWTHD/PubZXDC 提交类均为空,无法还原互动模块列表']],
+  ['Pub/Pub_API', ['blocked-infrastructure', '旧删除/复制/恢复依赖 Pub 定义与动态表,modelname 还会检查 SQL 物理表;目标没有可操作记录或动态建表运行时']],
+  ['Pub/Pubinfo', ['blocked-data', '旧动作只跳转到指定 Pub 定义编辑页,目标 Pub 没有任何历史模块']],
+  ['Pub/PubAdd', ['blocked-infrastructure', '新增互动模块需要动态模型、表、标签与模板选择,目标云函数没有等价创建运行时']],
+  ['Pub/PubAdd_Submit', ['blocked-infrastructure', '旧提交会动态建 SQL 表、Model/ModelField、XML 标签及两套服务器 HTML 模板,云函数不能等价执行']],
+  ['Pub/Pubsinfo', ['blocked-data', '目标没有 Pub 定义可确定提交表,三个已签约 Pub 提交类也全部为空']],
+  ['Pub/PubsinfoAdd', ['blocked-data', '目标缺少互动模块定义、动态字段与可编辑的提交记录']],
+  ['Pub/PubsinfoAdd_Submit', ['blocked-data', '目标无法根据空的 Pub 定义选择动态提交表并保存系统字段与模型字段']],
+  ['Pub/PubInfo_API', ['blocked-data', '目标没有可删除、审核或取消审核的互动提交记录']],
+  ['Pub/PubInfo2_API', ['blocked-data', '旧企业入驻分支硬编码依赖缺失的 ZL_Pub_BDQYSH 提交表,不能直接改写企业与用户归属']],
+  ['Pub/PubsinfoReply', ['blocked-data', '目标没有互动父记录、动态字段和回复上下文']],
+  ['Pub/PubsinfoReply_Submit', ['blocked-data', '目标缺少可确定回复表的 Pub 定义及父提交记录']],
+  ['Push/Index', ['blocked-data', '旧入口只跳转到推送 API 列表,目标缺少 Mobile_PushAPI 配置数据']],
+  ['Push/APIList', ['blocked-data', '目标 Schema 缺少 Mobile_PushAPI 的别名、平台、AppKey 与密钥记录']],
+  ['Push/API_del', ['blocked-data', '目标没有可定位并删除的推送服务配置']],
+  ['Push/AddAPI', ['blocked-data', '目标没有可编辑或新增的极光推送 API 配置模型']],
+  ['Push/AddAPI_Submit', ['blocked-data', '签约数据库未提供可安全存储和轮换推送 AppKey/AppSecret 的配置类']],
+  ['Push/PushMsg', ['blocked-data', '目标缺少可选择的推送 API 配置与设备受众数据']],
+  ['Push/PushMsg_Submit', ['blocked-infrastructure', '目标没有 JPush 凭据、SDK 消费者、ZL_UserBase.UC 设备注册号或移动端注册链,无法真实发送并记录结果']],
+  ['Push/PushTlp', ['blocked-data', '目标 Schema 缺少 Mobile_PushTlp 推送模板与状态数据']],
+  ['Push/AddPushTlp', ['blocked-data', '目标没有可编辑或新增的推送模板记录']],
+  ['Push/AddPushTlp_Submit', ['blocked-data', '目标缺少推送模板名称、标题、内容和参数持久层']],
+  ['Push/PushTlp_API', ['blocked-data', '目标没有可启用、停用或删除的推送模板']],
+  ['Push/MsgList', ['blocked-data', '目标 Schema 缺少 Mobile_PushMsg 发送历史、受众与结果记录']],
   ['Design / Interactive/InteractiveList', ['blocked-data', '目标缺少旧 Design_Pub 交互提交表及 sceneid、uid、fname 数据;DesignAnswer/Pub 不等价']],
   ['Design / Interactive/InteractiveList', ['blocked-data', '目标缺少旧 Design_Pub 交互提交表及 sceneid、uid、fname 数据;DesignAnswer/Pub 不等价']],
   ['Design / Interactive/Interactive_API', ['blocked-data', '目标没有可按旧 Design_Pub ID 删除的交互提交记录']],
   ['Design / Interactive/Interactive_API', ['blocked-data', '目标没有可按旧 Design_Pub ID 删除的交互提交记录']],
   ['Office / iServer/Default', ['blocked-data', '目标 Schema 缺少旧 IServer 服务工单主表与回复表']],
   ['Office / iServer/Default', ['blocked-data', '目标 Schema 缺少旧 IServer 服务工单主表与回复表']],

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

@@ -714,6 +714,9 @@ try {
   }
   }
   const promotionSchema = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'schema', className: 'UserPromotion' }); if (promotionSchema.writable !== false || promotionSchema.creatable !== false || !promotionSchema.fields?.some((field) => field.name === 'promotionCount' && field.writable === false)) throw new Error('用户推广关系 Schema 应保持只读'); const promotionPage = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'userPromotions', className: 'UserPromotion', page: 1, pageSize: 20, search: '', parentUserId: 0, onlyWithChildren: false, startDate: '', endDate: '' }); if (!Number.isInteger(promotionPage.total) || !Array.isArray(promotionPage.results)) throw new Error('用户推广关系列表响应异常'); const promotionSample = promotionPage.results?.[0]; if (promotionSample) { const promotionDetail = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'getUserPromotion', className: 'UserPromotion', objectId: promotionSample.objectId }); if (promotionDetail.objectId !== promotionSample.objectId || !Number.isInteger(Number(promotionDetail.promotionCount))) throw new Error('用户推广关系详情异常'); } const promotionParents = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'userPromotions', className: 'UserPromotion', page: 1, pageSize: 20, search: '', parentUserId: 0, onlyWithChildren: true, startDate: '', endDate: '' }); if (promotionParents.results?.some((entry) => Number(entry.promotionCount) < 1)) throw new Error('仅推广人筛选返回了无成员用户');
   const promotionSchema = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'schema', className: 'UserPromotion' }); if (promotionSchema.writable !== false || promotionSchema.creatable !== false || !promotionSchema.fields?.some((field) => field.name === 'promotionCount' && field.writable === false)) throw new Error('用户推广关系 Schema 应保持只读'); const promotionPage = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'userPromotions', className: 'UserPromotion', page: 1, pageSize: 20, search: '', parentUserId: 0, onlyWithChildren: false, startDate: '', endDate: '' }); if (!Number.isInteger(promotionPage.total) || !Array.isArray(promotionPage.results)) throw new Error('用户推广关系列表响应异常'); const promotionSample = promotionPage.results?.[0]; if (promotionSample) { const promotionDetail = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'getUserPromotion', className: 'UserPromotion', objectId: promotionSample.objectId }); if (promotionDetail.objectId !== promotionSample.objectId || !Number.isInteger(Number(promotionDetail.promotionCount))) throw new Error('用户推广关系详情异常'); } const promotionParents = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'userPromotions', className: 'UserPromotion', page: 1, pageSize: 20, search: '', parentUserId: 0, onlyWithChildren: true, startDate: '', endDate: '' }); if (promotionParents.results?.some((entry) => Number(entry.promotionCount) < 1)) throw new Error('仅推广人筛选返回了无成员用户');
   for (const action of ['Index','WD','WD_API','WDAudit','WDAudit_Accept','WDAudit_Reject','UserBank','UserBankAdd','UserBankAdd_Submit','UserBank_API']) { const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyUserPromoBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧推广提现动作 ${action} 未返回明确迁移阻塞`); }
   for (const action of ['Index','WD','WD_API','WDAudit','WDAudit_Accept','WDAudit_Reject','UserBank','UserBankAdd','UserBankAdd_Submit','UserBank_API']) { const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyUserPromoBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧推广提现动作 ${action} 未返回明确迁移阻塞`); }
+  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 ['TxtLog','TxtLogContent','TxtLog_Down','TaskList','TaskAdd','TaskAdd_API','TaskAdd_Submit','Task_API','TaskCenter']) {
   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} 未返回明确迁移阻塞`);
     const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyLogBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧日志任务动作 ${action} 未返回明确迁移阻塞`);
   }
   }

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

@@ -56,6 +56,10 @@ describe('legacy admin coverage inventory', () => {
     expect(byKey.get('User / PrivacyPolicy/LicenceUserSign')?.status).toBe('blocked-data');
     expect(byKey.get('User / PrivacyPolicy/LicenceUserSign')?.status).toBe('blocked-data');
     expect(byKey.get('User / Promo/PromoList')?.status).toBe('implemented');
     expect(byKey.get('User / Promo/PromoList')?.status).toBe('implemented');
     expect(byKey.get('User / Promo/WDAudit_Reject')?.status).toBe('blocked-data');
     expect(byKey.get('User / Promo/WDAudit_Reject')?.status).toBe('blocked-data');
+    expect(byKey.get('Pub/PubManage')?.status).toBe('blocked-data');
+    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('Config/FontIcon')?.status).toBe('implemented');
     expect(byKey.get('Config/FontIcon')?.status).toBe('implemented');
     expect(byKey.get('Config/Hotkey_API')?.status).toBe('blocked-infrastructure');
     expect(byKey.get('Config/Hotkey_API')?.status).toBe('blocked-infrastructure');
     expect(byKey.get('Config/Prize_Submit')?.status).toBe('blocked-infrastructure');
     expect(byKey.get('Config/Prize_Submit')?.status).toBe('blocked-infrastructure');

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

@@ -4920,169 +4920,169 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Pub",
     "module": "Pub",
     "action": "PubManage",
     "action": "PubManage",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Pub 模块定义与 PubTw/PubWTHD/PubZXDC 提交类均为空,无法还原互动模块列表"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "Pub_API",
     "action": "Pub_API",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "旧删除/复制/恢复依赖 Pub 定义与动态表,modelname 还会检查 SQL 物理表;目标没有可操作记录或动态建表运行时"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "Pubinfo",
     "action": "Pubinfo",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "旧动作只跳转到指定 Pub 定义编辑页,目标 Pub 没有任何历史模块"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "PubAdd",
     "action": "PubAdd",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "新增互动模块需要动态模型、表、标签与模板选择,目标云函数没有等价创建运行时"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "PubAdd_Submit",
     "action": "PubAdd_Submit",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "旧提交会动态建 SQL 表、Model/ModelField、XML 标签及两套服务器 HTML 模板,云函数不能等价执行"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "Pubsinfo",
     "action": "Pubsinfo",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有 Pub 定义可确定提交表,三个已签约 Pub 提交类也全部为空"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "PubsinfoAdd",
     "action": "PubsinfoAdd",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少互动模块定义、动态字段与可编辑的提交记录"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "PubsinfoAdd_Submit",
     "action": "PubsinfoAdd_Submit",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无法根据空的 Pub 定义选择动态提交表并保存系统字段与模型字段"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "PubInfo_API",
     "action": "PubInfo_API",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可删除、审核或取消审核的互动提交记录"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "PubInfo2_API",
     "action": "PubInfo2_API",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "旧企业入驻分支硬编码依赖缺失的 ZL_Pub_BDQYSH 提交表,不能直接改写企业与用户归属"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "PubsinfoReply",
     "action": "PubsinfoReply",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有互动父记录、动态字段和回复上下文"
   },
   },
   {
   {
     "module": "Pub",
     "module": "Pub",
     "action": "PubsinfoReply_Submit",
     "action": "PubsinfoReply_Submit",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少可确定回复表的 Pub 定义及父提交记录"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "Index",
     "action": "Index",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "旧入口只跳转到推送 API 列表,目标缺少 Mobile_PushAPI 配置数据"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "APIList",
     "action": "APIList",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少 Mobile_PushAPI 的别名、平台、AppKey 与密钥记录"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "API_del",
     "action": "API_del",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可定位并删除的推送服务配置"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "AddAPI",
     "action": "AddAPI",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可编辑或新增的极光推送 API 配置模型"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "AddAPI_Submit",
     "action": "AddAPI_Submit",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "签约数据库未提供可安全存储和轮换推送 AppKey/AppSecret 的配置类"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "PushMsg",
     "action": "PushMsg",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少可选择的推送 API 配置与设备受众数据"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "PushMsg_Submit",
     "action": "PushMsg_Submit",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有 JPush 凭据、SDK 消费者、ZL_UserBase.UC 设备注册号或移动端注册链,无法真实发送并记录结果"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "PushTlp",
     "action": "PushTlp",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少 Mobile_PushTlp 推送模板与状态数据"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "AddPushTlp",
     "action": "AddPushTlp",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可编辑或新增的推送模板记录"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "AddPushTlp_Submit",
     "action": "AddPushTlp_Submit",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少推送模板名称、标题、内容和参数持久层"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "PushTlp_API",
     "action": "PushTlp_API",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可启用、停用或删除的推送模板"
   },
   },
   {
   {
     "module": "Push",
     "module": "Push",
     "action": "MsgList",
     "action": "MsgList",
     "occurrence": 1,
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少 Mobile_PushMsg 发送历史、受众与结果记录"
   },
   },
   {
   {
     "module": "Extend / PWA",
     "module": "Extend / PWA",