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

feat: migrate platform company administration

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

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

@@ -35,6 +35,7 @@
 - User/Message 的五个站内信动作缺少 Message 收发主表;`MailTemp` 只是邮件模板,不被误用。Mobile 两个空控制器动作只剩已丢失的 Razor 视图,无法恢复页面基线。栏目 `ZhiDing` 的旧实现会把所有有效栏目强制扁平为根节点,现有 `saveNode/nodeBatch` 已安全维护层级并拒绝该破坏性修复;`UnionNode_Merge` 同时依赖未迁入的 `ZL_Commodities`,不能只改内容表形成半合并。以上均由专用云函数返回细分原因。
 - Office/OA 中字段完全匹配的 `MisType` 已迁为 Angular“OA 流程类型”页与 `misTypes/saveMisType/misTypeBatch`,支持帐套隔离、搜索、防重、原子旧 ID 和 `MisProcedure.typeId` 引用保护删除;用户选择与邮件通讯录复用现有用户管理,模型选择为部分迁移。其余旧 `OA_AuditFlow/OA_AuditFlowItem/Mis_Model/OA_Sign/OA_Document` 主表及套红文件运行时未迁入;目标空的 `MisProcedure/MisProLevel/MisSign/DocModel` 是不同版本结构,不被强行冒充,21 个动作由 `legacyOABlocker` 逐项说明。
 - `Page` 控制器中与旧字段完全匹配的 `PageStyle` 已迁为 Angular“黄页样式”页与 `pageStyles/savePageStyle/pageStyleBatch`:支持帐套隔离、名称搜索与防重、路径和状态校验、原子分配旧 `PageNodeid`、单一默认样式及 `PageTemplate.userGroup` 引用保护删除。目标 `PageTemplate` 虽保留旧字段但为空,且没有 `PageReg` 用户黄页申请主表或 `SiteOption` 黄页配置层;内容管理仅部分复用现有 `CommonModel`,其余 15 个旧入口由 `legacyPageBlocker` 返回逐项数据或基础设施原因。
+- `Office/Plat` 的 `PlatComp` 与折叠在 `_User.legacyUserPlat` 的旧 `User_Plat` 数据已迁为 Angular“协同办公企业”页面以及 `platformCompanies/savePlatformCompany/platformCompanyMembers/platformCompanyMemberBatch/platformCompanyBatch`:兼容失真的 count,支持企业资料防重、原子旧 ID、同帐套创建人、成员查看/加入/移动/移出,并保护仍有成员或 `PlatUserRole` 角色的企业删除;`legacyUserPlat` 已从通用用户编辑锁定。旧 `Common_UserApply/Blog_Msg/Plat_Topic/Plat_Group` 主表均不存在,申请审核、动态消息、话题和企业群组入口由 `legacyPlatBlocker` 逐项返回原因;旧 Razor `CreateComp/WordTlp` 视图也没有可恢复的交互基线。
 - 旧 `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/旧模板运行时。

Разница между файлами не показана из-за своего большого размера
+ 8 - 3
scripts/deploy-admin-functions.mjs


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

@@ -230,6 +230,28 @@ const explicit = new Map([
   ['Page/SetPageOrder_DownMove', ['blocked-data', '目标没有可按 PageReg 隔离的相邻模板顺序']],
   ['Page/Style_Add', ['implemented', '已由 savePageStyle 校验路径、状态、防重并原子分配旧 PageNodeid']],
   ['Page/Apply_Update', ['blocked-data', '目标缺少 PageReg 申请主表、黄页模型附表和用户申请字段']],
+  ['Office / Plat/Default', ['blocked-data', '旧默认页跳转到已缺失的 Blog_Msg 动态管理,目标没有可进入的信息首页']],
+  ['Office / Plat/AddComp', ['implemented', '已由 Angular 协同办公企业新建/编辑抽屉替代']],
+  ['Office / Plat/AddComp_Submit', ['implemented', 'savePlatformCompany 校验企业资料、创建人、防重并原子分配旧数字 ID']],
+  ['Office / Plat/AuditApply', ['blocked-data', '目标 Schema 缺少旧 Common_UserApply 协同办公申请主表']],
+  ['Office / Plat/AuditApply_Agree', ['blocked-data', '目标没有可批量通过并改为状态 99 的协同办公申请']],
+  ['Office / Plat/AuditApply_Reject', ['blocked-data', '目标没有可批量拒绝并改为状态 -1 的协同办公申请']],
+  ['Office / Plat/CompList', ['implemented', 'platformCompanies 提供帐套隔离、关键词、旧编号排序和分页列表']],
+  ['Office / Plat/CompInfo', ['implemented', '已由协同办公企业详情抽屉显示完整 PlatComp 字段']],
+  ['Office / Plat/CreateComp', ['partial', 'Angular 企业表单已覆盖主表创建;旧动作仅剩已丢失 Razor 向导,未知交互无法恢复']],
+  ['Office / Plat/MemberList', ['implemented', 'platformCompanyMembers 从 _User.legacyUserPlat 安全还原企业成员列表']],
+  ['Office / Plat/Member_Add', ['implemented', 'platformCompanyMemberBatch 按同帐套账号新增或移动旧 User_Plat 成员']],
+  ['Office / Plat/Member_API', ['implemented', 'platformCompanyMemberBatch remove 等价删除成员的 legacyUserPlat 记录']],
+  ['Office / Plat/PlatInfoDeail', ['blocked-data', '目标 Schema 缺少旧 Blog_Msg 动态消息主表']],
+  ['Office / Plat/PlatInfoDetail_Submit', ['blocked-data', '目标没有可编辑正文与日期的旧协同办公动态消息']],
+  ['Office / Plat/PlatInfoManage', ['blocked-data', '目标没有 Blog_Msg 顶级动态、回收状态和关键词数据']],
+  ['Office / Plat/TopicList', ['blocked-data', '目标 Schema 缺少旧 Plat_Topic 话题主表与星标状态']],
+  ['Office / Plat/WordTlp', ['blocked-infrastructure', '旧文字模板仅剩已丢失的 Razor 视图,目标没有模板数据或渲染协议']],
+  ['Office / Plat/API', ['partial', 'platformCompanyBatch 已覆盖带成员/角色引用保护的 comp_del;消息与话题分支因主表缺失而阻塞']],
+  ['Office / Plat/Crud', ['blocked-data', '目标 Schema 缺少旧 Plat_Group 企业部门/群组树']],
+  ['Office / Plat/CrudAdd', ['blocked-data', '目标没有可编辑的企业群组、上级和成员管理员关系']],
+  ['Office / Plat/CrudAdd_Submit', ['blocked-data', '目标缺少 Plat_Group 的 FirstID/Depth/ManageIDS/MemberIDS 持久层']],
+  ['Office / Plat/GroupAdmin_API', ['blocked-data', '目标没有可删除的 Plat_Group 企业群组记录']],
   ['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 服务工单主表与回复表']],

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

@@ -121,6 +121,8 @@ let temporaryCrmClientTypeId = '';
 let temporaryShopFareTemplateId = '';
 let temporaryMisTypeId = '';
 let temporaryPageStyleId = '';
+let temporaryPlatformCompanyId = '';
+let temporaryPlatformMemberId = '';
 const temporaryLessonIds = [];
 try {
   const sample = await jsonRequest(`${PARSE_URL}/classes/Company?limit=1&keys=objectId`, {}, true);
@@ -688,6 +690,15 @@ try {
   for (const action of ['ApplyAudit','ApplyInfo','PageContent','EditContent','Content_Edit','PageConfig','PageTemplate','PageTemplateAdd','SetPageOrder','PageConfig_Update','PageTemplate_Add','SetPageOrder_Batch','SetPageOrder_UpMove','SetPageOrder_DownMove','Apply_Update']) {
     const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyPageBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧黄页动作 ${action} 未返回明确迁移阻塞`);
   }
