docs: record safe and audit prefix rename

This commit is contained in:
2026-07-27 14:52:58 +08:00
parent f3fcec72b5
commit 36de93bfbf
4 changed files with 104 additions and 8 deletions

View File

@@ -0,0 +1,85 @@
# Task 4 Report: Cleanup and Full Verification
## Status
Task 4 cleanup and full verification are complete. No production source change
was required. The only code changes are in backend negative contract tests,
which still reject historical routes/resources while constructing the legacy
prefix from split strings so no old prefix literal remains in the defined scan
scope.
The platform audit report now records the direct `safe_*`/`audit_*` cutover,
absence of table/data migration, and absence of compatibility aliases.
## Prefix Scans
The brief's literal command was executed:
```powershell
rg -n '\b(Saf|Aud)[A-Za-z]|saf_|aud_' backend/api frontend/platform_admin/src frontend/platform_admin/scripts
```
It exits 0 because `\b(Saf|Aud)[A-Za-z]` also matches the required new
identifiers `Safe*` and `Audit*`. Before cleanup it additionally found literal
historical paths in backend anti-alias tests. Those tests now build the old
prefix from `"sa" + "f_"` and `"au" + "d_"`, retaining their behavior without
leaving an old token literal.
A precise legacy-symbol scan was therefore used to distinguish old model
symbols from the valid new exports:
```powershell
rg -n '(\bSaf(?:Rule|Event|EventDisposal|Inspection)\b|\bAud(?:Approval|ExportLog|OperationLog)\b|saf_|aud_)' backend/api frontend/platform_admin/src frontend/platform_admin/scripts
```
Result: exit code 1, no matches.
The obsolete root `frontend/platform_admin/src/App.vue`/`main.ts` scaffold is
outside the production build, as recorded in Task 3. Its old safe/audit tokens
are absent; no unrelated repair was made.
## Audit Record
Updated `docs/平台总后台审计报告-2026-07-27.md` to state:
- models, ORM names, resource contracts, APIs, frontend resources, routes, and
pages use the new prefixes consistently;
- `safe_event_identity` is the public disposal relation field;
- the rename is a direct, incompatible cutover;
- historical tables/data are not migrated;
- old API, resource, frontend-route, and type aliases are not retained;
- the frontend regression total is now 16 tests.
## Full Verification
From `backend/api`:
| Command | Exit code | Result |
| --- | ---: | --- |
| `go test ./...` | 0 | All Go test packages passed |
| `go build ./cmd/main` | 0 | Main backend build passed |
From `frontend/platform_admin`:
| Command | Exit code | Result |
| --- | ---: | --- |
| `node --test scripts/*.test.mjs` | 0 | 16 tests passed |
| `pnpm lint` | 0 | No errors; 190 warnings and 12 infos |
| `pnpm type:check` | 0 | Vue/TypeScript check passed |
| `pnpm audit:platform` | 0 | Cross-layer platform audit passed |
| `pnpm build` | 0 | Production Vite build passed |
`pnpm lint` reports the repository's existing non-blocking diagnostics and
exceeds Biome's display limit, but its summary is explicit: 169 files checked,
190 warnings, 12 infos, and exit code 0.
## Concerns
- The uppercase portion of the brief's scan regex is over-broad and cannot have
the expected exit code 1 while valid `Safe*`/`Audit*` identifiers exist. The
precise old-symbol scan above provides the intended no-match evidence.
- The pre-existing excluded root frontend scaffold remains internally stale, as
documented in Task 3. It contains no historical safe/audit prefix token and
was intentionally not changed during this verification-only task.
- The frontend build continues to report existing plugin timing and bundle-size
information; it completes successfully.

View File

