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

feat: migrate system logs and legacy entry coverage

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

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

@@ -29,6 +29,8 @@
 - Exam 模块已把仍有历史数据的 `ExamSysPapers/ExamSysQuestions/PaperQuestions/ExamType/ContentPublish/PublishNode` 接入 Angular 只读列表、搜索、分页和详情,并关闭通用写入,防止破坏试卷 `qids/questList`、复合答案和报刊父子结构。教材版本、学校、班级、班级学员、旧 Course、答卷评分数据均未迁入;对应 21 个动作由 `legacyExamBlocker` 返回数据或运行时阻塞。`CourseBinding` 明确不冒充旧考试 Course。
 - 商城 `DeliverType/DeliverAdd/DeliverAdd_Submit/Deliver_API` 已由 `ShopFareTlp` 真实历史数据、`shopFareTemplates/saveShopFareTemplate/shopFareTemplateBatch` 和 Angular“商城运费模板”页面迁移:校验 Express JSON、名称防重并原子分配旧数字 ID。订单/物流单、Manufacturers 厂商和 Trademark 品牌表均不存在,剩余 12 个动作返回细分阻塞;`DeliveryCenter` 不被误作运费模板。
 - Helper 的数据库自初始化与帮助精灵依赖服务器 `DBConfig.json`、本地 SQL 文件、旧 Notification 数据和 SignalR 分组协议,目标均不存在,两个动作由 `legacyHelperBlocker` 明确阻塞。Hms 的 10 个动作管理的是另一个产品“逐浪主题大师”的七档会员卡,依赖 `ZL_UserBase.cardType/ICQ` 及 `ZL_Orderinfo/ZL_CartPro`;迁移数据没有这些字段或表,不能误用小树现有 `VIP` 合伙人等级,已由 `legacyHmsBlocker` 逐项说明。
