diff --git a/docs/操作日志_root平台账户编辑角色误提交修复_20260817.md b/docs/操作日志_root平台账户编辑角色误提交修复_20260817.md new file mode 100644 index 0000000..7c5f82d --- /dev/null +++ b/docs/操作日志_root平台账户编辑角色误提交修复_20260817.md @@ -0,0 +1,42 @@ +# root 平台账户编辑角色误提交修复操作日志 + +操作时间:2026-08-17 +操作类型:修改 +影响模块:平台总后台 / 平台管理 / 平台账户编辑 + +## 操作前状态 + +- 编辑 root 账户资料时,平台角色控件仍可编辑并随请求提交 `root`。 +- 后端按安全规则拒绝把受保护 root 角色作为普通可分配角色,导致修改联系电话等正常资料时一并失败。 + +## 具体操作 + +- 增加受保护 root 平台账户判断。 +- root 角色在编辑页保持可见但只读。 +- root 账户更新请求不再携带 `platform_role_code`。 +- 其他平台账户仍可由 root 管理员修改角色。 +- 增加静态契约防止 root 角色重新进入编辑请求。 + +## 操作后状态 + +- root 管理员可正常维护显示名称、头像和联系电话。 +- root 角色不会被普通资料编辑接口修改或重复提交。 + +## 代码变更 + +- `frontend/platform_admin/src/api/resource-page-rules.ts`:增加 root 账户角色只读与提交过滤。 +- `frontend/platform_admin/scripts/check-resource-display-contracts.mjs`:增加 root 角色保护契约。 +- `docs/项目文档_平台资源中文展示统一_v1.0.md`:增加 v1.21 记录。 + +## 验证结果 + +- 资源展示契约检查通过:`pnpm resource-display-contracts:check`。 +- 后端接口契约检查通过:`pnpm contract:check`。 +- Biome 代码检查通过。 +- TypeScript 类型检查通过:`pnpm type:check`。 +- 前端生产构建通过:`pnpm build`。 + +## 风险评估 + +- 不改变后端 root 保护规则,仅修正前端提交字段。 +- 普通平台账户角色管理逻辑不受影响。 diff --git a/docs/项目文档_平台资源中文展示统一_v1.0.md b/docs/项目文档_平台资源中文展示统一_v1.0.md index 639a2a7..983bab1 100644 --- a/docs/项目文档_平台资源中文展示统一_v1.0.md +++ b/docs/项目文档_平台资源中文展示统一_v1.0.md @@ -83,3 +83,4 @@ frontend/platform_admin/ - v1.18:客服工单用户统一展示可读名称并保留标识复制;分类与优先级统一显示中文,新建和编辑改用中文下拉并保留未知值提示。 - v1.19:客服工单详情的气站、配送点和处理人员统一展示关系名称;未分派、未开始、未完成及未提交结果等空值改用明确业务状态,不再显示裸 `-`。 - v1.20:平台账户接口补回真实创建时间和更新时间;root 未填写联系电话时显示“未填写”,不生成虚假号码。 +- v1.21:编辑 root 平台账户时保留 root 角色只读展示,但更新请求不再提交受保护角色;普通账户角色编辑能力保持不变。 diff --git a/frontend/platform_admin/scripts/check-resource-display-contracts.mjs b/frontend/platform_admin/scripts/check-resource-display-contracts.mjs index ab67612..8c6d203 100644 --- a/frontend/platform_admin/scripts/check-resource-display-contracts.mjs +++ b/frontend/platform_admin/scripts/check-resource-display-contracts.mjs @@ -21,6 +21,7 @@ function assertIncludes(source, expected, message) { const resources = read('src/api/resources.ts'); const display = read('src/api/resource-display.ts'); const detailContract = read('src/api/resource-detail-contract.ts'); +const pageRules = read('src/api/resource-page-rules.ts'); const detailPage = read('src/views/resource/ResourceDetailContent.vue'); const listPage = read('src/views/shared/CrudListPage.vue'); const listFieldDisplay = read('src/views/shared/resource-list-field-display.ts'); @@ -388,6 +389,17 @@ assertIncludes( "f('phone', { emptyText: '未填写' })", '平台账户联系电话为空时必须显示业务文案', ); +for (const rootAccountContract of [ + "String(row.platform_role_code ?? '') === 'root'", + "visibleKeys.add('platform_role_code')", + "isProtectedPlatformRootAccount(definition, row)", +]) { + assertIncludes( + pageRules, + rootAccountContract, + `root 平台账户缺少角色只读保护契约:${rootAccountContract}`, + ); +} assertIncludes( detailPage, '!hasResourceFieldLabel(props.definition, actualKey)', diff --git a/frontend/platform_admin/src/api/resource-page-rules.ts b/frontend/platform_admin/src/api/resource-page-rules.ts index f299f9a..3ddbe98 100644 --- a/frontend/platform_admin/src/api/resource-page-rules.ts +++ b/frontend/platform_admin/src/api/resource-page-rules.ts @@ -193,6 +193,17 @@ const ownerKeys: Record = { gasorder_contract: ['user_account_identity', 'gas_basic_identity'], }; +/** 判断目标是否为受保护的 root 平台账户。 */ +function isProtectedPlatformRootAccount( + definition: ResourceUiDefinition, + row: ResourceRow, +) { + return ( + definition.name === 'platform_account' && + String(row.platform_role_code ?? '') === 'root' + ); +} + /** 返回当前模式需要展示的字段;编辑页会保留不可变字段但将其设为只读。 */ export function pageFields( definition: ResourceUiDefinition, @@ -210,7 +221,11 @@ export function pageFields( if (definition.name === 'product_repair' && row.result !== 'pending') { editableKeys = ['remark']; } - if (definition.name === 'platform_account' && context.role !== 'root') { + const protectedRootAccount = isProtectedPlatformRootAccount(definition, row); + if ( + definition.name === 'platform_account' && + (context.role !== 'root' || protectedRootAccount) + ) { editableKeys = editableKeys.filter((key) => key !== 'platform_role_code'); } const visibleKeys = new Set([ @@ -229,6 +244,8 @@ export function pageFields( ) .map((field) => field.key), ]); + // root 角色保持可见但只读,避免管理员误以为账户没有关联角色。 + if (protectedRootAccount) visibleKeys.add('platform_role_code'); return definition.fields.filter( (field) => visibleKeys.has(field.key) && @@ -246,7 +263,11 @@ export function editableFields( let keys = rule?.editKeys ?? definition.fields.map((field) => field.key); if (definition.name === 'product_repair' && row.result !== 'pending') keys = ['remark']; - if (definition.name === 'platform_account' && context.role !== 'root') { + if ( + definition.name === 'platform_account' && + (context.role !== 'root' || + isProtectedPlatformRootAccount(definition, row)) + ) { keys = keys.filter((key) => key !== 'platform_role_code'); } const allowed = new Set(keys);