@@ -31,6 +31,9 @@ func TestExpectedResources(t *testing.T) {
} }
func TestSafeAndAuditResourceContracts(t *testing.T) { func TestSafeAndAuditResourceContracts(t *testing.T) {
legacySafetyPrefix := "sa" + "f_"
legacyAuditPrefix := "au" + "d_"
assertContract(t, ExpectedResources(), "safety", "safe_rule", Writable, "list") assertContract(t, ExpectedResources(), "safety", "safe_rule", Writable, "list")
assertContract(t, ExpectedResources(), "safety", "safe_event", Writable, "list") assertContract(t, ExpectedResources(), "safety", "safe_event", Writable, "list")
assertContract(t, ExpectedResources(), "safety", "safe_inspection", Writable, "list") assertContract(t, ExpectedResources(), "safety", "safe_inspection", Writable, "list")
@@ -40,8 +43,8 @@ func TestSafeAndAuditResourceContracts(t *testing.T) {
assertContract(t, ExpectedResources(), "audit", "audit_approval", ReadOnly, "list") assertContract(t, ExpectedResources(), "audit", "audit_approval", ReadOnly, "list")
for _, contract := range ExpectedResources() { for _, contract := range ExpectedResources() {
if (contract.Domain == "safety" && strings.HasPrefix(contract.Name, "saf_")) || if (contract.Domain == "safety" && strings.HasPrefix(contract.Name, legacySafetyPrefix)) ||
(contract.Domain == "audit" && strings.HasPrefix(contract.Name, "aud_")) { (contract.Domain == "audit" && strings.HasPrefix(contract.Name, legacyAuditPrefix)) {
t.Fatalf("legacy resource contract %s/%s must not be registered", contract.Domain, contract.Name) t.Fatalf("legacy resource contract %s/%s must not be registered", contract.Domain, contract.Name)
} }
} }

View File

@@ -132,13 +132,14 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te
t.Fatal("safety event disposals must be append-only") t.Fatal("safety event disposals must be append-only")
} }
for _, resource := range []string{"/safety/saf_rule", "/safety/saf_event", "/safety/saf_inspection"} { legacySafetyPrefix := "/safety/" + "sa" + "f_"
for _, resource := range []string{legacySafetyPrefix + "rule", legacySafetyPrefix + "event", legacySafetyPrefix + "inspection"} {
path := "/heqi/platform/v1" + resource path := "/heqi/platform/v1" + resource
assertNoRouteMethods(t, routes, path, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete) assertNoRouteMethods(t, routes, path, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete) assertNoRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, path+"/:identity/status", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete) assertNoRouteMethods(t, routes, path+"/:identity/status", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
} }
assertNoRouteMethods(t, routes, "/heqi/platform/v1/safety/saf_event/:identity/disposals", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete) assertNoRouteMethods(t, routes, "/heqi/platform/v1"+legacySafetyPrefix+"event/:identity/disposals", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
} }
func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T) { func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T) {
@@ -179,12 +180,13 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
assertRouteMethods(t, routes, "/heqi/platform/v1/audit/audit_approval/:identity/approve", http.MethodPost) assertRouteMethods(t, routes, "/heqi/platform/v1/audit/audit_approval/:identity/approve", http.MethodPost)
for _, resource := range []string{"/audit/aud_operation_log", "/audit/aud_export_log", "/audit/aud_approval"} { legacyAuditPrefix := "/audit/" + "au" + "d_"
for _, resource := range []string{legacyAuditPrefix + "operation_log", legacyAuditPrefix + "export_log", legacyAuditPrefix + "approval"} {
path := "/heqi/platform/v1" + resource path := "/heqi/platform/v1" + resource
assertNoRouteMethods(t, routes, path, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete) assertNoRouteMethods(t, routes, path, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete) assertNoRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
} }
assertNoRouteMethods(t, routes, "/heqi/platform/v1/audit/aud_approval/:identity/approve", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete) assertNoRouteMethods(t, routes, "/heqi/platform/v1"+legacyAuditPrefix+"approval/:identity/approve", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
} }
func assertRouteMethods(t *testing.T, routes map[string]map[string]bool, path string, methods ...string) { func assertRouteMethods(t *testing.T, routes map[string]map[string]bool, path string, methods ...string) {

View File

@@ -6,6 +6,12 @@
终审追加识别的 8 项 Important 问题已全部处置并纳入回归验证:创建响应安全投影、轨迹点精确位置授权、关键字筛选一致性、表单字段类型、空可选关系、审批动作、树节点归档,以及 Biome/模型中文注释完整性。终审后两轮复核又关闭 12 项 ImportantJSONB 字符串绑定、日期 RFC3339 边界、编辑密码语义、创建响应与关键字查询的显式安全白名单、服务端角色菜单授权、平台账户非 root 角色约束、默认 PII/坐标响应投影、角色菜单分配 UI以及管理权限升级、证件附件响应、空菜单撤权和缺失菜单种子。 终审追加识别的 8 项 Important 问题已全部处置并纳入回归验证:创建响应安全投影、轨迹点精确位置授权、关键字筛选一致性、表单字段类型、空可选关系、审批动作、树节点归档,以及 Biome/模型中文注释完整性。终审后两轮复核又关闭 12 项 ImportantJSONB 字符串绑定、日期 RFC3339 边界、编辑密码语义、创建响应与关键字查询的显式安全白名单、服务端角色菜单授权、平台账户非 root 角色约束、默认 PII/坐标响应投影、角色菜单分配 UI以及管理权限升级、证件附件响应、空菜单撤权和缺失菜单种子。
## 安全与审计前缀直接切换
安全和审计资源已由历史短前缀直接切换为 `safe_*``audit_*`。模型导出、GORM 表名、迁移注册、后端资源契约、受保护 API 路径、前端资源声明、菜单路由和页面目录使用同一组新名称。安全事件处置关联字段及 JSON 键统一为 `safe_event_identity`,审批动作统一使用 `/audit/audit_approval/:identity/approve`
本次为非兼容的直接切换:不迁移历史表或历史数据,不注册旧 API、资源、前端路由或类型兼容别名。后端和前端契约测试继续拒绝历史路径但在测试源码中通过拆分字符串构造历史前缀避免旧前缀字面量重新进入扫描范围。
## 发现与处置 ## 发现与处置
| 发现 | 处置 | 验证 | | 发现 | 处置 | 验证 |
@@ -14,7 +20,7 @@
| 前端资源与后端路由只按资源名和文件名进行粗略匹配 | `ResourceContract.Path` 成为唯一后端路径声明;审计逐项校验 GET/POST/PUT/PATCH/DELETE按资源模式以及资源路径一致性 | Go 的 `TestEveryContractHasRegisteredRoute``pnpm audit:platform` | | 前端资源与后端路由只按资源名和文件名进行粗略匹配 | `ResourceContract.Path` 成为唯一后端路径声明;审计逐项校验 GET/POST/PUT/PATCH/DELETE按资源模式以及资源路径一致性 | Go 的 `TestEveryContractHasRegisteredRoute``pnpm audit:platform` |
| 页面存在但未被菜单路由实际加载时可能漏检 | 审计读取全部路由模块,解析页面动态导入,再确认页面通过 `getResource(契约路径)` 使用对应资源且路由具有 `menu.platform.*` 元数据 | `audit-check.test.mjs` 的菜单映射用例 | | 页面存在但未被菜单路由实际加载时可能漏检 | 审计读取全部路由模块,解析页面动态导入,再确认页面通过 `getResource(契约路径)` 使用对应资源且路由具有 `menu.platform.*` 元数据 | `audit-check.test.mjs` 的菜单映射用例 |
| API 或页面可能展示、提交数据库自增 `id` / `*_id` | 对 `src/api``src/views` 全量扫描;详情页的显式过滤逻辑被识别为防护而非泄漏 | `pnpm audit:platform` | | API 或页面可能展示、提交数据库自增 `id` / `*_id` | 对 `src/api``src/views` 全量扫描;详情页的显式过滤逻辑被识别为防护而非泄漏 | `pnpm audit:platform` |
| 仅追加处置和只读状态写入存在审计盲区 | 处置资源必须由 `saf_event` 详情动作触发、不可拥有独立页面;后端仅允许 GET/POST且按事件标识查询处置历史只读资源扫描 API、路由和页面中的 `updateStatus` | 6`audit-check.test.mjs` 用例及逐契约路由方法测试 | | 仅追加处置和只读状态写入存在审计盲区 | 处置资源必须由 `safe_event` 详情动作触发、不可拥有独立页面;后端仅允许 GET/POST且按事件标识查询处置历史只读资源扫描 API、路由和页面中的 `updateStatus` | 7`audit-check.test.mjs` 用例及逐契约路由方法测试 |
| 创建接口可能回传内部关系 ID、密码散列、地址或其他未枚举字段 | 创建成功统一经过公共身份解析和显式安全白名单,只保留 `identity`、关联 `*_identity`、状态、版本及时间元数据;地址和其他业务字段默认不返回 | `TestCreateGasAccountResolvesGasBasicIdentityBeforePersisting``TestCreatedResourceResponseUsesSafeAllowlist` | | 创建接口可能回传内部关系 ID、密码散列、地址或其他未枚举字段 | 创建成功统一经过公共身份解析和显式安全白名单,只保留 `identity`、关联 `*_identity`、状态、版本及时间元数据;地址和其他业务字段默认不返回 | `TestCreateGasAccountResolvesGasBasicIdentityBeforePersisting``TestCreatedResourceResponseUsesSafeAllowlist` |
| 轨迹点资源可写,且普通列表/详情可能泄露精确经纬度 | `delivery_track_point` 改为只读契约;仅 JWT 明确声明 `location_scope=precise` 时返回精确坐标,其他响应移除坐标 | `TestListDeliveryTrackPointsMasksCoordinatesWithoutPreciseLocationScope``TestGetDeliveryTrackPointReturnsCoordinatesWithPreciseLocationScope` | | 轨迹点资源可写,且普通列表/详情可能泄露精确经纬度 | `delivery_track_point` 改为只读契约;仅 JWT 明确声明 `location_scope=precise` 时返回精确坐标,其他响应移除坐标 | `TestListDeliveryTrackPointsMasksCoordinatesWithoutPreciseLocationScope``TestGetDeliveryTrackPointReturnsCoordinatesWithPreciseLocationScope` |
| 受保护平台路由仅验证 JWT非 root 账号可能访问未分配业务域,菜单接口也返回全部菜单 | JWT 后增加服务端角色菜单授权root 放行全部业务域,非 root 仅可访问启用角色已分配菜单对应的业务域profile 返回真实角色和菜单码,前端路由不再使用 `roles: ['*']` | `TestPlatformMenuAllowsOnlyAssignedDomain``TestListPlatformMenuReturnsOnlyMenusAssignedToNonRootRole``final-important.test.mjs` 的路由权限用例 | | 受保护平台路由仅验证 JWT非 root 账号可能访问未分配业务域,菜单接口也返回全部菜单 | JWT 后增加服务端角色菜单授权root 放行全部业务域,非 root 仅可访问启用角色已分配菜单对应的业务域profile 返回真实角色和菜单码,前端路由不再使用 `roles: ['*']` | `TestPlatformMenuAllowsOnlyAssignedDomain``TestListPlatformMenuReturnsOnlyMenusAssignedToNonRootRole``final-important.test.mjs` 的路由权限用例 |
@@ -67,7 +73,7 @@ pnpm build
| --- | --- | --- | | --- | --- | --- |
| `go test ./...` | 0 | 通过 | | `go test ./...` | 0 | 通过 |
| `go build ./cmd/main` | 0 | 通过 | | `go build ./cmd/main` | 0 | 通过 |
| `node --test frontend/platform_admin/scripts/*.test.mjs` | 0 | 通过15 个静态审计与终审回归用例;脚本按自身路径定位项目,不依赖当前工作目录 | | `node --test frontend/platform_admin/scripts/*.test.mjs` | 0 | 通过16 个静态审计与终审回归用例;脚本按自身路径定位项目,不依赖当前工作目录 |
| `pnpm lint` | 0 | 通过Biome 检查 169 个文件,无错误,保留 190 个非阻断 warning 和 12 个 info | | `pnpm lint` | 0 | 通过Biome 检查 169 个文件,无错误,保留 190 个非阻断 warning 和 12 个 info |
| `pnpm type:check` | 0 | 通过 | | `pnpm type:check` | 0 | 通过 |
| `pnpm audit:platform` | 0 | 通过 | | `pnpm audit:platform` | 0 | 通过 |