+- `Index` 后台入口控制器的 Default/Main/WorkTable、Login/Login_AJAX、Logout 已由 Angular 后台壳、管理概览、Parse 登录与会话撤销等价替代;FuncList/SiteMap 由侧栏、资源目录和后台快捷入口部分覆盖。旧主题桌面、ASP.NET Session 锁屏、动态 ASCX/Razor 加载、全局动画开关及公开管理员申请缺少服务器配置或安全审批链,8 个动作由 `legacyIndexBlocker` 明确阻塞,绝不复刻随机密码加固定高权限角色的不安全流程。
+- `Extend/Log.SysLog` 已用线上 470 条 `SysLog` 实际记录迁为只读 `systemLogs` 云函数和 Angular“系统日志”页,支持帐套隔离、分页、一级/二级类型、关键词与日期筛选,并禁止通用写入。旧文本日志目录和 Hangfire 任务中心缺少文件系统、`Content_ScheTask` 数据及调度控制面;`SenTask` 为空且语义不等价。Design/Interactive、Office/iServer、Office/Licence 也分别缺少 `Design_Pub`、服务工单/回复、协议/申请/签约主表及 PDF 文件运行时,合计 32 个动作由专用阻塞契约逐项返回原因。
 - 旧 `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/旧模板运行时。

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

@@ -15,12 +15,12 @@ const CLASS_LABELS = {
   GradeCate: '多级字典分类', Grade: '多级字典选项',
   Currency: '货币管理', SysHoliday: '节假日管理',
   Product: '商品', StoreProduct: '门店商品', StoreApplication: '门店申请', ShopFareTlp: '商城运费模板', ShopMoneyRegular: '金额规则', PayPlat: '支付平台',
-  GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科审核', Search: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', FontPicShape: '字体图形素材', FontPicShapeType: '字体图形分类', DesignAsk: '问卷调查', DesignQuestion: '问卷题目', DesignAnswer: '问卷答卷', ServiceSeat: '客服席位', Temp: '客服欢迎语', StoreApplication: '店铺管理', StoreStyle: '店铺样式', ContentTagKey: '内容标签词库', UserLevel: '积分等级', UserMoneyLog: '资金积分流水', CRMSAttr: 'CRM 客户类型', Agency: '代理机构', DeliveryCenter: '交付中心'
+  GuestBar: '互动社区', Guestbook: '留言', Guestcate: '留言分类', Feedback: '反馈', Baike: '百科审核', Search: '后台快捷入口', AdZone: '广告位管理', AdInfo: '广告内容管理', FontPicShape: '字体图形素材', FontPicShapeType: '字体图形分类', DesignAsk: '问卷调查', DesignQuestion: '问卷题目', DesignAnswer: '问卷答卷', ServiceSeat: '客服席位', Temp: '客服欢迎语', StoreApplication: '店铺管理', StoreStyle: '店铺样式', ContentTagKey: '内容标签词库', UserLevel: '积分等级', UserMoneyLog: '资金积分流水', CRMSAttr: 'CRM 客户类型', SysLog: '系统日志', Agency: '代理机构', DeliveryCenter: '交付中心'
 };
 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','ExamSysPapers','ExamSysQuestions','PaperQuestions','ExamType','ContentPublish','PublishNode']);
+const READ_ONLY_CLASSES = new Set(['_Role','Permission','PayPlat','ThirdPlatInfo','GuestBar','DesignAnswer','Baike','ExamSysPapers','ExamSysQuestions','PaperQuestions','ExamType','ContentPublish','PublishNode','SysLog']);
 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']);
@@ -1366,6 +1366,48 @@ async function handler(request, response) {
         OList: '目标 Schema 缺少旧 ZL_Orderinfo 与 ZL_CartPro,无法查询主题大师 VIP 卡订单'
       }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧 Hms 动作'); fail(501, 'migration_blocked: ' + blockers[action]);
     }
+    if (operation === 'legacyIndexBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        Scence: '旧桌面布局与主题写入 SiteConfig.SiteOption.Desk 和 B_Admin.Theme/StructureID;目标没有等价站点配置或管理员主题字段',
+        Lockin: '旧锁屏依赖 B_Admin.LockScreen、明文口令再认证和 ASP.NET Session;目标没有持久化锁屏状态,不能把停用账号冒充屏幕锁',
+        Lockin_Submit: '旧提交动作为空且锁屏状态依赖 ASP.NET Session/B_Admin,云函数没有可等价提交的数据',
+        ASCXLoad: '旧动作按请求名称动态装载服务器 Razor/ASCX 局部视图;Angular 构建产物不允许运行时拼接并执行任意服务端模板路径',
+        API: '旧 animate_start/animate_stop 写入全局 SiteConfig.SiteOption.Admin_ShowAnimate;目标没有该站点级持久化配置',
+        AccountForm: '旧公开管理员申请受 SiteOption.RegManager 控制并创建默认锁定、高权限角色账号;目标没有审批开关和管理员申请模型,不能开放不安全的自助管理员入口',
+        AccountForm_API: '目标没有管理员申请开关与待审批账号模型,不能提供公开管理员用户名/邮箱探测接口',
+        AccountForm_Submit: '旧动作生成随机密码并直接授予固定角色 2/4/6/7/8;目标权限模型不等价且没有审批链,不能安全创建管理员申请'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧后台入口动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
+    if (operation === 'legacyDesignInteractiveBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        InteractiveList: '目标 Schema 缺少旧 Design_Pub 交互提交表及 sceneid、uid、fname 数据;DesignAnswer 和 Pub 语义不等价',
+        Interactive_API: '目标没有可按旧 Design_Pub ID 删除的交互提交记录'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧互动提交动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
+    if (operation === 'legacyIServerBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        Default: '目标 Schema 缺少旧 IServer 服务工单主表与回复表', AddQuestionRecord: '目标无可新增的 IServer 工单、CRM 关联和附件模型', AddQuestionRecord_Submit: '目标缺少工单标题、内容、优先级、类型、来源、状态、附件及 CRM 关系持久层',
+        BiServer: '目标没有可按状态、类型和标题筛选的 IServer 工单数据', BiServer_Del: '目标没有可删除的 IServer 工单记录', BiServerInfo: '目标缺少工单详情、阅读计数及回复关系', DelIServer: '目标缺少 IServerReply 回复记录', UpdateIServer: '目标缺少工单状态、优先级、来源、类型、备注和解决时间持久层', BselectiServer: '目标没有可供 CRM 选择的旧服务工单数据', ISReplyAdd: '目标缺少工单回复主表与父工单关系', ISReplyAdd_Submit: '目标缺少管理员工单回复、附件和内容持久层'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧 iServer 动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
+    if (operation === 'legacyLicenceBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        LicenceFiles: '目标 Schema 缺少 LicenceFiles 协议文件定义与限定用户数据', LicenceFiles_API: '目标没有可删除的协议文件定义', LicenceFilesAdd: '目标无协议模板路径、字段、状态、限制类型和限定用户模型', LicenceFilesAdd_Submit: '目标缺少协议文件定义持久层且云函数不能写旧本地模板文件',
+        LicenceSignApplyList: '目标缺少 Common_UserApply 的 user_signlicence 申请数据', LicenceSignApplyDetail: '目标缺少许可签署申请、用户和协议文件关联', LicenceSignApply_API: '目标缺少申请审核、签署记录与服务器 PDF 生成/本地文件哈希归档运行时', LicenceSignList: '目标 Schema 缺少 LicenceUserSign 签约记录', LicenceSignDetail: '目标缺少签约详情、协议文件和用户关联', LicenceSign_API: '目标没有可审核或驳回的签约记录'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧许可签约动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
+    if (operation === 'legacyLogBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        TxtLog: '云函数运行时没有旧 /Log/{LogType}/ 本地日志目录', TxtLogContent: '云函数不能按文件名读取旧服务器文本日志', TxtLog_Down: '云函数没有旧服务器日志文件可下载',
+        TaskList: '现有 SenTask 是空的发送任务类,不含旧 Content_ScheTask 的 TaskName/TaskContent/ExecuteType/Interval/TaskFlag 数据', TaskAdd: '目标缺少旧调度任务模型与 Hangfire 控制面', TaskAdd_API: '目标不能复制旧任务并向 Hangfire 注册 RecurringJob', TaskAdd_Submit: '目标缺少旧任务持久层、本地脚本校验与 Hangfire 调度运行时', Task_API: '目标不能对不存在的 Hangfire 任务执行删除、停用、启用或立即执行', TaskCenter: '云函数环境没有旧 Hangfire Dashboard 管理页面'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧日志任务动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
+    if (operation === 'systemLogs') {
+      const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20)); const search = String(input.search || '').trim(); const type1 = String(input.type1 || '').trim(); const type2 = String(input.type2 || '').trim(); const startDate = String(input.startDate || '').trim(); const endDate = String(input.endDate || '').trim();
+      if (search.length > 100 || type1.length > 100 || type2.length > 100) fail(400, '日志关键词或类型不能超过 100 个字符'); if ((startDate && !/^\d{4}-\d{2}-\d{2}$/.test(startDate)) || (endDate && !/^\d{4}-\d{2}-\d{2}$/.test(endDate))) fail(400, '日志日期格式必须为 YYYY-MM-DD'); const start = startDate ? new Date(startDate + 'T00:00:00+08:00') : null; const end = endDate ? new Date(new Date(endDate + 'T00:00:00+08:00').getTime() + 86400000) : null; if ((start && Number.isNaN(start.getTime())) || (end && Number.isNaN(end.getTime())) || (start && end && start >= end)) fail(400, '日志日期范围无效'); const fields = await schemaFor('SysLog');
+      const scopedQuery = () => { const query = new Parse.Query('SysLog'); applyTenant(query, fields, context, input.companyId); if (type1) query.equalTo('type1', type1); if (type2) query.equalTo('type2', type2); if (start) query.greaterThanOrEqualTo('cdate', start); if (end) query.lessThan('cdate', end); return query; };
+      let query = scopedQuery(); if (search) { const pattern = escapeRegex(search); query = Parse.Query.or(...['cname','detail','content1','content2','rawUrl'].map((field) => { const candidate = scopedQuery(); candidate.matches(field, pattern, 'i'); return candidate; })); } const total = await query.count({ useMasterKey: true }); query.descending('cdate'); query.skip((page - 1) * pageSize); query.limit(pageSize); const results = await query.find({ useMasterKey: true }); return response.json({ success: true, data: { className: 'SysLog', page, pageSize, total, results: results.map(serializeObject) } });
+    }
     if (operation === 'shopFareTemplates') {
       const page = Math.max(1, Number(input.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(input.pageSize) || 20)); const search = String(input.search || '').trim(); if (search.length > 100) fail(400, '运费模板搜索词不能超过 100 个字符'); const fields = await schemaFor('ShopFareTlp'); const query = new Parse.Query('ShopFareTlp'); applyTenant(query, fields, context, input.companyId); if (search) query.matches('tlpName', escapeRegex(search), 'i'); query.ascending('id'); const total = await query.count({ useMasterKey: true }); query.skip((page - 1) * pageSize); query.limit(pageSize); const results = await query.find({ useMasterKey: true }); return response.json({ success: true, data: { className: 'ShopFareTlp', page, pageSize, total, results: results.map(serializeObject) } });
     }

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

@@ -153,6 +153,55 @@ const explicit = new Map([
   ['Hms/VipOverdue', ['blocked-data', '目标没有旧主题大师卡到期时间,无法展示强制过期上下文']],
   ['Hms/VipOverdue_Submit', ['blocked-data', '目标没有旧 ICQ 到期时间,不能误写当前 VIP 合伙人字段']],
   ['Hms/OList', ['blocked-data', '目标 Schema 缺少旧 ZL_Orderinfo 与 ZL_CartPro,无法查询主题大师 VIP 卡订单']],
+  ['Index/Default', ['implemented', '已由受 adminGuard 保护的 Angular 后台壳与 /admin/dashboard 替代;托管服务无需旧安装页分支']],
+  ['Index/Main', ['implemented', '已由 Angular AdminShellComponent 主框架替代']],
+  ['Index/WorkTable', ['implemented', '已由 dashboard 云函数和 Angular 管理概览替代']],
+  ['Index/Scence', ['blocked-infrastructure', '目标缺少 SiteOption.Desk 与管理员 Theme/StructureID 持久层及旧主题桌面运行时']],
+  ['Index/FuncList', ['partial', 'Angular 侧栏与资源目录已提供功能入口;旧完整功能磁贴和权限裁剪布局尚未逐项复刻']],
+  ['Index/Lockin', ['blocked-infrastructure', '目标没有 B_Admin.LockScreen 与 ASP.NET Session 锁屏状态,不能以停用账号冒充屏幕锁']],
+  ['Index/SiteMap', ['partial', '资源目录和后台快捷入口已支持搜索;旧 SiteMap_Admin.config 的全部 XML 菜单未迁入']],
+  ['Index/Lockin_Submit', ['blocked-infrastructure', '旧提交动作为空且锁屏状态依赖 ASP.NET Session/B_Admin,云函数无等价数据']],
+  ['Index/ASCXLoad', ['blocked-infrastructure', 'Angular 构建不允许按请求路径动态执行服务器 Razor/ASCX 局部视图']],
+  ['Index/Login', ['implemented', '已由 /admin/login 登录页、Parse 登录接口和登录态恢复替代']],
+  ['Index/Login_AJAX', ['implemented', '已由 Parse 密码校验、admin meta 权限复核和统一错误处理替代']],
+  ['Index/Logout', ['implemented', '已由 logout 云函数撤销 _Session 并清理 Angular sessionStorage 替代']],
+  ['Index/API', ['blocked-infrastructure', '目标缺少 SiteOption.Admin_ShowAnimate 全局持久化配置']],
+  ['Index/AccountForm', ['blocked-infrastructure', '目标没有 SiteOption.RegManager、管理员申请模型和审批链,不能开放高权限自助申请']],
+  ['Index/AccountForm_API', ['blocked-infrastructure', '目标没有管理员申请开关与待审批模型,不能提供公开账号/邮箱探测接口']],
+  ['Index/AccountForm_Submit', ['blocked-infrastructure', '旧动作随机生成密码并直接授予固定高权限角色,目标权限模型不等价且没有审批链']],
+  ['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 服务工单主表与回复表']],
+  ['Office / iServer/AddQuestionRecord', ['blocked-data', '目标无可新增的 IServer 工单、CRM 关联和附件模型']],
+  ['Office / iServer/AddQuestionRecord_Submit', ['blocked-data', '目标缺少工单字段、附件及 CRM 关系持久层']],
+  ['Office / iServer/BiServer', ['blocked-data', '目标没有可按状态、类型和标题筛选的 IServer 工单数据']],
+  ['Office / iServer/BiServer_Del', ['blocked-data', '目标没有可删除的 IServer 工单记录']],
+  ['Office / iServer/BiServerInfo', ['blocked-data', '目标缺少工单详情、阅读计数及回复关系']],
+  ['Office / iServer/DelIServer', ['blocked-data', '目标缺少 IServerReply 回复记录']],
+  ['Office / iServer/UpdateIServer', ['blocked-data', '目标缺少工单状态、优先级、来源、类型、备注和解决时间持久层']],
+  ['Office / iServer/BselectiServer', ['blocked-data', '目标没有可供 CRM 选择的旧服务工单数据']],
+  ['Office / iServer/ISReplyAdd', ['blocked-data', '目标缺少工单回复主表与父工单关系']],
+  ['Office / iServer/ISReplyAdd_Submit', ['blocked-data', '目标缺少管理员工单回复、附件和内容持久层']],
+  ['Office / Licence/LicenceFiles', ['blocked-data', '目标缺少 LicenceFiles 协议文件定义与限定用户数据']],
+  ['Office / Licence/LicenceFiles_API', ['blocked-data', '目标没有可删除的协议文件定义']],
+  ['Office / Licence/LicenceFilesAdd', ['blocked-data', '目标无协议模板路径、字段、状态、限制类型和限定用户模型']],
+  ['Office / Licence/LicenceFilesAdd_Submit', ['blocked-infrastructure', '目标缺少协议定义持久层且云函数不能写旧本地模板文件']],
+  ['Office / Licence/LicenceSignApplyList', ['blocked-data', '目标缺少 Common_UserApply 的 user_signlicence 申请数据']],
+  ['Office / Licence/LicenceSignApplyDetail', ['blocked-data', '目标缺少许可签署申请、用户和协议文件关联']],
+  ['Office / Licence/LicenceSignApply_API', ['blocked-infrastructure', '目标缺少申请审核、签署记录及服务器 PDF/本地哈希归档运行时']],
+  ['Office / Licence/LicenceSignList', ['blocked-data', '目标缺少 LicenceUserSign 签约记录']],
+  ['Office / Licence/LicenceSignDetail', ['blocked-data', '目标缺少签约详情、协议文件和用户关联']],
+  ['Office / Licence/LicenceSign_API', ['blocked-data', '目标没有可审核或驳回的签约记录']],
+  ['Extend / Log/TxtLog', ['blocked-infrastructure', '云函数运行时没有旧 /Log/{LogType}/ 本地日志目录']],
+  ['Extend / Log/TxtLogContent', ['blocked-infrastructure', '云函数不能按文件名读取旧服务器文本日志']],
+  ['Extend / Log/TxtLog_Down', ['blocked-infrastructure', '云函数没有旧服务器日志文件可下载']],
+  ['Extend / Log/SysLog', ['implemented', '已由只读 systemLogs 云函数与 Angular 系统日志页覆盖帐套分页、类型、关键词和日期筛选']],
+  ['Extend / Log/TaskList', ['blocked-data', 'SenTask 为空且字段不含旧 Content_ScheTask 任务定义']],
+  ['Extend / Log/TaskAdd', ['blocked-infrastructure', '目标缺少旧调度任务模型与 Hangfire 控制面']],
+  ['Extend / Log/TaskAdd_API', ['blocked-infrastructure', '目标不能复制旧任务并注册 Hangfire RecurringJob']],
+  ['Extend / Log/TaskAdd_Submit', ['blocked-infrastructure', '目标缺少旧任务持久层、本地脚本校验与 Hangfire 调度运行时']],
+  ['Extend / Log/Task_API', ['blocked-infrastructure', '目标不能删除、停用、启用或立即执行旧 Hangfire 任务']],
+  ['Extend / Log/TaskCenter', ['blocked-infrastructure', '云函数环境没有旧 Hangfire Dashboard 管理页面']],
   ['Shop / Exp/DeliverType', ['implemented', '已由 shopFareTemplates 提供帐套隔离、名称搜索、分页和旧运费规则列表']],
   ['Shop / Exp/DeliverAdd', ['implemented', '已由 Angular 商城运费模板新建/编辑抽屉替代']],
   ['Shop / Exp/DeliverAdd_Submit', ['implemented', '已由 saveShopFareTemplate 校验计价方式、规则 JSON、名称防重并原子分配旧编号']],

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

@@ -660,6 +660,22 @@ try {
   for (const action of ['UList','VipAdd','VipAddJump','VipUpdate','VipUpdate_Submit','VipRenewal','VipRenewal_Submit','VipOverdue','VipOverdue_Submit','OList']) {
     const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyHmsBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧 Hms 动作 ${action} 未返回明确迁移阻塞`);
   }
+  for (const action of ['Scence','Lockin','Lockin_Submit','ASCXLoad','API','AccountForm','AccountForm_API','AccountForm_Submit']) {
+    const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyIndexBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧后台入口动作 ${action} 未返回明确迁移阻塞`);
+  }
+  for (const action of ['InteractiveList','Interactive_API']) {
+    const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyDesignInteractiveBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧互动提交动作 ${action} 未返回明确迁移阻塞`);
+  }
+  for (const action of ['Default','AddQuestionRecord','AddQuestionRecord_Submit','BiServer','BiServer_Del','BiServerInfo','DelIServer','UpdateIServer','BselectiServer','ISReplyAdd','ISReplyAdd_Submit']) {
+    const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyIServerBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧 iServer 动作 ${action} 未返回明确迁移阻塞`);
+  }
+  for (const action of ['LicenceFiles','LicenceFiles_API','LicenceFilesAdd','LicenceFilesAdd_Submit','LicenceSignApplyList','LicenceSignApplyDetail','LicenceSignApply_API','LicenceSignList','LicenceSignDetail','LicenceSign_API']) {
+    const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyLicenceBlocker', 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']) {
+    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 systemLogs = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'systemLogs', className: 'SysLog', page: 1, pageSize: 2, search: '', type1: '', type2: '', startDate: '', endDate: '' }); if (!Array.isArray(systemLogs.results) || systemLogs.pageSize !== 2 || Number(systemLogs.total) < systemLogs.results.length) throw new Error('系统日志只读列表异常');
   const fareSuffix = Date.now().toString(36); const fareRules = [{ name: '冒烟快递', mode: '1', enabled: true, price: '10', plus: '1', free_sel: '0', free_num: '', free_money: '' }]; const createdFare = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'saveShopFareTemplate', className: 'ShopFareTlp', fields: { tlpName: `冒烟运费-${fareSuffix}`, priceMode: 1, express: JSON.stringify(fareRules), remind: '临时运费模板', remind2: '' } }); temporaryShopFareTemplateId = String(createdFare.objectId || ''); if (!temporaryShopFareTemplateId || !Number(createdFare.id) || createdFare.sourceKey !== `[["ID",${Number(createdFare.id)}]]`) throw new Error(`商城运费模板原子编号创建异常:${JSON.stringify(createdFare)}`);
   const fareList = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'shopFareTemplates', className: 'ShopFareTlp', page: 1, pageSize: 20, search: fareSuffix }); if (fareList.total !== 1 || fareList.results?.[0]?.objectId !== temporaryShopFareTemplateId) throw new Error('商城运费模板专用列表失败');
   const updatedFare = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'saveShopFareTemplate', className: 'ShopFareTlp', objectId: temporaryShopFareTemplateId, fields: { tlpName: `更新运费-${fareSuffix}`, priceMode: 1, express: JSON.stringify(fareRules), remind: '已更新', remind2: '' } }); if (updatedFare.tlpName !== `更新运费-${fareSuffix}` || Number(updatedFare.id) !== Number(createdFare.id)) throw new Error('商城运费模板专用编辑失败');

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

@@ -50,6 +50,7 @@ export const ADMIN_NAVIGATION = [
   { label: '资金积分流水', icon: 'wallet-cards', route: '/admin/resources/UserMoneyLog' },
   { label: 'CRM 客户类型', icon: 'contact-round', route: '/admin/resources/CRMSAttr' },
   { label: '商城运费模板', icon: 'truck', route: '/admin/resources/ShopFareTlp' },
+  { label: '系统日志', icon: 'scroll-text', route: '/admin/resources/SysLog' },
   { 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' },

+ 2 - 1
src/app/admin/layout/admin-shell.component.ts

@@ -31,6 +31,7 @@ import {
   Palette,
   Route,
   Search,
+  ScrollText,
   Shapes,
   Store,
   TableProperties,
@@ -55,7 +56,7 @@ export class AdminShellComponent {
   readonly mobileOpen = signal(false);
   readonly icons = {
     BadgeCent, Bell, BookMarked, BookOpen, Braces, CalendarDays, ChartBarBig, ChartNoAxesCombined, ChevronLeft, ChevronRight,
-    ClipboardCheck, ClipboardList, Files, GalleryHorizontalEnd, GraduationCap, Headphones, Languages, LayoutDashboard, ListChecks, ListTodo,
+    ClipboardCheck, ClipboardList, Files, GalleryHorizontalEnd, GraduationCap, Headphones, Languages, LayoutDashboard, ListChecks, ListTodo, ScrollText,
     LayoutTemplate, LogOut, Megaphone, Menu, MessageSquareText, Palette, Route, Search, Shapes, Store, TableProperties, Tags, Users, WalletCards,
   };
 

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

@@ -2813,344 +2813,344 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Index",
     "action": "Default",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "已由受 adminGuard 保护的 Angular 后台壳与 /admin/dashboard 替代;托管服务无需旧安装页分支"
   },
   {
     "module": "Index",
     "action": "Main",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "已由 Angular AdminShellComponent 主框架替代"
   },
   {
     "module": "Index",
     "action": "WorkTable",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "已由 dashboard 云函数和 Angular 管理概览替代"
   },
   {
     "module": "Index",
     "action": "Scence",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标缺少 SiteOption.Desk 与管理员 Theme/StructureID 持久层及旧主题桌面运行时"
   },
   {
     "module": "Index",
     "action": "FuncList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "Angular 侧栏与资源目录已提供功能入口;旧完整功能磁贴和权限裁剪布局尚未逐项复刻"
   },
   {
     "module": "Index",
     "action": "Lockin",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有 B_Admin.LockScreen 与 ASP.NET Session 锁屏状态,不能以停用账号冒充屏幕锁"
   },
   {
     "module": "Index",
     "action": "SiteMap",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "资源目录和后台快捷入口已支持搜索;旧 SiteMap_Admin.config 的全部 XML 菜单未迁入"
   },
   {
     "module": "Index",
     "action": "Lockin_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "旧提交动作为空且锁屏状态依赖 ASP.NET Session/B_Admin,云函数无等价数据"
   },
   {
     "module": "Index",
     "action": "ASCXLoad",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "Angular 构建不允许按请求路径动态执行服务器 Razor/ASCX 局部视图"
   },
   {
     "module": "Index",
     "action": "Login",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "已由 /admin/login 登录页、Parse 登录接口和登录态恢复替代"
   },
   {
     "module": "Index",
     "action": "Login_AJAX",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "已由 Parse 密码校验、admin meta 权限复核和统一错误处理替代"
   },
   {
     "module": "Index",
     "action": "Logout",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "已由 logout 云函数撤销 _Session 并清理 Angular sessionStorage 替代"
   },
   {
     "module": "Index",
     "action": "API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标缺少 SiteOption.Admin_ShowAnimate 全局持久化配置"
   },
   {
     "module": "Index",
     "action": "AccountForm",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有 SiteOption.RegManager、管理员申请模型和审批链,不能开放高权限自助申请"
   },
   {
     "module": "Index",
     "action": "AccountForm_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标没有管理员申请开关与待审批模型,不能提供公开账号/邮箱探测接口"
   },
   {
     "module": "Index",
     "action": "AccountForm_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "旧动作随机生成密码并直接授予固定高权限角色,目标权限模型不等价且没有审批链"
   },
   {
     "module": "Design / Interactive",
     "action": "InteractiveList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少旧 Design_Pub 交互提交表及 sceneid、uid、fname 数据;DesignAnswer/Pub 不等价"
   },
   {
     "module": "Design / Interactive",
     "action": "Interactive_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可按旧 Design_Pub ID 删除的交互提交记录"
   },
   {
     "module": "Office / iServer",
     "action": "Default",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 IServer 服务工单主表与回复表"
   },
   {
     "module": "Office / iServer",
     "action": "AddQuestionRecord",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无可新增的 IServer 工单、CRM 关联和附件模型"
   },
   {
     "module": "Office / iServer",
     "action": "AddQuestionRecord_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少工单字段、附件及 CRM 关系持久层"
   },
   {
     "module": "Office / iServer",
     "action": "BiServer",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可按状态、类型和标题筛选的 IServer 工单数据"
   },
   {
     "module": "Office / iServer",
     "action": "BiServer_Del",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可删除的 IServer 工单记录"
   },
   {
     "module": "Office / iServer",
     "action": "BiServerInfo",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少工单详情、阅读计数及回复关系"
   },
   {
     "module": "Office / iServer",
     "action": "DelIServer",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少 IServerReply 回复记录"
   },
   {
     "module": "Office / iServer",
     "action": "UpdateIServer",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少工单状态、优先级、来源、类型、备注和解决时间持久层"
   },
   {
     "module": "Office / iServer",
     "action": "BselectiServer",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可供 CRM 选择的旧服务工单数据"
   },
   {
     "module": "Office / iServer",
     "action": "ISReplyAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少工单回复主表与父工单关系"
   },
   {
     "module": "Office / iServer",
     "action": "ISReplyAdd_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少管理员工单回复、附件和内容持久层"
   },
   {
     "module": "Office / Licence",
     "action": "LicenceFiles",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少 LicenceFiles 协议文件定义与限定用户数据"
   },
   {
     "module": "Office / Licence",
     "action": "LicenceFiles_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可删除的协议文件定义"
   },
   {
     "module": "Office / Licence",
     "action": "LicenceFilesAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无协议模板路径、字段、状态、限制类型和限定用户模型"
   },
   {
     "module": "Office / Licence",
     "action": "LicenceFilesAdd_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标缺少协议定义持久层且云函数不能写旧本地模板文件"
   },
   {
     "module": "Office / Licence",
     "action": "LicenceSignApplyList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少 Common_UserApply 的 user_signlicence 申请数据"
   },
   {
     "module": "Office / Licence",
     "action": "LicenceSignApplyDetail",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少许可签署申请、用户和协议文件关联"
   },
   {
     "module": "Office / Licence",
     "action": "LicenceSignApply_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标缺少申请审核、签署记录及服务器 PDF/本地哈希归档运行时"
   },
   {
     "module": "Office / Licence",
     "action": "LicenceSignList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少 LicenceUserSign 签约记录"
   },
   {
     "module": "Office / Licence",
     "action": "LicenceSignDetail",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少签约详情、协议文件和用户关联"
   },
   {
     "module": "Office / Licence",
     "action": "LicenceSign_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可审核或驳回的签约记录"
   },
   {
     "module": "Extend / Log",
     "action": "TxtLog",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "云函数运行时没有旧 /Log/{LogType}/ 本地日志目录"
   },
   {
     "module": "Extend / Log",
     "action": "TxtLogContent",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "云函数不能按文件名读取旧服务器文本日志"
   },
   {
     "module": "Extend / Log",
     "action": "TxtLog_Down",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "云函数没有旧服务器日志文件可下载"
   },
   {
     "module": "Extend / Log",
     "action": "SysLog",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "已由只读 systemLogs 云函数与 Angular 系统日志页覆盖帐套分页、类型、关键词和日期筛选"
   },
   {
     "module": "Extend / Log",
     "action": "TaskList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "SenTask 为空且字段不含旧 Content_ScheTask 任务定义"
   },
   {
     "module": "Extend / Log",
     "action": "TaskAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标缺少旧调度任务模型与 Hangfire 控制面"
   },
   {
     "module": "Extend / Log",
     "action": "TaskAdd_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标不能复制旧任务并注册 Hangfire RecurringJob"
   },
   {
     "module": "Extend / Log",
     "action": "TaskAdd_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标缺少旧任务持久层、本地脚本校验与 Hangfire 调度运行时"
   },
   {
     "module": "Extend / Log",
     "action": "Task_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "目标不能删除、停用、启用或立即执行旧 Hangfire 任务"
   },
   {
     "module": "Extend / Log",
     "action": "TaskCenter",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "云函数环境没有旧 Hangfire Dashboard 管理页面"
   },
   {
     "module": "User / Mail",

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

@@ -39,6 +39,9 @@
   @if (className() === 'Baike') {
     <div class="bulk-toolbar"><strong>百科状态</strong><select [ngModel]="filterBaikeStatus()" (ngModelChange)="filterBaikeStatus.set(+$event); load(1)"><option [ngValue]="-100">全部审核状态</option><option [ngValue]="1">已审核</option><option [ngValue]="0">未审核</option></select><select [ngModel]="filterBaikeElite()" (ngModelChange)="filterBaikeElite.set(+$event); load(1)"><option [ngValue]="-100">全部推荐状态</option><option [ngValue]="1">已推荐</option><option [ngValue]="0">未推荐</option></select></div>
   }
+  @if (className() === 'SysLog') {
+    <div class="bulk-toolbar"><strong>系统日志范围</strong><input [ngModel]="filterSystemLogType1()" (ngModelChange)="filterSystemLogType1.set($event)" placeholder="一级类型(精确)" /><input [ngModel]="filterSystemLogType2()" (ngModelChange)="filterSystemLogType2.set($event)" placeholder="二级类型(精确)" /><input type="date" aria-label="日志开始日期" [ngModel]="filterSystemLogStartDate()" (ngModelChange)="filterSystemLogStartDate.set($event)" /><input type="date" aria-label="日志结束日期" [ngModel]="filterSystemLogEndDate()" (ngModelChange)="filterSystemLogEndDate.set($event)" /><button type="button" (click)="load(1)">筛选日志</button></div>
+  }
   @if (bulkEnabled() && selectedIds().length) {
     <div class="bulk-toolbar"><strong>已选 {{ selectedIds().length }} 条</strong>
       @if (className() === '_User') {

+ 8 - 0
src/app/admin/pages/admin-resource.component.spec.ts

@@ -1016,4 +1016,12 @@ describe('AdminResourceComponent', () => {
     expect(functions.admin.calls.first().args).toEqual(['shopFareTemplateBatch', { className: 'ShopFareTlp', action: 'delete', objectIds: ['fare-a', 'fare-b'] }]);
     expect(component.bulkMessage()).toContain('2 个运费模板');
   });
+
+  it('loads read-only system logs with legacy type and date filters', async () => {
+    component.className.set('SysLog'); component.schema.set({ className: 'SysLog', label: '系统日志', writable: false, creatable: false, supportsSoftDelete: false, fields: [] }); component.search.set('管理员'); component.filterSystemLogType1.set('后台'); component.filterSystemLogType2.set('登录'); component.filterSystemLogStartDate.set('2026-08-01'); component.filterSystemLogEndDate.set('2026-08-19'); functions.admin.and.resolveTo({ className: 'SysLog', page: 1, pageSize: 20, total: 1, results: [] });
+
+    await component.load(1);
+
+    expect(functions.admin).toHaveBeenCalledWith('systemLogs', { className: 'SysLog', page: 1, pageSize: 20, search: '管理员', type1: '后台', type2: '登录', startDate: '2026-08-01', endDate: '2026-08-19' });
+  });
 });

+ 8 - 2
src/app/admin/pages/admin-resource.component.ts

@@ -143,13 +143,17 @@ export class AdminResourceComponent implements OnInit {
   readonly updatingBaike = signal(false);
   readonly updatingCrmType = signal(false);
   readonly updatingShopFare = signal(false);
+  readonly filterSystemLogType1 = signal('');
+  readonly filterSystemLogType2 = signal('');
+  readonly filterSystemLogStartDate = signal('');
+  readonly filterSystemLogEndDate = signal('');
   readonly selectedIds = signal<string[]>([]);
   readonly bulkError = signal('');
   readonly bulkMessage = signal('');
   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', 'tlpName', '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 preferred = ['objectId', 'name', 'title', 'tlpName', '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', 'cname', 'type1', 'type2', 'detail', 'cdate', '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);
   });
@@ -177,6 +181,7 @@ export class AdminResourceComponent implements OnInit {
       this.resetTagKeyActions();
       this.filterUserMoneyType.set(1); this.filterUserMoneyStartDate.set(''); this.filterUserMoneyEndDate.set('');
       this.filterBaikeStatus.set(-100); this.filterBaikeElite.set(-100);
+      this.filterSystemLogType1.set(''); this.filterSystemLogType2.set(''); this.filterSystemLogStartDate.set(''); this.filterSystemLogEndDate.set('');
       void this.load(1, true);
     });
   }
@@ -190,7 +195,7 @@ export class AdminResourceComponent implements OnInit {
       if (this.className() === 'FontPicShape' && reloadSchema) await this.loadFontShapeTypes();
       if (['DesignQuestion', 'DesignAnswer'].includes(this.className()) && reloadSchema) { await this.loadSurveys(); if (!this.filterSurveyId() && this.surveys().length) this.filterSurveyId.set(this.surveyIdOf(this.surveys()[0])); }
       if (this.className() === 'StoreApplication' && reloadSchema) await this.loadStoreStyles();
-      const pageOperation = this.className() === 'Grade' && this.filterGradeCategoryId() > 0 ? 'gradeOptions' : this.className() === 'Search' ? 'searchNavigations' : this.className() === 'AdZone' ? 'adZones' : this.className() === 'AdInfo' ? 'adInfos' : this.className() === 'FontPicShape' ? 'fontShapes' : this.className() === 'FontPicShapeType' ? 'fontShapeTypes' : this.className() === 'DesignAsk' ? 'surveys' : this.className() === 'DesignQuestion' ? 'surveyQuestions' : this.className() === 'DesignAnswer' ? 'surveyResults' : this.className() === 'ServiceSeat' ? 'serviceSeats' : this.className() === 'Temp' ? 'serviceCodes' : this.className() === 'StoreApplication' ? 'storeApplications' : this.className() === 'StoreStyle' ? 'storeStyles' : this.className() === 'ContentTagKey' ? 'contentTagKeys' : this.className() === 'UserLevel' ? 'userLevels' : this.className() === 'UserMoneyLog' ? 'userMoneyLogs' : this.className() === 'Baike' ? 'baikeEntries' : this.className() === 'CRMSAttr' ? 'crmClientTypes' : this.className() === 'ShopFareTlp' ? 'shopFareTemplates' : 'list';
+      const pageOperation = this.className() === 'Grade' && this.filterGradeCategoryId() > 0 ? 'gradeOptions' : this.className() === 'Search' ? 'searchNavigations' : this.className() === 'AdZone' ? 'adZones' : this.className() === 'AdInfo' ? 'adInfos' : this.className() === 'FontPicShape' ? 'fontShapes' : this.className() === 'FontPicShapeType' ? 'fontShapeTypes' : this.className() === 'DesignAsk' ? 'surveys' : this.className() === 'DesignQuestion' ? 'surveyQuestions' : this.className() === 'DesignAnswer' ? 'surveyResults' : this.className() === 'ServiceSeat' ? 'serviceSeats' : this.className() === 'Temp' ? 'serviceCodes' : this.className() === 'StoreApplication' ? 'storeApplications' : this.className() === 'StoreStyle' ? 'storeStyles' : this.className() === 'ContentTagKey' ? 'contentTagKeys' : this.className() === 'UserLevel' ? 'userLevels' : this.className() === 'UserMoneyLog' ? 'userMoneyLogs' : this.className() === 'Baike' ? 'baikeEntries' : this.className() === 'CRMSAttr' ? 'crmClientTypes' : this.className() === 'ShopFareTlp' ? 'shopFareTemplates' : this.className() === 'SysLog' ? 'systemLogs' : 'list';
       const pagePayload: Record<string, unknown> = { className: this.className(), page: pageNumber, pageSize: 20, search: this.search().trim() };
       if (pageOperation === 'gradeOptions') { pagePayload['categoryId'] = this.filterGradeCategoryId(); pagePayload['parentId'] = this.filterGradeParentId(); }
       if (pageOperation === 'searchNavigations') { pagePayload['type'] = this.filterSearchNavigationType(); pagePayload['state'] = this.filterSearchNavigationState(); pagePayload['elite'] = this.filterSearchNavigationElite(); }
@@ -202,6 +207,7 @@ export class AdminResourceComponent implements OnInit {
       if (pageOperation === 'storeApplications') pagePayload['status'] = this.filterStoreStatus();
       if (pageOperation === 'userMoneyLogs') { pagePayload['type'] = this.filterUserMoneyType(); pagePayload['startDate'] = this.filterUserMoneyStartDate(); pagePayload['endDate'] = this.filterUserMoneyEndDate(); }
       if (pageOperation === 'baikeEntries') { pagePayload['status'] = this.filterBaikeStatus(); pagePayload['elite'] = this.filterBaikeElite(); }
+      if (pageOperation === 'systemLogs') { pagePayload['type1'] = this.filterSystemLogType1(); pagePayload['type2'] = this.filterSystemLogType2(); pagePayload['startDate'] = this.filterSystemLogStartDate(); pagePayload['endDate'] = this.filterSystemLogEndDate(); }
       this.page.set(await this.functions.admin<ResourcePage>(pageOperation, pagePayload));
       if (this.className() === '_User' && reloadSchema) await this.loadUserGroups();
       if (this.className() === 'Datadic' && reloadSchema) await this.loadDictionaryCategories();