+  const platformSuffix = Date.now().toString(36); const platformUsername = `plat_member_${platformSuffix}`; const platformUser = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'createUser', username: platformUsername, password: randomBytes(24).toString('base64url'), displayName: '协同办公冒烟成员' }); temporaryPlatformMemberId = String(platformUser.objectId || ''); if (!temporaryPlatformMemberId || !Number(platformUser.userId)) throw new Error('协同办公临时成员创建失败');
+  const createdPlatformCompany = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'savePlatformCompany', className: 'PlatComp', creatorUsername: platformUsername, fields: { compName: `冒烟协同企业-${platformSuffix}`, compLogo: '', createTime: new Date().toISOString(), registerTime: new Date(Date.now() - 86400000).toISOString(), telephone: '010-12345678', mobile: '13800138000', compHref: '/platform-smoke', compDesc: '临时协同办公企业', mails: 'smoke@example.com', compShort: '冒烟企业', photo: '', faren: '冒烟法人', compType: 1, peoples: '1', registerMoney: 100, keywords: '冒烟', industry: '教育', registerAddr: '测试地址', proAddr: '测试地址' } }); temporaryPlatformCompanyId = String(createdPlatformCompany.objectId || ''); if (!temporaryPlatformCompanyId || !Number(createdPlatformCompany.id) || createdPlatformCompany.sourceKey !== `[["ID",${Number(createdPlatformCompany.id)}]]`) throw new Error(`协同办公企业原子编号创建异常:${JSON.stringify(createdPlatformCompany)}`);
+  const platformCompanyList = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'platformCompanies', className: 'PlatComp', page: 1, pageSize: 20, search: platformSuffix }); if (platformCompanyList.total !== 1 || platformCompanyList.results?.[0]?.objectId !== temporaryPlatformCompanyId) throw new Error('协同办公企业专用列表失败');
+  const updatedPlatformCompany = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'savePlatformCompany', className: 'PlatComp', objectId: temporaryPlatformCompanyId, creatorUsername: platformUsername, fields: { ...createdPlatformCompany, compName: `更新协同企业-${platformSuffix}`, compHref: '/platform-smoke-updated', createTime: createdPlatformCompany.createTime, registerTime: createdPlatformCompany.registerTime } }); if (updatedPlatformCompany.compName !== `更新协同企业-${platformSuffix}` || Number(updatedPlatformCompany.id) !== Number(createdPlatformCompany.id)) throw new Error('协同办公企业专用编辑失败');
+  const addedPlatformMember = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'platformCompanyMemberBatch', companyObjectId: temporaryPlatformCompanyId, action: 'add', usernames: [platformUsername] }); if (addedPlatformMember.updated !== 1) throw new Error('协同办公企业成员加入失败'); const platformMembers = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'platformCompanyMembers', companyObjectId: temporaryPlatformCompanyId, page: 1, pageSize: 100, search: platformSuffix }); if (platformMembers.total !== 1 || platformMembers.results?.[0]?.objectId !== temporaryPlatformMemberId) throw new Error('协同办公企业成员列表失败'); await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'platformCompanyBatch', className: 'PlatComp', action: 'delete', objectIds: [temporaryPlatformCompanyId] }, 409);
+  const removedPlatformMember = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'platformCompanyMemberBatch', companyObjectId: temporaryPlatformCompanyId, action: 'remove', objectIds: [temporaryPlatformMemberId] }); if (removedPlatformMember.updated !== 1) throw new Error('协同办公企业成员移出失败'); const deletedPlatformCompany = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'platformCompanyBatch', className: 'PlatComp', action: 'delete', objectIds: [temporaryPlatformCompanyId] }); if (deletedPlatformCompany.updated !== 1) throw new Error('协同办公企业删除失败'); temporaryPlatformCompanyId = ''; await jsonRequest(`${PARSE_URL}/users/${temporaryPlatformMemberId}`, { method: 'DELETE' }, true); temporaryPlatformMemberId = '';
+  for (const action of ['Default','AuditApply','AuditApply_Agree','AuditApply_Reject','CreateComp','PlatInfoDeail','PlatInfoDetail_Submit','PlatInfoManage','TopicList','WordTlp','Crud','CrudAdd','CrudAdd_Submit','GroupAdmin_API','API_msg_topic']) {
+    const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyPlatBlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧协同办公动作 ${action} 未返回明确迁移阻塞`);
+  }
   for (const action of ['Default','FlowInfo','FlowInfo_Submit','Flow_API','SelFlowModel','FlowItem','FlowItemInfo','FlowItemInfo_Submit','FlowItem_API','AddMisModel','AddMisModel_Submit','AddSign','AddSign_Submit','AddSign_Delete','ApplyManage','ApplyManage_Delete','MisModelManage','MisModelManage_Delete','OAConfig','SelModelFieds','SignManage']) {
     const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyOABlocker', action }, 501); if (!String(blocked.message).includes('migration_blocked')) throw new Error(`旧 OA 动作 ${action} 未返回明确迁移阻塞`);
   }
@@ -927,6 +938,8 @@ try {
   if (temporaryShopFareTemplateId) await jsonRequest(`${PARSE_URL}/classes/ShopFareTlp/${temporaryShopFareTemplateId}`, { method: 'DELETE' }, true).catch((error) => { console.error(`临时商城运费模板清理失败:${error.message}`); process.exitCode = 1; });
   if (temporaryMisTypeId) await jsonRequest(`${PARSE_URL}/classes/MisType/${temporaryMisTypeId}`, { method: 'DELETE' }, true).catch((error) => { console.error(`临时 OA 流程类型清理失败:${error.message}`); process.exitCode = 1; });
   if (temporaryPageStyleId) await jsonRequest(`${PARSE_URL}/classes/PageStyle/${temporaryPageStyleId}`, { method: 'DELETE' }, true).catch((error) => { console.error(`临时黄页样式清理失败:${error.message}`); process.exitCode = 1; });
+  if (temporaryPlatformMemberId) await jsonRequest(`${PARSE_URL}/users/${temporaryPlatformMemberId}`, { method: 'DELETE' }, true).catch((error) => { console.error(`临时协同办公成员清理失败:${error.message}`); process.exitCode = 1; });
+  if (temporaryPlatformCompanyId) await jsonRequest(`${PARSE_URL}/classes/PlatComp/${temporaryPlatformCompanyId}`, { method: 'DELETE' }, true).catch((error) => { console.error(`临时协同办公企业清理失败:${error.message}`); process.exitCode = 1; });
   if (baikeRestore) await jsonRequest(`${PARSE_URL}/classes/Baike/${baikeRestore.objectId}`, { method: 'PUT', body: JSON.stringify(baikeRestore.fields) }, true).catch((error) => { console.error(`百科冒烟数据恢复失败:${error.message}`); process.exitCode = 1; });
   if (contentHitObjectId) await jsonRequest(`${PARSE_URL}/classes/CommonModel/${contentHitObjectId}`, { method: 'PUT', body: JSON.stringify({ hits: contentHitOriginal }) }, true).catch((error) => {
     console.error(`公开内容浏览量恢复失败:${error.message}`);

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

@@ -54,6 +54,7 @@ export const ADMIN_NAVIGATION = [
   { label: '系统日志', icon: 'scroll-text', route: '/admin/resources/SysLog' },
   { label: 'OA 流程类型', icon: 'workflow', route: '/admin/resources/MisType' },
   { label: '黄页样式', icon: 'layout-template', route: '/admin/resources/PageStyle' },
+  { label: '协同办公企业', icon: 'building-2', route: '/admin/resources/PlatComp' },
   { 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' },

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

@@ -4444,155 +4444,155 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Office / Plat",
     "action": "Default",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "旧默认页跳转到已缺失的 Blog_Msg 动态管理,目标没有可进入的信息首页"
   },
   {
     "module": "Office / Plat",
     "action": "AddComp",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "已由 Angular 协同办公企业新建/编辑抽屉替代"
   },
   {
     "module": "Office / Plat",
     "action": "AddComp_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "savePlatformCompany 校验企业资料、创建人、防重并原子分配旧数字 ID"
   },
   {
     "module": "Office / Plat",
     "action": "AuditApply",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 Common_UserApply 协同办公申请主表"
   },
   {
     "module": "Office / Plat",
     "action": "AuditApply_Agree",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可批量通过并改为状态 99 的协同办公申请"
   },
   {
     "module": "Office / Plat",
     "action": "AuditApply_Reject",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可批量拒绝并改为状态 -1 的协同办公申请"
   },
   {
     "module": "Office / Plat",
     "action": "CompList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "platformCompanies 提供帐套隔离、关键词、旧编号排序和分页列表"
   },
   {
     "module": "Office / Plat",
     "action": "CompInfo",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "已由协同办公企业详情抽屉显示完整 PlatComp 字段"
   },
   {
     "module": "Office / Plat",
     "action": "CreateComp",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "Angular 企业表单已覆盖主表创建;旧动作仅剩已丢失 Razor 向导,未知交互无法恢复"
   },
   {
     "module": "Office / Plat",
     "action": "MemberList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "platformCompanyMembers 从 _User.legacyUserPlat 安全还原企业成员列表"
   },
   {
     "module": "Office / Plat",
     "action": "Member_Add",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "platformCompanyMemberBatch 按同帐套账号新增或移动旧 User_Plat 成员"
   },
   {
     "module": "Office / Plat",
     "action": "Member_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "implemented",
+    "reason": "platformCompanyMemberBatch remove 等价删除成员的 legacyUserPlat 记录"
   },
   {
     "module": "Office / Plat",
     "action": "PlatInfoDeail",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 Blog_Msg 动态消息主表"
   },
   {
     "module": "Office / Plat",
     "action": "PlatInfoDetail_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可编辑正文与日期的旧协同办公动态消息"
   },
   {
     "module": "Office / Plat",
     "action": "PlatInfoManage",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有 Blog_Msg 顶级动态、回收状态和关键词数据"
   },
   {
     "module": "Office / Plat",
     "action": "TopicList",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 Plat_Topic 话题主表与星标状态"
   },
   {
     "module": "Office / Plat",
     "action": "WordTlp",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-infrastructure",
+    "reason": "旧文字模板仅剩已丢失的 Razor 视图,目标没有模板数据或渲染协议"
   },
   {
     "module": "Office / Plat",
     "action": "API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "partial",
+    "reason": "platformCompanyBatch 已覆盖带成员/角色引用保护的 comp_del;消息与话题分支因主表缺失而阻塞"
   },
   {
     "module": "Office / Plat",
     "action": "Crud",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 Plat_Group 企业部门/群组树"
   },
   {
     "module": "Office / Plat",
     "action": "CrudAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可编辑的企业群组、上级和成员管理员关系"
   },
   {
     "module": "Office / Plat",
     "action": "CrudAdd_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标缺少 Plat_Group 的 FirstID/Depth/ManageIDS/MemberIDS 持久层"
   },
   {
     "module": "Office / Plat",
     "action": "GroupAdmin_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可删除的 Plat_Group 企业群组记录"
   },
   {
     "module": "Label / PowerShell",

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

@@ -96,6 +96,8 @@
         <button class="danger-action" type="button" [disabled]="updatingMisType()" (click)="applyBulkMisTypeDelete()">删除流程类型</button>
       } @else if (className() === 'PageStyle') {
         <button class="danger-action" type="button" [disabled]="updatingPageStyle()" (click)="applyBulkPageStyleDelete()">删除黄页样式</button>
+      } @else if (className() === 'PlatComp') {
+        <button class="danger-action" type="button" [disabled]="updatingPlatformCompany()" (click)="applyBulkPlatformCompanyDelete()">删除企业</button>
       } @else if (className() === 'Baike') {
         <button type="button" [disabled]="updatingBaike()" (click)="applyBulkBaikeAction('audit')">审核</button><button type="button" [disabled]="updatingBaike()" (click)="applyBulkBaikeAction('unaudit')">取消审核</button><button type="button" [disabled]="updatingBaike()" (click)="applyBulkBaikeAction('elite')">推荐</button><button type="button" [disabled]="updatingBaike()" (click)="applyBulkBaikeAction('unelite')">取消推荐</button>
       } @else {
@@ -178,6 +180,20 @@
         @if (className() === 'PageStyle') {
           <section class="account-create-note"><strong>用户黄页样式</strong><span>名称、模板路径、缩略图、排序、启用与默认状态由专用云函数校验;旧 PageNodeid 在帐套内原子分配,同一帐套只保留一个默认样式,仍被页面模板引用的样式不能删除。</span></section>
         }
+        @if (className() === 'PlatComp') {
+          <section class="account-create-note"><strong>协同办公企业</strong><span>企业主表与旧 User_Plat 成员已按原字段迁移;企业 ID 由云函数原子分配,创建账号必须是同帐套且具备旧用户 ID。删除会保护仍有成员或 PlatUserRole 角色的企业。</span></section>
+          <label><span>创建账号 <small>同帐套登录账号 · 必填</small></span><input autocomplete="off" [ngModel]="platformCreatorUsername()" (ngModelChange)="platformCreatorUsername.set($event)" placeholder="username" /></label>
+          @if (draftId()) {
+            <section class="content-controls platform-members">
+              <header><div><strong>企业成员</strong><span>对应旧 MemberList / Member_Add / Member_API</span></div><b>{{ platformMembers()?.total || 0 }} 人</b></header>
+              <label><span>加入成员账号</span><input autocomplete="off" [ngModel]="platformMemberUsername()" (ngModelChange)="platformMemberUsername.set($event)" (keyup.enter)="addPlatformMember()" placeholder="输入同帐套账号" /></label>
+              <button type="button" [disabled]="updatingPlatformCompany() || !platformMemberUsername().trim()" (click)="addPlatformMember()">加入企业</button>
+              @if (platformMembersLoading()) { <div class="content-action-status">正在加载成员…</div> }
+              @if (platformMembersError()) { <div class="content-action-status error">{{ platformMembersError() }}</div> }
+              <div class="platform-member-table"><table><thead><tr><th>用户 ID</th><th>账号</th><th>姓名</th><th>岗位</th><th>操作</th></tr></thead><tbody>@for (member of platformMembers()?.results || []; track member['objectId']) { <tr><td>{{ member['userId'] }}</td><td>{{ member['username'] }}</td><td>{{ member['displayName'] }}</td><td>{{ member['post'] || '—' }}</td><td><button class="danger-action" type="button" [disabled]="updatingPlatformCompany()" (click)="removePlatformMember(member)">移出</button></td></tr> } @empty { <tr><td colspan="5">暂无成员</td></tr> }</tbody></table></div>
+            </section>
+          }
+        }
         @if (['ExamSysPapers', 'ExamSysQuestions', 'PaperQuestions', 'ExamType', 'ContentPublish', 'PublishNode'].includes(className())) {
           <section class="account-create-note"><strong>考试与报刊历史数据只读</strong><span>已保留帐套隔离列表、搜索和详情;试卷题目关系、答案解析、发布父子结构与旧数字 ID 不能通过通用表单修改,等待对应专用写入链完成。</span></section>
         }

+ 1 - 0
src/app/admin/pages/admin-resource.component.scss

@@ -9,4 +9,5 @@
 .password-reset { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; gap: 13px; margin-top: 4px; padding: 16px; border: 1px solid #dce8e3; border-radius: 11px; background: #f7fbf9; }.password-reset > header { grid-column: 1 / -1; justify-content: flex-start; padding: 0 0 12px; border-bottom: 1px solid #dfeae5; color: #28795b; }.password-reset > header div { gap: 2px; }.password-reset > header strong { font-size: 13px; }.password-reset > header span { font-size: 10px; }.password-reset > button { grid-column: 1 / -1; justify-self: end; padding: 8px 13px; border: 0; border-radius: 8px; color: white; background: #287e5e; cursor: pointer; }.password-reset > button:disabled { opacity: .55; }.password-status { grid-column: 1 / -1; font-size: 11px; }.password-status.error { color: #b33f48; }.password-status.success { color: #237454; }
 .account-create { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; gap: 17px; }.account-create-note { grid-column: 1 / -1; display: grid; gap: 5px; padding: 14px 15px; border: 1px solid #dce8e3; border-radius: 10px; color: #28795b; background: #f7fbf9; }.account-create-note strong { font-size: 13px; }.account-create-note span { color: #668175; font-size: 11px; line-height: 1.6; }.account-create-error { grid-column: 1 / -1; padding: 10px 12px; border-radius: 8px; color: #b33f48; background: #fff2f3; font-size: 11px; }
 .user-controls, .content-controls { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr auto; gap: 13px; padding: 16px; border: 1px solid #dbe4ed; border-radius: 11px; background: #f8fafc; }.content-controls { border-color: #e2dfd2; background: #fcfbf6; }:is(.user-controls,.content-controls) > header { grid-column: 1 / -1; padding: 0 0 12px; border-bottom: 1px solid #e2e8ef; }:is(.user-controls,.content-controls) > header strong { font-size: 13px; }:is(.user-controls,.content-controls) > header b { padding: 5px 9px; border-radius: 999px; font-size: 11px; }.user-controls > header b { color: #237454; background: #e5f5ed; }.content-controls > header b { color: #775f20; background: #f3e9c8; }:is(.user-controls > header b.disabled,.content-controls > header b.recycled) { color: #a23e47; background: #fdebed; }.reason-field, .status-actions, .content-recycle { grid-column: 1 / -1; }.status-actions, .content-recycle { display: flex; justify-content: flex-end; }:is(.status-actions button,.group-submit,.content-controls > button,.content-recycle button) { align-self: end; min-height: 38px; padding: 0 13px; border: 1px solid #cfd9e4; border-radius: 8px; color: #476174; background: white; cursor: pointer; }:is(.status-actions,.content-recycle) .danger-action { color: #a74049; }:is(.user-controls,.content-controls) button:disabled { opacity: .5; }.user-action-status, .content-action-status { grid-column: 1 / -1; font-size: 11px; }:is(.user-action-status,.content-action-status).error { color: #b33f48; }:is(.user-action-status,.content-action-status).success { color: #237454; }
+.platform-member-table { grid-column: 1 / -1; max-height: 250px; overflow: auto; }.platform-member-table table { min-width: 500px; }.platform-member-table button { color: #a74049; }
 @media (max-width: 700px) { .resource-header { align-items: start; }.resource-header span { display: none; }.header-actions button:first-child { display: none; }.editor-fields, .account-create, .user-controls, .content-controls { grid-template-columns: 1fr; }.editor-fields label:has(textarea) { grid-column: auto; }.pagination > span { display: none; }.pagination { justify-content: flex-end; } }

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

@@ -1052,4 +1052,26 @@ describe('AdminResourceComponent', () => {
     await component.applyBulkPageStyleDelete();
     expect(functions.admin.calls.first().args).toEqual(['pageStyleBatch', { className: 'PageStyle', action: 'delete', objectIds: ['style-a', 'style-b'] }]); expect(component.bulkMessage()).toContain('2 个黄页样式');
   });
+
+  it('loads and saves platform companies through dedicated operations', async () => {
+    component.className.set('PlatComp'); component.schema.set({ className: 'PlatComp', label: '协同办公企业', writable: true, creatable: true, supportsSoftDelete: false, fields: [] }); component.search.set('小树'); component.platformCreatorUsername.set('creator'); functions.admin.and.callFake(async (operation: string) => operation === 'savePlatformCompany' ? { objectId: 'plat-company', id: 2 } : { className: 'PlatComp', page: 1, pageSize: 20, total: 1, results: [] });
+    await component.load(1);
+    expect(functions.admin.calls.first().args).toEqual(['platformCompanies', { className: 'PlatComp', page: 1, pageSize: 20, search: '小树' }]);
+    functions.admin.calls.reset(); component.draft.set({ compName: '小树协同', compType: 1, registerMoney: 100 }); await component.save();
+    expect(functions.admin.calls.first().args).toEqual(['savePlatformCompany', { className: 'PlatComp', objectId: undefined, fields: { compName: '小树协同', compType: 1, registerMoney: 100 }, creatorUsername: 'creator' }]);
+  });
+
+  it('adds and removes embedded platform members through protected operations', async () => {
+    component.className.set('PlatComp'); component.draftId.set('company-a'); component.platformMemberUsername.set('member-a'); functions.admin.and.callFake(async (operation: string) => operation === 'platformCompanyMembers' ? { companyObjectId: 'company-a', compId: 2, creatorUsername: 'creator', page: 1, pageSize: 100, total: 0, results: [] } : { updated: 1 });
+    await component.addPlatformMember();
+    expect(functions.admin.calls.first().args).toEqual(['platformCompanyMemberBatch', { companyObjectId: 'company-a', action: 'add', usernames: ['member-a'] }]);
+    functions.admin.calls.reset(); spyOn(window, 'confirm').and.returnValue(true); await component.removePlatformMember({ objectId: 'user-a', username: 'member-a' });
+    expect(functions.admin.calls.first().args).toEqual(['platformCompanyMemberBatch', { companyObjectId: 'company-a', action: 'remove', objectIds: ['user-a'] }]);
+  });
+
+  it('deletes selected platform companies through the reference-protected batch operation', async () => {
+    spyOn(window, 'confirm').and.returnValue(true); component.className.set('PlatComp'); component.selectedIds.set(['company-a']); functions.admin.and.callFake(async (operation: string) => operation === 'platformCompanyBatch' ? { action: 'delete', updated: 1 } : { className: 'PlatComp', page: 1, pageSize: 20, total: 0, results: [] });
+    await component.applyBulkPlatformCompanyDelete();
+    expect(functions.admin.calls.first().args).toEqual(['platformCompanyBatch', { className: 'PlatComp', action: 'delete', objectIds: ['company-a'] }]); expect(component.bulkMessage()).toContain('1 个协同办公企业');
+  });
 });

+ 60 - 7
src/app/admin/pages/admin-resource.component.ts

@@ -14,6 +14,16 @@ interface SurveyChart {
   questions: { id: string; title: string; type: string; options: { text: string; value: string; count: number }[]; otherCount: number }[];
 }
 
+interface PlatformMemberPage {
+  companyObjectId: string;
+  compId: number;
+  creatorUsername: string;
+  page: number;
+  pageSize: number;
+  total: number;
+  results: Record<string, unknown>[];
+}
+
 @Component({
   selector: 'app-admin-resource',
   standalone: true,
@@ -149,18 +159,24 @@ export class AdminResourceComponent implements OnInit {
   readonly filterSystemLogEndDate = signal('');
   readonly updatingMisType = signal(false);
   readonly updatingPageStyle = signal(false);
+  readonly updatingPlatformCompany = signal(false);
+  readonly platformMembers = signal<PlatformMemberPage | null>(null);
+  readonly platformMembersLoading = signal(false);
+  readonly platformMembersError = signal('');
+  readonly platformMemberUsername = signal('');
+  readonly platformCreatorUsername = 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', 'typeName', 'pageNodeName', '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 preferred = ['objectId', 'name', 'title', 'typeName', 'pageNodeName', 'compName', '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);
   });
   readonly pageCount = computed(() => Math.max(1, Math.ceil((this.page()?.total ?? 0) / (this.page()?.pageSize || 20))));
-  readonly bulkEnabled = computed(() => ['_User', 'CommonModel', 'Node', 'Special', 'Guestbook', 'GuestBar', 'Datadiccategory', 'Datadic', 'GradeCate', 'Grade', 'Currency', 'SysHoliday', 'Search', 'AdZone', 'AdInfo', 'FontPicShape', 'FontPicShapeType', 'DesignAsk', 'DesignQuestion', 'DesignAnswer', 'ServiceSeat', 'Temp', 'StoreApplication', 'StoreStyle', 'UserLevel', 'Baike', 'CRMSAttr', 'ShopFareTlp', 'MisType', 'PageStyle'].includes(this.className()));
+  readonly bulkEnabled = computed(() => ['_User', 'CommonModel', 'Node', 'Special', 'Guestbook', 'GuestBar', 'Datadiccategory', 'Datadic', 'GradeCate', 'Grade', 'Currency', 'SysHoliday', 'Search', 'AdZone', 'AdInfo', 'FontPicShape', 'FontPicShapeType', 'DesignAsk', 'DesignQuestion', 'DesignAnswer', 'ServiceSeat', 'Temp', 'StoreApplication', 'StoreStyle', 'UserLevel', 'Baike', 'CRMSAttr', 'ShopFareTlp', 'MisType', 'PageStyle', 'PlatComp'].includes(this.className()));
   readonly allPageSelected = computed(() => {
     const ids = (this.page()?.results || []).map((row) => String(row['objectId'] || '')).filter(Boolean);
     return Boolean(ids.length) && ids.every((id) => this.selectedIds().includes(id));
@@ -197,7 +213,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' : this.className() === 'SysLog' ? 'systemLogs' : this.className() === 'MisType' ? 'misTypes' : this.className() === 'PageStyle' ? 'pageStyles' : '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' : this.className() === 'MisType' ? 'misTypes' : this.className() === 'PageStyle' ? 'pageStyles' : this.className() === 'PlatComp' ? 'platformCompanies' : '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(); }
@@ -217,21 +233,22 @@ export class AdminResourceComponent implements OnInit {
     finally { this.loading.set(false); }
   }
 
-  openCreate(): void { this.draftId.set(''); this.draft.set({}); this.fieldErrors.set({}); this.fieldInputs.set({}); this.resetPasswordFields(); this.resetCreateUserFields(); this.resetUserActions(); this.resetContentActions(); this.resetNodeActions(); this.resetSpecialActions(); this.resetGuestbookActions(); this.resetGuestCategoryActions(); this.resetBarActions(); this.resetExamClassActions(); this.resetExamPointActions(); this.resetKnowledgeActions(); this.resetTeacherActions(); this.resetDictionaryActions(); this.resetGradeActions(); this.resetAdActions(); this.resetFontShapeActions(); this.resetSurveyActions(); this.resetServiceActions(); this.resetStoreActions(); this.resetTagKeyActions(); this.editorOpen.set(true); }
+  openCreate(): void { this.draftId.set(''); this.draft.set({}); this.fieldErrors.set({}); this.fieldInputs.set({}); this.resetPasswordFields(); this.resetCreateUserFields(); this.resetUserActions(); this.resetContentActions(); this.resetNodeActions(); this.resetSpecialActions(); this.resetGuestbookActions(); this.resetGuestCategoryActions(); this.resetBarActions(); this.resetExamClassActions(); this.resetExamPointActions(); this.resetKnowledgeActions(); this.resetTeacherActions(); this.resetDictionaryActions(); this.resetGradeActions(); this.resetAdActions(); this.resetFontShapeActions(); this.resetSurveyActions(); this.resetServiceActions(); this.resetStoreActions(); this.resetTagKeyActions(); this.resetPlatformCompanyActions(); this.editorOpen.set(true); }
   async openEdit(row: Record<string, unknown>): Promise<void> {
     const objectId = String(row['objectId'] ?? '');
     this.error.set('');
     try {
       const detailOperation = this.className() === 'ContentTagKey' ? 'getContentTagKeys' : this.className() === 'UserMoneyLog' ? 'getUserMoneyLog' : 'get';
       const detail = await this.functions.admin<Record<string, unknown>>(detailOperation, { className: this.className(), objectId });
-      this.draftId.set(objectId); this.draft.set(detail); this.fieldErrors.set({}); this.fieldInputs.set({}); this.resetPasswordFields(); this.resetCreateUserFields(); this.resetUserActions(detail); this.resetContentActions(detail); this.resetNodeActions(detail); this.resetSpecialActions(detail); this.resetGuestbookActions(detail); this.resetGuestCategoryActions(detail); this.resetBarActions(detail); this.resetExamClassActions(detail); this.resetExamPointActions(detail); this.resetKnowledgeActions(detail); this.resetTeacherActions(detail); this.resetDictionaryActions(detail); this.resetGradeActions(detail); this.resetAdActions(detail); this.resetFontShapeActions(detail); this.resetSurveyActions(); this.resetServiceActions(detail); this.resetStoreActions(detail); this.resetTagKeyActions(detail); this.editorOpen.set(true);
+      this.draftId.set(objectId); this.draft.set(detail); this.fieldErrors.set({}); this.fieldInputs.set({}); this.resetPasswordFields(); this.resetCreateUserFields(); this.resetUserActions(detail); this.resetContentActions(detail); this.resetNodeActions(detail); this.resetSpecialActions(detail); this.resetGuestbookActions(detail); this.resetGuestCategoryActions(detail); this.resetBarActions(detail); this.resetExamClassActions(detail); this.resetExamPointActions(detail); this.resetKnowledgeActions(detail); this.resetTeacherActions(detail); this.resetDictionaryActions(detail); this.resetGradeActions(detail); this.resetAdActions(detail); this.resetFontShapeActions(detail); this.resetSurveyActions(); this.resetServiceActions(detail); this.resetStoreActions(detail); this.resetTagKeyActions(detail); this.resetPlatformCompanyActions(detail); this.editorOpen.set(true);
       if (this.className() === '_User') await this.loadUserGroups();
       if (this.className() === 'AdInfo' && !this.adZones().length) await this.loadAdZones();
       if (this.className() === 'AdZone') await this.loadAdPreview();
       if (this.className() === 'FontPicShape' && !this.fontShapeTypes().length) await this.loadFontShapeTypes();
+      if (this.className() === 'PlatComp') await this.loadPlatformMembers();
     } catch (error) { this.error.set(error instanceof Error ? error.message : '详情加载失败'); }
   }
-  closeEditor(): void { this.editorOpen.set(false); this.draft.set({}); this.draftId.set(''); this.fieldErrors.set({}); this.fieldInputs.set({}); this.resetPasswordFields(); this.resetCreateUserFields(); this.resetUserActions(); this.resetContentActions(); this.resetNodeActions(); this.resetSpecialActions(); this.resetGuestbookActions(); this.resetGuestCategoryActions(); this.resetBarActions(); this.resetExamClassActions(); this.resetExamPointActions(); this.resetKnowledgeActions(); this.resetTeacherActions(); this.resetDictionaryActions(); this.resetGradeActions(); this.resetAdActions(); this.resetFontShapeActions(); this.resetSurveyActions(); this.resetServiceActions(); this.resetStoreActions(); this.resetTagKeyActions(); }
+  closeEditor(): void { this.editorOpen.set(false); this.draft.set({}); this.draftId.set(''); this.fieldErrors.set({}); this.fieldInputs.set({}); this.resetPasswordFields(); this.resetCreateUserFields(); this.resetUserActions(); this.resetContentActions(); this.resetNodeActions(); this.resetSpecialActions(); this.resetGuestbookActions(); this.resetGuestCategoryActions(); this.resetBarActions(); this.resetExamClassActions(); this.resetExamPointActions(); this.resetKnowledgeActions(); this.resetTeacherActions(); this.resetDictionaryActions(); this.resetGradeActions(); this.resetAdActions(); this.resetFontShapeActions(); this.resetSurveyActions(); this.resetServiceActions(); this.resetStoreActions(); this.resetTagKeyActions(); this.resetPlatformCompanyActions(); }
   setField(field: AdminFieldSchema, raw: unknown): void {
     let value = raw;
     if (field.type === 'Number') value = raw === '' ? null : Number(raw);
@@ -295,7 +312,8 @@ export class AdminResourceComponent implements OnInit {
                                                                   : this.className() === 'CRMSAttr' ? 'saveCrmClientType'
                                                                     : this.className() === 'ShopFareTlp' ? 'saveShopFareTemplate'
                                                                       : this.className() === 'MisType' ? 'saveMisType'
-                                                                        : this.className() === 'PageStyle' ? 'savePageStyle' : 'save';
+                                                                        : this.className() === 'PageStyle' ? 'savePageStyle'
+                                                                          : this.className() === 'PlatComp' ? 'savePlatformCompany' : 'save';
       const payload: Record<string, unknown> = { className: this.className(), objectId: this.draftId() || undefined, fields: this.draft() };
       if (this.className() === 'Node') payload['parentId'] = this.targetNodeParentId();
       if (this.className() === 'Special') payload['pid'] = this.targetSpecialParentId();
@@ -311,6 +329,7 @@ export class AdminResourceComponent implements OnInit {
       if (this.className() === 'ServiceSeat') payload['username'] = this.targetServiceSeatUsername();
       if (this.className() === 'StoreApplication') { payload['username'] = this.targetStoreUsername(); payload['styleId'] = this.targetStoreStyleId(); }
       if (this.className() === 'ContentTagKey') payload['content'] = this.tagKeyContent();
+      if (this.className() === 'PlatComp') payload['creatorUsername'] = this.platformCreatorUsername();
       await this.functions.admin(operation, payload);
       this.closeEditor(); await this.load();
     } catch (error) { this.error.set(error instanceof Error ? error.message : '保存失败'); }
@@ -781,6 +800,38 @@ export class AdminResourceComponent implements OnInit {
     catch (error) { this.bulkError.set(error instanceof Error ? error.message : '黄页样式删除失败'); }
     finally { this.updatingPageStyle.set(false); }
   }
+  async loadPlatformMembers(): Promise<void> {
+    const companyObjectId = this.draftId(); this.platformMembersError.set(''); this.platformMembers.set(null);
+    if (this.className() !== 'PlatComp' || !companyObjectId) return;
+    this.platformMembersLoading.set(true);
+    try { const result = await this.functions.admin<PlatformMemberPage>('platformCompanyMembers', { companyObjectId, page: 1, pageSize: 100, search: '' }); this.platformMembers.set(result); if (result.creatorUsername) this.platformCreatorUsername.set(result.creatorUsername); }
+    catch (error) { this.platformMembersError.set(error instanceof Error ? error.message : '企业成员加载失败'); }
+    finally { this.platformMembersLoading.set(false); }
+  }
+  async addPlatformMember(): Promise<void> {
+    const username = this.platformMemberUsername().trim(); this.platformMembersError.set('');
+    if (this.className() !== 'PlatComp' || !this.draftId() || !username) { this.platformMembersError.set('请输入要加入的成员账号'); return; }
+    this.updatingPlatformCompany.set(true);
+    try { await this.functions.admin('platformCompanyMemberBatch', { companyObjectId: this.draftId(), action: 'add', usernames: [username] }); this.platformMemberUsername.set(''); await this.loadPlatformMembers(); }
+    catch (error) { this.platformMembersError.set(error instanceof Error ? error.message : '企业成员添加失败'); }
+    finally { this.updatingPlatformCompany.set(false); }
+  }
+  async removePlatformMember(member: Record<string, unknown>): Promise<void> {
+    const objectId = String(member['objectId'] || ''); if (!objectId || !confirm(`确认将 ${String(member['username'] || objectId)} 移出该企业?`)) return;
+    this.platformMembersError.set(''); this.updatingPlatformCompany.set(true);
+    try { await this.functions.admin('platformCompanyMemberBatch', { companyObjectId: this.draftId(), action: 'remove', objectIds: [objectId] }); await this.loadPlatformMembers(); }
+    catch (error) { this.platformMembersError.set(error instanceof Error ? error.message : '企业成员移除失败'); }
+    finally { this.updatingPlatformCompany.set(false); }
+  }
+  async applyBulkPlatformCompanyDelete(): Promise<void> {
+    const objectIds = this.selectedIds(); this.bulkError.set(''); this.bulkMessage.set('');
+    if (this.className() !== 'PlatComp' || !objectIds.length) { this.bulkError.set('请先选择协同办公企业'); return; }
+    if (!confirm(`确认删除选中的 ${objectIds.length} 个协同办公企业?仍有成员或角色时会拒绝。`)) return;
+    this.updatingPlatformCompany.set(true);
+    try { const result = await this.functions.admin<{ updated: number }>('platformCompanyBatch', { className: 'PlatComp', action: 'delete', objectIds }); await this.load(); this.bulkMessage.set(`已删除 ${result.updated} 个协同办公企业`); }
+    catch (error) { this.bulkError.set(error instanceof Error ? error.message : '协同办公企业删除失败'); }
+    finally { this.updatingPlatformCompany.set(false); }
+  }
   async applyBulkBaikeAction(action: 'audit' | 'unaudit' | 'elite' | 'unelite'): Promise<void> {
     const objectIds = this.selectedIds(); this.bulkError.set(''); this.bulkMessage.set('');
     if (this.className() !== 'Baike' || !objectIds.length) { this.bulkError.set('请先选择百科词条'); return; }
@@ -807,6 +858,7 @@ export class AdminResourceComponent implements OnInit {
       else if (this.className() === 'ShopFareTlp') await this.functions.admin('shopFareTemplateBatch', { className: 'ShopFareTlp', action: 'delete', objectIds: [objectId] });
       else if (this.className() === 'MisType') await this.functions.admin('misTypeBatch', { className: 'MisType', action: 'delete', objectIds: [objectId] });
       else if (this.className() === 'PageStyle') await this.functions.admin('pageStyleBatch', { className: 'PageStyle', action: 'delete', objectIds: [objectId] });
+      else if (this.className() === 'PlatComp') await this.functions.admin('platformCompanyBatch', { className: 'PlatComp', action: 'delete', objectIds: [objectId] });
       else await this.functions.admin('delete', { className: this.className(), objectId });
       await this.load();
     }
@@ -888,4 +940,5 @@ export class AdminResourceComponent implements OnInit {
   private resetServiceActions(detail: Record<string, unknown> = {}): void { this.targetServiceSeatUsername.set(String(detail['sRemrk'] || '')); }
   private resetStoreActions(detail: Record<string, unknown> = {}): void { this.targetStoreUsername.set(String(detail['userName'] || '')); this.targetStoreStyleId.set(Number(detail['storeStyleId']) || 0); }
   private resetTagKeyActions(detail: Record<string, unknown> = {}): void { this.tagKeyContent.set(String(detail['content'] || '')); }
+  private resetPlatformCompanyActions(detail: Record<string, unknown> = {}): void { this.platformMembers.set(null); this.platformMembersLoading.set(false); this.platformMembersError.set(''); this.platformMemberUsername.set(''); this.platformCreatorUsername.set(String(detail['creatorUsername'] || '')); }
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов