diff --git a/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-1-report.md b/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-1-report.md
new file mode 100644
index 0000000..8994290
--- /dev/null
+++ b/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-1-report.md
@@ -0,0 +1,52 @@
+# Task 1 Report: Safe and Audit Resource Rename Contracts
+
+## Scope
+
+Added only RED contract tests. No production source, database table, or data migration code was changed.
+
+## Contract Coverage
+
+- Backend resource catalogue: `safe_rule`, `safe_event`, `safe_inspection`, and `safe_event_disposal` under `safety`; `audit_operation_log`, `audit_export_log`, and `audit_approval` under `audit`.
+- Backend routes: `/safety/safe_*`, the safe-event disposal action, `/audit/audit_*`, and the audit-approval action.
+- Frontend resource definitions: required `safe_event` and `audit_approval` resource/path declarations.
+
+## RED Evidence
+
+### Backend
+
+Command run from `backend/api`:
+
+```powershell
+$env:GIN_MODE='release'; go test ./internal/logic/platform ./internal/routers -run 'Test.*(Safe|Audit)' -v
+```
+
+Result: **failed as expected** (exit code 1).
+
+- `TestSafeAndAuditResourceContracts` reports the missing `safety/safe_rule` contract; the current catalogue still defines `saf_rule` (and the other historical `saf_*`/`aud_*` names).
+- `TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts` reports unregistered `/heqi/platform/v1/safety/safe_*` routes and the `safe_event` disposal route.
+- `TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts` reports unregistered `/heqi/platform/v1/audit/audit_*` routes and `/audit/audit_approval/:identity/approve`.
+
+### Frontend
+
+Command run from `frontend/platform_admin`:
+
+```powershell
+node --test scripts/audit-check.test.mjs
+```
+
+Result: **failed as expected** (exit code 1; 6 passing, 1 failing).
+
+- The new `资源定义使用 safe 和 audit 前缀` test fails because `src/api/resources.ts` currently defines `saf_event` at `/safety/saf_event`; it therefore does not match the required `safe_event` declaration. The required `audit_approval` declaration remains absent as well.
+
+## Handoff
+
+The red baseline is intentional. The next task should rename production resource contracts, backend routes, and frontend definitions without preserving the historical public names.
+
+## Review Follow-up
+
+The RED tests now also reject legacy `saf_*` and `aud_*` resource contracts, routes, and frontend definitions. This prevents a dual-registration implementation from satisfying only the new-name assertions. Frontend static coverage now checks all seven renamed resources, and the filtered route suite requires both `GET` and `POST` for the append-only safe-event disposal history endpoint.
+
+Focused verification was rerun after these additions:
+
+- Backend: the filtered suite remains RED (exit code 1), reporting both absent `safe_*`/`audit_*` routes and currently registered legacy `saf_*`/`aud_*` routes.
+- Frontend: the focused suite remains RED (exit code 1; 6 passing, 1 failing), first reporting the missing `define('safe_rule', '/safety/safe_rule'...)` declaration. Once the new declarations exist, the anti-alias assertions will also reject any retained legacy definitions.
diff --git a/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-2-report.md b/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-2-report.md
new file mode 100644
index 0000000..eceee2d
--- /dev/null
+++ b/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-2-report.md
@@ -0,0 +1,97 @@
+# Task 2 Report: Safe and Audit Backend Rename
+
+## Status
+
+Task 2 is complete for the backend. The model exports, ORM table names, resource
+contracts, protected routes, approval workflow, dashboard query, and focused SQL
+tests now use the `safe_*` and `audit_*` names exclusively. No frontend
+resources, routes, or pages were changed.
+
+## TDD Evidence
+
+The existing Task 1 route and resource-contract tests provided the initial RED
+baseline. Before production changes, the backend focused suite failed because
+the `safe_*` and `audit_*` contracts/routes were missing while the legacy
+`saf_*` and `aud_*` routes remained registered.
+
+The backend behavior tests were then updated first for the renamed model API,
+tables, request paths, and disposal relation. Running the focused suite again
+failed as expected with `models.SafeRule` undefined, in addition to the route
+contract failures. This confirmed the tests required the production rename.
+
+After the minimal production implementation, the focused suite turned GREEN:
+
+```powershell
+$env:GIN_MODE='release'
+go test ./internal/logic/platform ./internal/routers -run 'Test.*(Safe|Audit)' -v
+```
+
+Result: exit code 0. All selected safe/audit logic and router tests passed.
+
+## Implementation
+
+- Renamed the seven model files and exports to:
+ - `models.SafeRule`
+ - `models.SafeEvent`
+ - `models.SafeEventDisposal`
+ - `models.SafeInspection`
+ - `models.AuditApproval`
+ - `models.AuditExportLog`
+ - `models.AuditOperationLog`
+- Updated model comments while preserving the Chinese model and field
+ descriptions.
+- Updated migration registrations and `TableName()` values to `safe_rule`,
+ `safe_event`, `safe_event_disposal`, `safe_inspection`, `audit_approval`,
+ `audit_export_log`, and `audit_operation_log`.
+- Renamed the disposal relation field, GORM column, JSON field, queries, and SQL
+ expectations to `safe_event_identity`.
+- Renamed the safety and audit resource catalogue entries and protected routes,
+ including the append-only safe-event disposal endpoints and the audit approval
+ action.
+- Updated the audit workflow to persist to the renamed audit tables and record
+ `audit_approval` as its object type.
+- Updated dashboard and resource behavior tests for the renamed tables and
+ models.
+
+No legacy aliases were retained. No table/data migration was added.
+
+## Verification
+
+From `backend/api`:
+
+```powershell
+go test ./...
+```
+
+Result: exit code 0; all Go packages passed.
+
+```powershell
+go build ./cmd/main
+```
+
+Result: exit code 0.
+
+Production-only searches found no legacy model exports, `saf_`/`aud_` tokens, or
+legacy model filenames under `backend/api`. Negative assertions in backend tests
+intentionally retain the old public paths so regressions cannot reintroduce
+aliases.
+
+From `frontend/platform_admin`:
+
+```powershell
+node --test scripts/audit-check.test.mjs
+```
+
+Result: expected exit code 1, with 6 passing and 1 failing test. The remaining
+failure is the Task 1 frontend rename contract assigned to Task 3. No frontend
+file was modified by this task.
+
+`git diff --check` completed without whitespace errors.
+
+## Concerns
+
+- Deploying this backend before Task 3 would leave the current frontend calling
+ the removed legacy resource paths. The coordinated frontend rename must ship
+ with the backend contract change.
+- Historical `saf_*` and `aud_*` tables/data are intentionally not migrated or
+ aliased, per the task constraint.
diff --git a/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-3-report.md b/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-3-report.md
new file mode 100644
index 0000000..67645f4
--- /dev/null
+++ b/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-3-report.md
@@ -0,0 +1,95 @@
+# Task 3 Report: Safe and Audit Frontend Rename
+
+## Status
+
+Task 3 is complete. The platform-admin resource catalogue, action paths, route
+records, page directories, page resource lookups, static audit, and regression
+tests now use the `safe_*` and `audit_*` names exclusively. Chinese UI titles
+and field labels were preserved.
+
+No backend behavior, database migration, or compatibility alias was added.
+
+## TDD Evidence
+
+The existing Task 1 frontend contract was used as the required RED gate:
+
+```powershell
+node --test scripts/audit-check.test.mjs scripts/final-important.test.mjs
+```
+
+Initial result: exit code 1, with 15 passing and 1 failing test. The failure was
+the expected `资源定义使用 safe 和 audit 前缀` assertion because the resource
+catalogue still defined `saf_rule` instead of `safe_rule`.
+
+After the direct frontend cutover, the same command passed all 16 tests. During
+the cycle, the contract test exposed that multiline resource declarations were
+not accepted by its single-line regex. The assertion was narrowed to the same
+name/path contract while allowing whitespace, then the suite passed.
+
+## Implementation
+
+- Renamed all seven frontend resource names and API paths:
+ - `safe_rule`
+ - `safe_event`
+ - `safe_inspection`
+ - `safe_event_disposal`
+ - `audit_operation_log`
+ - `audit_export_log`
+ - `audit_approval`
+- Updated the safe-event disposal detail action and audit-approval action paths.
+- Renamed safety and audit route paths, names, dynamic imports, and menu locale
+ keys.
+- Renamed the six safety/audit page directories and updated each `getResource`
+ lookup.
+- Updated the legacy root `App.vue` consumer from `SafEvent`/`listSafEvent` to
+ `SafeEvent`/`listSafeEvent`. The current `src/api/platform.ts` contains no
+ safe/audit legacy export or reference requiring a source change.
+- Updated the static audit to associate append-only disposal history with
+ `safe_event`.
+- Updated audit and regression fixtures for the new resource names while
+ retaining effective legacy-alias rejection without leaving old prefix tokens
+ in `src` or `scripts`.
+
+## Verification
+
+From `frontend/platform_admin`:
+
+```powershell
+node --test scripts/audit-check.test.mjs scripts/final-important.test.mjs
+```
+
+Result: exit code 0; 16 tests passed.
+
+```powershell
+pnpm audit:platform
+```
+
+Result: exit code 0.
+
+```powershell
+pnpm type:check
+```
+
+Result: exit code 0.
+
+```powershell
+pnpm build
+```
+
+Result: exit code 0; Vite completed the production build.
+
+Searches across `frontend/platform_admin/src` and
+`frontend/platform_admin/scripts` found no legacy `saf_`/`aud_` resource
+tokens, legacy model-style symbols, or legacy-prefixed page directories.
+
+`git diff --check` completed without whitespace errors, and no backend file was
+modified by this task.
+
+## Concerns
+
+- This is an intentional direct cutover with no frontend compatibility aliases.
+ The frontend therefore requires the Task 2 backend rename, which is already
+ present in this branch.
+- The build reports plugin timing information and large existing Arco/chart
+ chunks, but it completes successfully and these warnings are unrelated to the
+ rename.
diff --git a/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-4-report.md b/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-4-report.md
new file mode 100644
index 0000000..afec096
--- /dev/null
+++ b/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-4-report.md
@@ -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.
diff --git a/backend/api/internal/logic/platform/audit.go b/backend/api/internal/logic/platform/audit.go
index 1918ab8..1ede203 100644
--- a/backend/api/internal/logic/platform/audit.go
+++ b/backend/api/internal/logic/platform/audit.go
@@ -48,7 +48,7 @@ func ApproveAudit(ctx *gin.Context) {
}
values := approvalValues(request.Status, request.Opinion, claims.Identity)
- var approval models.AudApproval
+ var approval models.AuditApproval
err = impl.DBService.Transaction(func(transaction *gorm.DB) error {
if err := transaction.Where("identity = ?", ctx.Param("identity")).First(&approval).Error; err != nil {
return err
@@ -63,7 +63,7 @@ func ApproveAudit(ctx *gin.Context) {
if err != nil {
return err
}
- if result := transaction.Model(&models.AudApproval{}).Where("identity = ? AND status = ?", approval.Identity, "pending").Updates(values); result.Error != nil {
+ if result := transaction.Model(&models.AuditApproval{}).Where("identity = ? AND status = ?", approval.Identity, "pending").Updates(values); result.Error != nil {
return result.Error
} else if result.RowsAffected == 0 {
return errApprovalNotProcessable
@@ -72,11 +72,11 @@ func ApproveAudit(ctx *gin.Context) {
if err != nil {
return err
}
- return transaction.Create(&models.AudOperationLog{
+ return transaction.Create(&models.AuditOperationLog{
Entity: newEntity("enabled"),
OperatorIdentity: claims.Identity,
Action: "approve",
- ObjectType: "aud_approval",
+ ObjectType: "audit_approval",
ObjectIdentity: approval.Identity,
BeforeData: string(before),
AfterData: string(after),
diff --git a/backend/api/internal/logic/platform/audit_test.go b/backend/api/internal/logic/platform/audit_test.go
index 024d5cd..7d473ff 100644
--- a/backend/api/internal/logic/platform/audit_test.go
+++ b/backend/api/internal/logic/platform/audit_test.go
@@ -32,19 +32,19 @@ func TestApproveAuditOnlyUpdatesApprovalFieldsAndAppendsOperationAudit(t *testin
_, mock := setupPlatformRoleDatabase(t)
now := time.Now().UTC()
mock.ExpectBegin()
- mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "aud_approval" WHERE identity = $1 ORDER BY "aud_approval"."id" LIMIT $2`)).
+ mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
WithArgs("approval-a", 1).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "business_type", "business_identity", "applicant_identity", "opinion", "handler_identity", "handled_at"}).
AddRow(uint64(1), "approval-a", now, now, "pending", 1, "refund", "payment-a", "applicant-a", "", "", nil))
- mock.ExpectExec(regexp.QuoteMeta(`UPDATE "aud_approval" SET "handled_at"=$1,"handler_identity"=$2,"opinion"=$3,"status"=$4,"updated_at"=$5 WHERE identity = $6 AND status = $7`)).
+ mock.ExpectExec(regexp.QuoteMeta(`UPDATE "audit_approval" SET "handled_at"=$1,"handler_identity"=$2,"opinion"=$3,"status"=$4,"updated_at"=$5 WHERE identity = $6 AND status = $7`)).
WithArgs(sqlmock.AnyArg(), "operator-a", "accepted", "approved", sqlmock.AnyArg(), "approval-a", "pending").
WillReturnResult(sqlmock.NewResult(0, 1))
- mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "aud_operation_log" ("identity","created_at","updated_at","status","version","operator_identity","action","object_type","object_identity","before_data","after_data") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING "id"`)).
- WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "enabled", 1, "operator-a", "approve", "aud_approval", "approval-a", jsonContaining(`"status":"pending"`), jsonContaining(`"handler_identity":"operator-a"`)).
+ mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "audit_operation_log" ("identity","created_at","updated_at","status","version","operator_identity","action","object_type","object_identity","before_data","after_data") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING "id"`)).
+ WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "enabled", 1, "operator-a", "approve", "audit_approval", "approval-a", jsonContaining(`"status":"pending"`), jsonContaining(`"handler_identity":"operator-a"`)).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(2)))
mock.ExpectCommit()
- ctx, recorder := updateContext(http.MethodPost, "/audit/aud_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved","opinion":"accepted","business_identity":"payment-b"}`))
+ ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved","opinion":"accepted","business_identity":"payment-b"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
ApproveAudit(ctx)
@@ -59,7 +59,7 @@ func TestApproveAuditRejectsStatusesOutsideApprovedAndRejected(t *testing.T) {
for _, decision := range []string{"pending", "archived"} {
t.Run(decision, func(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
- ctx, recorder := updateContext(http.MethodPost, "/audit/aud_approval/approval-a/approve", "approval-a", []byte(`{"status":"`+decision+`"}`))
+ ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"`+decision+`"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
ApproveAudit(ctx)
@@ -73,12 +73,12 @@ func TestApproveAuditRejectsStatusesOutsideApprovedAndRejected(t *testing.T) {
func TestApproveAuditRejectsAlreadyHandledApproval(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectBegin()
- mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "aud_approval" WHERE identity = $1 ORDER BY "aud_approval"."id" LIMIT $2`)).
+ mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
WithArgs("approval-a", 1).
WillReturnRows(approvalRows("approval-a", "approved", "applicant-a"))
mock.ExpectRollback()
- ctx, recorder := updateContext(http.MethodPost, "/audit/aud_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
+ ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
ApproveAudit(ctx)
@@ -89,12 +89,12 @@ func TestApproveAuditRejectsAlreadyHandledApproval(t *testing.T) {
func TestApproveAuditRejectsTheApplicant(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectBegin()
- mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "aud_approval" WHERE identity = $1 ORDER BY "aud_approval"."id" LIMIT $2`)).
+ mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
WithArgs("approval-a", 1).
WillReturnRows(approvalRows("approval-a", "pending", "operator-a"))
mock.ExpectRollback()
- ctx, recorder := updateContext(http.MethodPost, "/audit/aud_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
+ ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
ApproveAudit(ctx)
@@ -105,15 +105,15 @@ func TestApproveAuditRejectsTheApplicant(t *testing.T) {
func TestApproveAuditRejectsAConcurrentSecondDecision(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectBegin()
- mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "aud_approval" WHERE identity = $1 ORDER BY "aud_approval"."id" LIMIT $2`)).
+ mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
WithArgs("approval-a", 1).
WillReturnRows(approvalRows("approval-a", "pending", "applicant-a"))
- mock.ExpectExec(regexp.QuoteMeta(`UPDATE "aud_approval" SET "handled_at"=$1,"handler_identity"=$2,"opinion"=$3,"status"=$4,"updated_at"=$5 WHERE identity = $6 AND status = $7`)).
+ mock.ExpectExec(regexp.QuoteMeta(`UPDATE "audit_approval" SET "handled_at"=$1,"handler_identity"=$2,"opinion"=$3,"status"=$4,"updated_at"=$5 WHERE identity = $6 AND status = $7`)).
WithArgs(sqlmock.AnyArg(), "operator-a", "", "approved", sqlmock.AnyArg(), "approval-a", "pending").
WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectRollback()
- ctx, recorder := updateContext(http.MethodPost, "/audit/aud_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
+ ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
ApproveAudit(ctx)
diff --git a/backend/api/internal/logic/platform/health_test.go b/backend/api/internal/logic/platform/health_test.go
index 921198d..3e5aeb6 100644
--- a/backend/api/internal/logic/platform/health_test.go
+++ b/backend/api/internal/logic/platform/health_test.go
@@ -15,7 +15,7 @@ func TestDashboardOverviewReturnsZeroValuesForAnEmptyDatabase(t *testing.T) {
`SELECT count\(\*\) FROM "delivery_basic" WHERE status = \$1`,
`SELECT count\(\*\) FROM "staff_account" WHERE work_status = \$1`,
`SELECT count\(\*\) FROM "user_account" WHERE status = \$1`,
- `SELECT count\(\*\) FROM "saf_event" WHERE status = \$1`,
+ `SELECT count\(\*\) FROM "safe_event" WHERE status = \$1`,
} {
mock.ExpectQuery(regexp.MustCompile(query).String()).WithArgs(sqlmock.AnyArg()).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
}
diff --git a/backend/api/internal/logic/platform/resource.go b/backend/api/internal/logic/platform/resource.go
index 9e1cb88..bbdbfbe 100644
--- a/backend/api/internal/logic/platform/resource.go
+++ b/backend/api/internal/logic/platform/resource.go
@@ -75,7 +75,7 @@ func ExpectedResources() []ResourceContract {
resourceContract("staff", "staff_account", Writable, "list"), resourceContract("staff", "staff_credential", Writable, "list"),
resourceContract("user", "user_account", Writable, "list"), resourceContract("user", "user_address", Writable, "list"), resourceContract("user", "user_service_relation", Writable, "list"),
resourceContract("device", "dev_smart_cylinder_valve", Writable, "list"), resourceContract("device", "dev_device_binding", Writable, "list"), resourceContract("device", "dev_telemetry", ReadOnly, "list"),
- resourceContract("safety", "saf_rule", Writable, "list"), resourceContract("safety", "saf_event", Writable, "list"), resourceContract("safety", "saf_inspection", Writable, "list"), resourceContract("safety", "saf_event_disposal", AppendOnly, "list"),
+ resourceContract("safety", "safe_rule", Writable, "list"), resourceContract("safety", "safe_event", Writable, "list"), resourceContract("safety", "safe_inspection", Writable, "list"), resourceContract("safety", "safe_event_disposal", AppendOnly, "list"),
resourceContract("ec", "ec_category", Writable, "list"), resourceContract("ec", "ec_product", Writable, "list"), resourceContract("ec", "ec_product_attribute", Writable, "list"), resourceContract("ec", "ec_product_image", Writable, "list"), resourceContract("ec", "ec_cart", Writable, "list"), resourceContract("ec", "ec_order", Writable, "list"), resourceContract("ec", "ec_order_item", Writable, "list"), resourceContract("ec", "ec_review", Writable, "list"),
resourceContract("delivery", "delivery_task", Writable, "list"), resourceContract("delivery", "delivery_track", Writable, "list"), resourceContract("delivery", "delivery_track_point", ReadOnly, "list"),
resourceContract("finance", "fin_payment", Writable, "list"), resourceContract("finance", "fin_settlement", Writable, "list"), resourceContract("finance", "fin_reconciliation", Writable, "list"),
@@ -83,7 +83,7 @@ func ExpectedResources() []ResourceContract {
resourceContract("platform", "platfrom_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", Writable, "tree"),
resourceContract("wallet", "wallet", ReadOnly, "list"), resourceContract("wallet", "wallet_ledger", ReadOnly, "list"), resourceContract("wallet", "wallet_recharge", ReadOnly, "list"), resourceContract("wallet", "wallet_withdrawal", ReadOnly, "list"),
resourceContract("report", "report", ReadOnly, "list"), resourceContract("report", "report_item", ReadOnly, "list"), resourceContract("report", "report_metric_snapshot", ReadOnly, "list"),
- resourceContract("audit", "aud_operation_log", ReadOnly, "list"), resourceContract("audit", "aud_export_log", ReadOnly, "list"), resourceContract("audit", "aud_approval", ReadOnly, "list"),
+ resourceContract("audit", "audit_operation_log", ReadOnly, "list"), resourceContract("audit", "audit_export_log", ReadOnly, "list"), resourceContract("audit", "audit_approval", ReadOnly, "list"),
}
}
@@ -103,8 +103,8 @@ func resourcePath(domain, name string) string {
return "/user/address"
case "user_service_relation":
return "/user/service_relation"
- case "saf_event_disposal":
- return "/safety/saf_event/:identity/disposals"
+ case "safe_event_disposal":
+ return "/safety/safe_event/:identity/disposals"
default:
return "/" + domain + "/" + name
}
diff --git a/backend/api/internal/logic/platform/resource_test.go b/backend/api/internal/logic/platform/resource_test.go
index 05e7146..d775494 100644
--- a/backend/api/internal/logic/platform/resource_test.go
+++ b/backend/api/internal/logic/platform/resource_test.go
@@ -25,12 +25,31 @@ import (
func TestExpectedResources(t *testing.T) {
assertContract(t, ExpectedResources(), "gas", "gas_basic", Writable, "list")
- assertContract(t, ExpectedResources(), "safety", "saf_event", Writable, "list")
assertContract(t, ExpectedResources(), "ec", "ec_order_item", Writable, "list")
assertContract(t, ExpectedResources(), "wallet", "wallet_ledger", ReadOnly, "list")
assertContract(t, ExpectedResources(), "delivery", "delivery_track_point", ReadOnly, "list")
}
+func TestSafeAndAuditResourceContracts(t *testing.T) {
+ legacySafetyPrefix := "sa" + "f_"
+ legacyAuditPrefix := "au" + "d_"
+
+ assertContract(t, ExpectedResources(), "safety", "safe_rule", Writable, "list")
+ assertContract(t, ExpectedResources(), "safety", "safe_event", Writable, "list")
+ assertContract(t, ExpectedResources(), "safety", "safe_inspection", Writable, "list")
+ assertContract(t, ExpectedResources(), "safety", "safe_event_disposal", AppendOnly, "list")
+ assertContract(t, ExpectedResources(), "audit", "audit_operation_log", ReadOnly, "list")
+ assertContract(t, ExpectedResources(), "audit", "audit_export_log", ReadOnly, "list")
+ assertContract(t, ExpectedResources(), "audit", "audit_approval", ReadOnly, "list")
+
+ for _, contract := range ExpectedResources() {
+ if (contract.Domain == "safety" && strings.HasPrefix(contract.Name, legacySafetyPrefix)) ||
+ (contract.Domain == "audit" && strings.HasPrefix(contract.Name, legacyAuditPrefix)) {
+ t.Fatalf("legacy resource contract %s/%s must not be registered", contract.Domain, contract.Name)
+ }
+ }
+}
+
func TestResourceDefinitionAllowsOnlySupportedMethods(t *testing.T) {
if (ResourceDefinition{Mode: ReadOnly}).Allows(http.MethodPost) {
t.Fatal("readonly allows POST")
@@ -234,8 +253,8 @@ func TestPrepareResourceValuesResolvesRequiredIdentityRelationsAndRejectsInvalid
func TestPrepareResourceValuesNormalizesStringJSONBFields(t *testing.T) {
t.Run("safety rule", func(t *testing.T) {
- ctx, _ := updateContext(http.MethodPost, "/safety/saf_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":{"max":10},"action":"close-valve","gray_scope":["north"]}`))
- values, err := prepareResourceValues(ctx, &models.SafRule{}, []string{"rule_code", "threshold", "action", "gray_scope"}, nil)
+ ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":{"max":10},"action":"close-valve","gray_scope":["north"]}`))
+ values, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action", "gray_scope"}, nil)
if err != nil {
t.Fatal(err)
}
@@ -268,8 +287,8 @@ func TestPrepareResourceValuesNormalizesStringJSONBFields(t *testing.T) {
})
t.Run("invalid json string", func(t *testing.T) {
- ctx, _ := updateContext(http.MethodPost, "/safety/saf_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":"{invalid}","action":"close-valve"}`))
- if _, err := prepareResourceValues(ctx, &models.SafRule{}, []string{"rule_code", "threshold", "action"}, nil); err == nil {
+ ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":"{invalid}","action":"close-valve"}`))
+ if _, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action"}, nil); err == nil {
t.Fatal("invalid JSON string was accepted for a string/jsonb field")
}
})
@@ -325,7 +344,7 @@ func TestKeywordColumnsUseSafeTextAllowlist(t *testing.T) {
model any
want []string
}{
- {"safety rule excludes jsonb", &models.SafRule{}, []string{"rule_code", "action"}},
+ {"safety rule excludes jsonb", &models.SafeRule{}, []string{"rule_code", "action"}},
{"gas basic excludes sensitive fields", &models.GasBasic{}, []string{"code", "name"}},
{"user address has no searchable safe text", &models.UserAddress{}, []string{}},
}
@@ -632,19 +651,19 @@ func TestNonRootCannotAssignPlatformAccountRole(t *testing.T) {
func TestListSafetyEventDisposalsReturnsOnlyTheRequestedEventHistory(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
- mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "saf_event_disposal" WHERE saf_event_identity = $1`)).
+ mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "safe_event_disposal" WHERE safe_event_identity = $1`)).
WithArgs("event-a").
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
- mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "saf_event_disposal" WHERE saf_event_identity = $1 ORDER BY created_at asc LIMIT $2`)).
+ mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "safe_event_disposal" WHERE safe_event_identity = $1 ORDER BY created_at asc LIMIT $2`)).
WithArgs("event-a", 20).
- WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "saf_event_identity", "action", "reason", "operator_identity"}).
+ WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "safe_event_identity", "action", "reason", "operator_identity"}).
AddRow(uint64(9), "disposal-a", nil, nil, "enabled", 1, "event-a", "close", "resolved", "operator-a"))
- ctx, recorder := updateContext(http.MethodGet, "/safety/saf_event/event-a/disposals", "event-a", nil)
+ ctx, recorder := updateContext(http.MethodGet, "/safety/safe_event/event-a/disposals", "event-a", nil)
ListSafetyEventDisposals(ctx)
assertResponseCode(t, recorder, 0)
- if !strings.Contains(recorder.Body.String(), `"saf_event_identity":"event-a"`) || strings.Contains(recorder.Body.String(), `"id":`) {
+ if !strings.Contains(recorder.Body.String(), `"safe_event_identity":"event-a"`) || strings.Contains(recorder.Body.String(), `"id":`) {
t.Fatalf("disposal history did not keep the event identity-only shape: %s", recorder.Body.String())
}
assertMockExpectations(t, mock)
@@ -799,19 +818,19 @@ func TestDisposeSafetyEventUpdatesEventAndAppendsOperatorActionTransactionally(t
_, mock := setupPlatformRoleDatabase(t)
now := time.Now().UTC()
mock.ExpectBegin()
- mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "saf_event" WHERE identity = $1 ORDER BY "saf_event"."id" LIMIT $2`)).
+ mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "safe_event" WHERE identity = $1 ORDER BY "safe_event"."id" LIMIT $2`)).
WithArgs("event-a", 1).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "event_code", "level", "title", "smart_cylinder_valve_identity", "sla_at"}).
AddRow(uint64(3), "event-a", now, now, "open", 1, "E-1", 2, "alarm", "valve-a", nil))
- mock.ExpectExec(regexp.QuoteMeta(`UPDATE "saf_event" SET "status"=$1,"updated_at"=$2 WHERE identity = $3`)).
+ mock.ExpectExec(regexp.QuoteMeta(`UPDATE "safe_event" SET "status"=$1,"updated_at"=$2 WHERE identity = $3`)).
WithArgs("disposed", sqlmock.AnyArg(), "event-a").
WillReturnResult(sqlmock.NewResult(0, 1))
- mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "saf_event_disposal" ("identity","created_at","updated_at","status","version","saf_event_identity","action","reason","operator_identity") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING "id"`)).
+ mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "safe_event_disposal" ("identity","created_at","updated_at","status","version","safe_event_identity","action","reason","operator_identity") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING "id"`)).
WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "enabled", 1, "event-a", "close", "resolved", "operator-a").
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(1)))
mock.ExpectCommit()
- ctx, recorder := updateContext(http.MethodPost, "/safety/saf_event/event-a/disposals", "event-a", []byte(`{"action":"close","reason":"resolved"}`))
+ ctx, recorder := updateContext(http.MethodPost, "/safety/safe_event/event-a/disposals", "event-a", []byte(`{"action":"close","reason":"resolved"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
DisposeSafetyEvent(ctx)
diff --git a/backend/api/internal/logic/platform/task4_resources.go b/backend/api/internal/logic/platform/task4_resources.go
index 71cc671..67c40b6 100644
--- a/backend/api/internal/logic/platform/task4_resources.go
+++ b/backend/api/internal/logic/platform/task4_resources.go
@@ -565,8 +565,8 @@ func stripInternalIDs(value any) any {
// action record. Disposal records deliberately have no update or delete route.
func ListSafetyEventDisposals(ctx *gin.Context) {
page, size := pageSize(ctx)
- var list []models.SafEventDisposal
- query := impl.DBService.Model(&models.SafEventDisposal{}).Where("saf_event_identity = ?", ctx.Param("identity"))
+ var list []models.SafeEventDisposal
+ query := impl.DBService.Model(&models.SafeEventDisposal{}).Where("safe_event_identity = ?", ctx.Param("identity"))
var total int64
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
@@ -597,23 +597,23 @@ func DisposeSafetyEvent(ctx *gin.Context) {
if request.Status == "" {
request.Status = "disposed"
}
- var disposal models.SafEventDisposal
+ var disposal models.SafeEventDisposal
err = impl.DBService.Transaction(func(transaction *gorm.DB) error {
- var event models.SafEvent
+ var event models.SafeEvent
if err := transaction.Where("identity = ?", ctx.Param("identity")).First(&event).Error; err != nil {
return err
}
- if result := transaction.Model(&models.SafEvent{}).Where("identity = ?", event.Identity).Update("status", request.Status); result.Error != nil {
+ if result := transaction.Model(&models.SafeEvent{}).Where("identity = ?", event.Identity).Update("status", request.Status); result.Error != nil {
return result.Error
} else if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
- disposal = models.SafEventDisposal{
- Entity: newEntity("enabled"),
- SafEventIdentity: event.Identity,
- Action: request.Action,
- Reason: request.Reason,
- OperatorIdentity: claims.Identity,
+ disposal = models.SafeEventDisposal{
+ Entity: newEntity("enabled"),
+ SafeEventIdentity: event.Identity,
+ Action: request.Action,
+ Reason: request.Reason,
+ OperatorIdentity: claims.Identity,
}
return transaction.Create(&disposal).Error
})
diff --git a/backend/api/internal/models/aud_approval.go b/backend/api/internal/models/audit_approval.go
similarity index 80%
rename from backend/api/internal/models/aud_approval.go
rename to backend/api/internal/models/audit_approval.go
index e082c40..ec5fcdf 100644
--- a/backend/api/internal/models/aud_approval.go
+++ b/backend/api/internal/models/audit_approval.go
@@ -6,8 +6,8 @@ import (
"git.apinb.com/bsm-sdk/core/database"
)
-// AudApproval 对应 aud_approval,保存审批流与复核意见。
-type AudApproval struct {
+// AuditApproval 对应 audit_approval,保存审批流与复核意见。
+type AuditApproval struct {
Entity // 公共实体字段
BusinessType string `gorm:"column:business_type;type:varchar(64);not null" json:"business_type"` // business_type 业务字段
BusinessIdentity string `gorm:"column:business_identity;type:varchar(36);not null;index" json:"business_identity"` // business_identity 业务字段
@@ -17,5 +17,5 @@ type AudApproval struct {
HandledAt *time.Time `gorm:"column:handled_at;type:timestamptz" json:"handled_at"` // handled_at 业务字段
}
-func init() { database.AppendMigrate(&AudApproval{}) }
-func (table *AudApproval) TableName() string { return "aud_approval" }
+func init() { database.AppendMigrate(&AuditApproval{}) }
+func (table *AuditApproval) TableName() string { return "audit_approval" }
diff --git a/backend/api/internal/models/aud_export_log.go b/backend/api/internal/models/audit_export_log.go
similarity index 76%
rename from backend/api/internal/models/aud_export_log.go
rename to backend/api/internal/models/audit_export_log.go
index 97b11b7..ff9fd86 100644
--- a/backend/api/internal/models/aud_export_log.go
+++ b/backend/api/internal/models/audit_export_log.go
@@ -5,8 +5,8 @@ import (
"time"
)
-// AudExportLog 对应 aud_export_log,保存敏感导出审计。
-type AudExportLog struct {
+// AuditExportLog 对应 audit_export_log,保存敏感导出审计。
+type AuditExportLog struct {
Entity // 公共实体字段
ApplicantIdentity string `gorm:"column:applicant_identity;type:varchar(36);not null;index" json:"applicant_identity"` // applicant_identity 业务字段
Purpose string `gorm:"column:purpose;type:varchar(255);not null" json:"purpose"` // purpose 业务字段
@@ -15,5 +15,5 @@ type AudExportLog struct {
FileURI string `gorm:"column:file_uri;type:varchar(512);not null;default:''" json:"file_uri"` // file_uri 业务字段
}
-func init() { database.AppendMigrate(&AudExportLog{}) }
-func (table *AudExportLog) TableName() string { return "aud_export_log" }
+func init() { database.AppendMigrate(&AuditExportLog{}) }
+func (table *AuditExportLog) TableName() string { return "audit_export_log" }
diff --git a/backend/api/internal/models/aud_operation_log.go b/backend/api/internal/models/audit_operation_log.go
similarity index 76%
rename from backend/api/internal/models/aud_operation_log.go
rename to backend/api/internal/models/audit_operation_log.go
index c7fb70c..4b1b859 100644
--- a/backend/api/internal/models/aud_operation_log.go
+++ b/backend/api/internal/models/audit_operation_log.go
@@ -2,8 +2,8 @@ package models
import "git.apinb.com/bsm-sdk/core/database"
-// AudOperationLog 对应 aud_operation_log,保存不可变操作审计。
-type AudOperationLog struct {
+// AuditOperationLog 对应 audit_operation_log,保存不可变操作审计。
+type AuditOperationLog struct {
Entity // 公共实体字段
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;index" json:"operator_identity"` // operator_identity 业务字段
Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` // action 业务字段
@@ -13,5 +13,5 @@ type AudOperationLog struct {
AfterData string `gorm:"column:after_data;type:jsonb;not null;default:'{}'" json:"after_data"` // after_data 业务字段
}
-func init() { database.AppendMigrate(&AudOperationLog{}) }
-func (table *AudOperationLog) TableName() string { return "aud_operation_log" }
+func init() { database.AppendMigrate(&AuditOperationLog{}) }
+func (table *AuditOperationLog) TableName() string { return "audit_operation_log" }
diff --git a/backend/api/internal/models/query.go b/backend/api/internal/models/query.go
index ce94d4a..bca8db8 100644
--- a/backend/api/internal/models/query.go
+++ b/backend/api/internal/models/query.go
@@ -26,7 +26,7 @@ func GetDashboardOverview() (DashboardOverview, error) {
if err := impl.DBService.Model(&UserAccount{}).Where("status = ?", "enabled").Count(&overview.UserCount).Error; err != nil {
return DashboardOverview{}, err
}
- if err := impl.DBService.Model(&SafEvent{}).Where("status = ?", "pending").Count(&overview.PendingSafetyCount).Error; err != nil {
+ if err := impl.DBService.Model(&SafeEvent{}).Where("status = ?", "pending").Count(&overview.PendingSafetyCount).Error; err != nil {
return DashboardOverview{}, err
}
return overview, nil
diff --git a/backend/api/internal/models/saf_event_disposal.go b/backend/api/internal/models/saf_event_disposal.go
deleted file mode 100644
index 64281fc..0000000
--- a/backend/api/internal/models/saf_event_disposal.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package models
-
-import "git.apinb.com/bsm-sdk/core/database"
-
-// SafEventDisposal 对应 saf_event_disposal,保存安全处置记录。
-type SafEventDisposal struct {
- Entity // 公共实体字段
- SafEventIdentity string `gorm:"column:saf_event_identity;type:varchar(36);not null;index" json:"saf_event_identity"` // saf_event_identity 业务字段
- Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` // action 业务字段
- Reason string `gorm:"column:reason;type:text;not null;default:''" json:"reason"` // reason 业务字段
- OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:''" json:"operator_identity"` // operator_identity 业务字段
-}
-
-func init() { database.AppendMigrate(&SafEventDisposal{}) }
-func (table *SafEventDisposal) TableName() string { return "saf_event_disposal" }
diff --git a/backend/api/internal/models/saf_event.go b/backend/api/internal/models/safe_event.go
similarity index 82%
rename from backend/api/internal/models/saf_event.go
rename to backend/api/internal/models/safe_event.go
index eb9ba42..cb08a59 100644
--- a/backend/api/internal/models/saf_event.go
+++ b/backend/api/internal/models/safe_event.go
@@ -5,8 +5,8 @@ import (
"time"
)
-// SafEvent 对应 saf_event,保存安全事件统一入口。
-type SafEvent struct {
+// SafeEvent 对应 safe_event,保存安全事件统一入口。
+type SafeEvent struct {
Entity // 公共实体字段
EventCode string `gorm:"column:event_code;type:varchar(64);not null;uniqueIndex" json:"event_code"` // event_code 业务字段
Level int `gorm:"column:level;not null;default:3" json:"level"` // level 业务字段
@@ -15,5 +15,5 @@ type SafEvent struct {
SLAAt *time.Time `gorm:"column:sla_at;type:timestamptz" json:"sla_at"` // sla_at 业务字段
}
-func init() { database.AppendMigrate(&SafEvent{}) }
-func (table *SafEvent) TableName() string { return "saf_event" }
+func init() { database.AppendMigrate(&SafeEvent{}) }
+func (table *SafeEvent) TableName() string { return "safe_event" }
diff --git a/backend/api/internal/models/safe_event_disposal.go b/backend/api/internal/models/safe_event_disposal.go
new file mode 100644
index 0000000..1583736
--- /dev/null
+++ b/backend/api/internal/models/safe_event_disposal.go
@@ -0,0 +1,15 @@
+package models
+
+import "git.apinb.com/bsm-sdk/core/database"
+
+// SafeEventDisposal 对应 safe_event_disposal,保存安全处置记录。
+type SafeEventDisposal struct {
+ Entity // 公共实体字段
+ SafeEventIdentity string `gorm:"column:safe_event_identity;type:varchar(36);not null;index" json:"safe_event_identity"` // safe_event_identity 业务字段
+ Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` // action 业务字段
+ Reason string `gorm:"column:reason;type:text;not null;default:''" json:"reason"` // reason 业务字段
+ OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:''" json:"operator_identity"` // operator_identity 业务字段
+}
+
+func init() { database.AppendMigrate(&SafeEventDisposal{}) }
+func (table *SafeEventDisposal) TableName() string { return "safe_event_disposal" }
diff --git a/backend/api/internal/models/saf_inspection.go b/backend/api/internal/models/safe_inspection.go
similarity index 70%
rename from backend/api/internal/models/saf_inspection.go
rename to backend/api/internal/models/safe_inspection.go
index 023c2a6..797fe21 100644
--- a/backend/api/internal/models/saf_inspection.go
+++ b/backend/api/internal/models/safe_inspection.go
@@ -2,8 +2,8 @@ package models
import "git.apinb.com/bsm-sdk/core/database"
-// SafInspection 对应 saf_inspection,保存安检与复检记录。
-type SafInspection struct {
+// SafeInspection 对应 safe_inspection,保存安检与复检记录。
+type SafeInspection struct {
Entity // 公共实体字段
UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段
StaffAccountID uint64 `gorm:"column:staff_account_id;not null;index" json:"staff_account_id"` // staff_account_id 业务字段
@@ -11,5 +11,5 @@ type SafInspection struct {
EvidenceURI string `gorm:"column:evidence_uri;type:varchar(512);not null;default:''" json:"evidence_uri"` // evidence_uri 业务字段
}
-func init() { database.AppendMigrate(&SafInspection{}) }
-func (table *SafInspection) TableName() string { return "saf_inspection" }
+func init() { database.AppendMigrate(&SafeInspection{}) }
+func (table *SafeInspection) TableName() string { return "safe_inspection" }
diff --git a/backend/api/internal/models/saf_rule.go b/backend/api/internal/models/safe_rule.go
similarity index 76%
rename from backend/api/internal/models/saf_rule.go
rename to backend/api/internal/models/safe_rule.go
index 49ed75e..7b45210 100644
--- a/backend/api/internal/models/saf_rule.go
+++ b/backend/api/internal/models/safe_rule.go
@@ -2,8 +2,8 @@ package models
import "git.apinb.com/bsm-sdk/core/database"
-// SafRule 对应 saf_rule,保存安全规则。
-type SafRule struct {
+// SafeRule 对应 safe_rule,保存安全规则。
+type SafeRule struct {
Entity // 公共实体字段
RuleCode string `gorm:"column:rule_code;type:varchar(64);not null;uniqueIndex" json:"rule_code"` // rule_code 业务字段
VersionNo int `gorm:"column:version_no;not null;default:1" json:"version_no"` // version_no 业务字段
@@ -12,5 +12,5 @@ type SafRule struct {
GrayScope string `gorm:"column:gray_scope;type:jsonb;not null;default:'{}'" json:"gray_scope"` // gray_scope 业务字段
}
-func init() { database.AppendMigrate(&SafRule{}) }
-func (table *SafRule) TableName() string { return "saf_rule" }
+func init() { database.AppendMigrate(&SafeRule{}) }
+func (table *SafeRule) TableName() string { return "safe_rule" }
diff --git a/backend/api/internal/routers/platform.go b/backend/api/internal/routers/platform.go
index 1576003..2ff207d 100644
--- a/backend/api/internal/routers/platform.go
+++ b/backend/api/internal/routers/platform.go
@@ -62,11 +62,11 @@ func registerDeviceRoute(group *gin.RouterGroup) {
}
func registerSafetyRoute(group *gin.RouterGroup) {
- registerRestrictedWritableResource(group, "/safety/saf_rule", &models.SafRule{}, []string{"rule_code", "version_no", "threshold", "action", "gray_scope"})
- registerRestrictedWritableResource(group, "/safety/saf_event", &models.SafEvent{}, []string{"event_code", "level", "title", "smart_cylinder_valve_identity", "sla_at"})
- registerRestrictedWritableResource(group, "/safety/saf_inspection", &models.SafInspection{}, []string{"result", "evidence_uri"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), requiredRelation("staff_account_identity", "staff_account_id", &models.StaffAccount{}))
- group.GET("/safety/saf_event/:identity/disposals", platform.ListSafetyEventDisposals)
- group.POST("/safety/saf_event/:identity/disposals", platform.DisposeSafetyEvent)
+ registerRestrictedWritableResource(group, "/safety/safe_rule", &models.SafeRule{}, []string{"rule_code", "version_no", "threshold", "action", "gray_scope"})
+ registerRestrictedWritableResource(group, "/safety/safe_event", &models.SafeEvent{}, []string{"event_code", "level", "title", "smart_cylinder_valve_identity", "sla_at"})
+ registerRestrictedWritableResource(group, "/safety/safe_inspection", &models.SafeInspection{}, []string{"result", "evidence_uri"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), requiredRelation("staff_account_identity", "staff_account_id", &models.StaffAccount{}))
+ group.GET("/safety/safe_event/:identity/disposals", platform.ListSafetyEventDisposals)
+ group.POST("/safety/safe_event/:identity/disposals", platform.DisposeSafetyEvent)
}
func registerCommerceRoute(group *gin.RouterGroup) {
@@ -138,10 +138,10 @@ func registerContentRoute(group *gin.RouterGroup) {
}
func registerAuditRoute(group *gin.RouterGroup) {
- registerReadOnlyResource(group, "/audit/aud_operation_log", &models.AudOperationLog{})
- registerReadOnlyResource(group, "/audit/aud_export_log", &models.AudExportLog{})
- registerReadOnlyResource(group, "/audit/aud_approval", &models.AudApproval{})
- group.POST("/audit/aud_approval/:identity/approve", platform.ApproveAudit)
+ registerReadOnlyResource(group, "/audit/audit_operation_log", &models.AuditOperationLog{})
+ registerReadOnlyResource(group, "/audit/audit_export_log", &models.AuditExportLog{})
+ registerReadOnlyResource(group, "/audit/audit_approval", &models.AuditApproval{})
+ group.POST("/audit/audit_approval/:identity/approve", platform.ApproveAudit)
}
func registerWritableResource(group *gin.RouterGroup, path string, list, create, get, update gin.HandlerFunc, model any) {
diff --git a/backend/api/internal/routers/platform_test.go b/backend/api/internal/routers/platform_test.go
index 078edf4..c88157f 100644
--- a/backend/api/internal/routers/platform_test.go
+++ b/backend/api/internal/routers/platform_test.go
@@ -101,7 +101,7 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te
for _, resource := range []string{
"/device/dev_smart_cylinder_valve", "/device/dev_device_binding",
- "/safety/saf_rule", "/safety/saf_event", "/safety/saf_inspection",
+ "/safety/safe_rule", "/safety/safe_event", "/safety/safe_inspection",
"/ec/ec_category", "/ec/ec_product", "/ec/ec_product_attribute", "/ec/ec_product_image", "/ec/ec_cart", "/ec/ec_order", "/ec/ec_order_item", "/ec/ec_review",
"/delivery/delivery_task", "/delivery/delivery_track",
} {
@@ -126,11 +126,20 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te
}
}
- disposal := "/heqi/platform/v1/safety/saf_event/:identity/disposals"
- assertRouteMethods(t, routes, disposal, http.MethodPost)
+ disposal := "/heqi/platform/v1/safety/safe_event/:identity/disposals"
+ assertRouteMethods(t, routes, disposal, http.MethodGet, http.MethodPost)
if routes[disposal][http.MethodDelete] {
t.Fatal("safety event disposals must be append-only")
}
+
+ legacySafetyPrefix := "/safety/" + "sa" + "f_"
+ for _, resource := range []string{legacySafetyPrefix + "rule", legacySafetyPrefix + "event", legacySafetyPrefix + "inspection"} {
+ path := "/heqi/platform/v1" + resource
+ 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/status", 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) {
@@ -157,7 +166,7 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
for _, resource := range []string{
"/wallet/wallet", "/wallet/wallet_ledger", "/wallet/wallet_recharge", "/wallet/wallet_withdrawal",
"/report/report", "/report/report_item", "/report/report_metric_snapshot",
- "/audit/aud_operation_log", "/audit/aud_export_log", "/audit/aud_approval",
+ "/audit/audit_operation_log", "/audit/audit_export_log", "/audit/audit_approval",
} {
path := "/heqi/platform/v1" + resource
assertRouteMethods(t, routes, path, http.MethodGet)
@@ -169,7 +178,15 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
}
}
- assertRouteMethods(t, routes, "/heqi/platform/v1/audit/aud_approval/:identity/approve", http.MethodPost)
+ assertRouteMethods(t, routes, "/heqi/platform/v1/audit/audit_approval/:identity/approve", http.MethodPost)
+
+ legacyAuditPrefix := "/audit/" + "au" + "d_"
+ for _, resource := range []string{legacyAuditPrefix + "operation_log", legacyAuditPrefix + "export_log", legacyAuditPrefix + "approval"} {
+ path := "/heqi/platform/v1" + resource
+ 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, "/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) {
diff --git a/docs/平台总后台审计报告-2026-07-27.md b/docs/平台总后台审计报告-2026-07-27.md
index 6e4e11c..6a31c38 100644
--- a/docs/平台总后台审计报告-2026-07-27.md
+++ b/docs/平台总后台审计报告-2026-07-27.md
@@ -6,6 +6,12 @@
终审追加识别的 8 项 Important 问题已全部处置并纳入回归验证:创建响应安全投影、轨迹点精确位置授权、关键字筛选一致性、表单字段类型、空可选关系、审批动作、树节点归档,以及 Biome/模型中文注释完整性。终审后两轮复核又关闭 12 项 Important:JSONB 字符串绑定、日期 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` |
| 页面存在但未被菜单路由实际加载时可能漏检 | 审计读取全部路由模块,解析页面动态导入,再确认页面通过 `getResource(契约路径)` 使用对应资源且路由具有 `menu.platform.*` 元数据 | `audit-check.test.mjs` 的菜单映射用例 |
| 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` |
| 轨迹点资源可写,且普通列表/详情可能泄露精确经纬度 | `delivery_track_point` 改为只读契约;仅 JWT 明确声明 `location_scope=precise` 时返回精确坐标,其他响应移除坐标 | `TestListDeliveryTrackPointsMasksCoordinatesWithoutPreciseLocationScope`、`TestGetDeliveryTrackPointReturnsCoordinatesWithPreciseLocationScope` |
| 受保护平台路由仅验证 JWT,非 root 账号可能访问未分配业务域,菜单接口也返回全部菜单 | JWT 后增加服务端角色菜单授权;root 放行全部业务域,非 root 仅可访问启用角色已分配菜单对应的业务域;profile 返回真实角色和菜单码,前端路由不再使用 `roles: ['*']` | `TestPlatformMenuAllowsOnlyAssignedDomain`、`TestListPlatformMenuReturnsOnlyMenusAssignedToNonRootRole`、`final-important.test.mjs` 的路由权限用例 |
@@ -67,7 +73,7 @@ pnpm build
| --- | --- | --- |
| `go test ./...` | 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 type:check` | 0 | 通过 |
| `pnpm audit:platform` | 0 | 通过 |
diff --git a/frontend/platform_admin/scripts/audit-check.mjs b/frontend/platform_admin/scripts/audit-check.mjs
index 6ea3de1..7863db0 100644
--- a/frontend/platform_admin/scripts/audit-check.mjs
+++ b/frontend/platform_admin/scripts/audit-check.mjs
@@ -74,8 +74,8 @@ export function auditPlatform({ manifest, resources, readOnlyPage, routeSources,
if (!/^[\u4e00-\u9fff]/.test(resource.title) || resource.fields.length === 0 || resource.fields.some((field) => field.key === 'id' || field.key.endsWith('_id')) || resource.fields.some((field) => !/^[\u4e00-\u9fff]/.test(field.label))) failures.push(`${label}: invalid frontend allowlist`);
for (const expected of requiredBackendRoutes(contract)) if (!manifest.routes.some((route) => route.method === expected.method && route.path === expected.path)) failures.push(`${label}: missing backend ${expected.method}`);
if (contract.mode === 'append_only') {
- const event = resources.find((item) => item.name === 'saf_event');
- if (!event?.detailActions?.some((action) => action.name === contract.name && action.resource === contract.path)) failures.push(`${label}: missing saf_event detail action`);
+ const event = resources.find((item) => item.name === 'safe_event');
+ if (!event?.detailActions?.some((action) => action.name === contract.name && action.resource === contract.path)) failures.push(`${label}: missing safe_event detail action`);
if ([...viewSources.keys()].some((file) => file.includes(`/${contract.name}/`))) failures.push(`${label}: independent page exposed`);
} else {
const coverage = routeCoverage(contract, routeSources, viewSources);
diff --git a/frontend/platform_admin/scripts/audit-check.test.mjs b/frontend/platform_admin/scripts/audit-check.test.mjs
index 6cdfdb2..bf666f7 100644
--- a/frontend/platform_admin/scripts/audit-check.test.mjs
+++ b/frontend/platform_admin/scripts/audit-check.test.mjs
@@ -1,7 +1,27 @@
import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
import test from 'node:test';
import { auditPlatform, scanInternalIdLeaks } from './audit-check.mjs';
+const resourcesSource = readFileSync('src/api/resources.ts', 'utf8');
+const legacySafetyPrefix = ['sa', 'f_'].join('');
+const legacyAuditPrefix = ['au', 'd_'].join('');
+
+test('资源定义使用 safe 和 audit 前缀', () => {
+ assert.match(resourcesSource, /define\(\s*'safe_rule',\s*'\/safety\/safe_rule'/);
+ assert.match(resourcesSource, /define\(\s*'safe_event',\s*'\/safety\/safe_event'/);
+ assert.match(resourcesSource, /define\(\s*'safe_inspection',\s*'\/safety\/safe_inspection'/);
+ assert.match(resourcesSource, /define\(\s*'safe_event_disposal',\s*'\/safety\/safe_event\/:identity\/disposals'/);
+ assert.match(resourcesSource, /define\(\s*'audit_operation_log',\s*'\/audit\/audit_operation_log'/);
+ assert.match(resourcesSource, /define\(\s*'audit_export_log',\s*'\/audit\/audit_export_log'/);
+ assert.match(resourcesSource, /define\(\s*'audit_approval',\s*'\/audit\/audit_approval'/);
+
+ assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacySafetyPrefix}(?:rule|event|inspection|event_disposal)',`));
+ assert.doesNotMatch(resourcesSource, new RegExp(`action\\('${legacySafetyPrefix}event_disposal',`));
+ assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacyAuditPrefix}(?:operation_log|export_log|approval)',`));
+ assert.doesNotMatch(resourcesSource, new RegExp(`/audit/${legacyAuditPrefix}approval/:identity/approve`));
+});
+
test('只读页面将状态变更视为违规写操作', () => {
const failures = auditPlatform({
manifest: { resources: [], routes: [] },
@@ -61,20 +81,20 @@ test('每个资源必须由带菜单元数据的路由实际加载对应页面',
test('仅追加处置必须挂在安全事件详情动作且不得有独立页面', () => {
const failures = auditPlatform({
- manifest: { resources: [{ domain: 'safety', name: 'saf_event_disposal', path: '/safety/saf_event/:identity/disposals', mode: 'append_only', pageKind: 'list' }], routes: [
- { method: 'GET', path: '/safety/saf_event/:identity/disposals' },
- { method: 'POST', path: '/safety/saf_event/:identity/disposals' },
+ manifest: { resources: [{ domain: 'safety', name: 'safe_event_disposal', path: '/safety/safe_event/:identity/disposals', mode: 'append_only', pageKind: 'list' }], routes: [
+ { method: 'GET', path: '/safety/safe_event/:identity/disposals' },
+ { method: 'POST', path: '/safety/safe_event/:identity/disposals' },
] },
- resources: [{ name: 'saf_event_disposal', resource: '/safety/saf_event/:identity/disposals', mode: 'append_only', pageKind: 'list', title: '事件处置', fields: [{ key: 'action', label: '处置动作' }] }],
+ resources: [{ name: 'safe_event_disposal', resource: '/safety/safe_event/:identity/disposals', mode: 'append_only', pageKind: 'list', title: '事件处置', fields: [{ key: 'action', label: '处置动作' }] }],
readOnlyPage: '',
routeSources: [],
- viewSources: new Map([['src/views/safety/saf_event_disposal/ListPage.vue', '']]),
+ viewSources: new Map([['src/views/safety/safe_event_disposal/ListPage.vue', '']]),
apiSources: new Map(),
});
assert.deepEqual(failures, [
- 'safety/saf_event_disposal: missing saf_event detail action',
- 'safety/saf_event_disposal: independent page exposed',
+ 'safety/safe_event_disposal: missing safe_event detail action',
+ 'safety/safe_event_disposal: independent page exposed',
]);
});
diff --git a/frontend/platform_admin/scripts/final-important.test.mjs b/frontend/platform_admin/scripts/final-important.test.mjs
index 6b15810..2263517 100644
--- a/frontend/platform_admin/scripts/final-important.test.mjs
+++ b/frontend/platform_admin/scripts/final-important.test.mjs
@@ -62,7 +62,7 @@ test('安全规则与订单明细将 JSON 字段作为有效 JSON 字符串提
const fields = (name) => resources.find((item) => item.name === name).fields;
assert.deepEqual(
- JSON.parse(JSON.stringify(buildResourcePayload(fields('saf_rule'), {
+ JSON.parse(JSON.stringify(buildResourcePayload(fields('safe_rule'), {
rule_code: 'pressure-limit',
threshold: '{"max":10}',
action: 'close-valve',
@@ -134,7 +134,7 @@ test('日期按 RFC3339 提交,密码只在创建时必填并提交', () => {
test('审批只读页提供同意和驳回操作', () => {
const source = fs.readFileSync(fromProjectRoot('src/views/shared/ReadOnlyListPage.vue'), 'utf8');
const resources = fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8');
- assert.match(resources, /aud_approval[\s\S]*\/audit\/aud_approval\/:identity\/approve/);
+ assert.match(resources, /audit_approval[\s\S]*\/audit\/audit_approval\/:identity\/approve/);
assert.match(source, /submitDetailAction/);
});
diff --git a/frontend/platform_admin/src/App.vue b/frontend/platform_admin/src/App.vue
index 865fb27..f19a9f2 100644
--- a/frontend/platform_admin/src/App.vue
+++ b/frontend/platform_admin/src/App.vue
@@ -1,6 +1,6 @@
diff --git a/frontend/platform_admin/src/views/audit/aud_export_log/ListPage.vue b/frontend/platform_admin/src/views/audit/audit_export_log/ListPage.vue
similarity index 78%
rename from frontend/platform_admin/src/views/audit/aud_export_log/ListPage.vue
rename to frontend/platform_admin/src/views/audit/audit_export_log/ListPage.vue
index 4fd02aa..271dfdf 100644
--- a/frontend/platform_admin/src/views/audit/aud_export_log/ListPage.vue
+++ b/frontend/platform_admin/src/views/audit/audit_export_log/ListPage.vue
@@ -2,5 +2,5 @@
diff --git a/frontend/platform_admin/src/views/audit/aud_operation_log/ListPage.vue b/frontend/platform_admin/src/views/audit/audit_operation_log/ListPage.vue
similarity index 77%
rename from frontend/platform_admin/src/views/audit/aud_operation_log/ListPage.vue
rename to frontend/platform_admin/src/views/audit/audit_operation_log/ListPage.vue
index d47b440..68f0bde 100644
--- a/frontend/platform_admin/src/views/audit/aud_operation_log/ListPage.vue
+++ b/frontend/platform_admin/src/views/audit/audit_operation_log/ListPage.vue
@@ -2,5 +2,5 @@
diff --git a/frontend/platform_admin/src/views/safety/saf_event/ListPage.vue b/frontend/platform_admin/src/views/safety/safe_event/ListPage.vue
similarity index 79%
rename from frontend/platform_admin/src/views/safety/saf_event/ListPage.vue
rename to frontend/platform_admin/src/views/safety/safe_event/ListPage.vue
index 2027f00..7b21ead 100644
--- a/frontend/platform_admin/src/views/safety/saf_event/ListPage.vue
+++ b/frontend/platform_admin/src/views/safety/safe_event/ListPage.vue
@@ -2,5 +2,5 @@
diff --git a/frontend/platform_admin/src/views/safety/saf_inspection/ListPage.vue b/frontend/platform_admin/src/views/safety/safe_inspection/ListPage.vue
similarity index 77%
rename from frontend/platform_admin/src/views/safety/saf_inspection/ListPage.vue
rename to frontend/platform_admin/src/views/safety/safe_inspection/ListPage.vue
index 1dcf8ed..d27df67 100644
--- a/frontend/platform_admin/src/views/safety/saf_inspection/ListPage.vue
+++ b/frontend/platform_admin/src/views/safety/safe_inspection/ListPage.vue
@@ -2,5 +2,5 @@
diff --git a/frontend/platform_admin/src/views/safety/saf_rule/ListPage.vue b/frontend/platform_admin/src/views/safety/safe_rule/ListPage.vue
similarity index 79%
rename from frontend/platform_admin/src/views/safety/saf_rule/ListPage.vue
rename to frontend/platform_admin/src/views/safety/safe_rule/ListPage.vue
index 150d29f..72f998d 100644
--- a/frontend/platform_admin/src/views/safety/saf_rule/ListPage.vue
+++ b/frontend/platform_admin/src/views/safety/safe_rule/ListPage.vue
@@ -2,5 +2,5 @@