From a23fea3760a61b45a8f22e36ab5c27a42b12d9ec Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 27 Jul 2026 14:03:45 +0800 Subject: [PATCH 1/5] test: define safe and audit resource rename contract --- .../task-1-report.md | 43 +++++++++++++++++++ .../internal/logic/platform/resource_test.go | 11 ++++- backend/api/internal/routers/platform_test.go | 8 ++-- .../scripts/audit-check.test.mjs | 8 ++++ 4 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 .superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-1-report.md 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..cef9b39 --- /dev/null +++ b/.superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-1-report.md @@ -0,0 +1,43 @@ +# 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. diff --git a/backend/api/internal/logic/platform/resource_test.go b/backend/api/internal/logic/platform/resource_test.go index 05e7146..f40ae77 100644 --- a/backend/api/internal/logic/platform/resource_test.go +++ b/backend/api/internal/logic/platform/resource_test.go @@ -25,12 +25,21 @@ 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) { + 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") +} + func TestResourceDefinitionAllowsOnlySupportedMethods(t *testing.T) { if (ResourceDefinition{Mode: ReadOnly}).Allows(http.MethodPost) { t.Fatal("readonly allows POST") diff --git a/backend/api/internal/routers/platform_test.go b/backend/api/internal/routers/platform_test.go index 078edf4..41964d7 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,7 +126,7 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te } } - disposal := "/heqi/platform/v1/safety/saf_event/:identity/disposals" + disposal := "/heqi/platform/v1/safety/safe_event/:identity/disposals" assertRouteMethods(t, routes, disposal, http.MethodPost) if routes[disposal][http.MethodDelete] { t.Fatal("safety event disposals must be append-only") @@ -157,7 +157,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 +169,7 @@ 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) } func assertRouteMethods(t *testing.T, routes map[string]map[string]bool, path string, methods ...string) { diff --git a/frontend/platform_admin/scripts/audit-check.test.mjs b/frontend/platform_admin/scripts/audit-check.test.mjs index 6cdfdb2..329ac8f 100644 --- a/frontend/platform_admin/scripts/audit-check.test.mjs +++ b/frontend/platform_admin/scripts/audit-check.test.mjs @@ -1,7 +1,15 @@ 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'); + +test('资源定义使用 safe 和 audit 前缀', () => { + assert.match(resourcesSource, /define\('safe_event', '\/safety\/safe_event'/); + assert.match(resourcesSource, /define\('audit_approval', '\/audit\/audit_approval'/); +}); + test('只读页面将状态变更视为违规写操作', () => { const failures = auditPlatform({ manifest: { resources: [], routes: [] }, From d38cd42ee3522d564c5461734468c5536a76c334 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 27 Jul 2026 14:16:39 +0800 Subject: [PATCH 2/5] test: prohibit legacy safe and audit aliases --- .../task-1-report.md | 9 +++++++++ .../internal/logic/platform/resource_test.go | 7 +++++++ backend/api/internal/routers/platform_test.go | 17 ++++++++++++++++- .../platform_admin/scripts/audit-check.test.mjs | 10 ++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) 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 index cef9b39..8994290 100644 --- 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 @@ -41,3 +41,12 @@ Result: **failed as expected** (exit code 1; 6 passing, 1 failing). ## 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/backend/api/internal/logic/platform/resource_test.go b/backend/api/internal/logic/platform/resource_test.go index f40ae77..e6113c9 100644 --- a/backend/api/internal/logic/platform/resource_test.go +++ b/backend/api/internal/logic/platform/resource_test.go @@ -38,6 +38,13 @@ func TestSafeAndAuditResourceContracts(t *testing.T) { 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, "saf_")) || + (contract.Domain == "audit" && strings.HasPrefix(contract.Name, "aud_")) { + t.Fatalf("legacy resource contract %s/%s must not be registered", contract.Domain, contract.Name) + } + } } func TestResourceDefinitionAllowsOnlySupportedMethods(t *testing.T) { diff --git a/backend/api/internal/routers/platform_test.go b/backend/api/internal/routers/platform_test.go index 41964d7..da92a0c 100644 --- a/backend/api/internal/routers/platform_test.go +++ b/backend/api/internal/routers/platform_test.go @@ -127,10 +127,18 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te } disposal := "/heqi/platform/v1/safety/safe_event/:identity/disposals" - assertRouteMethods(t, routes, disposal, http.MethodPost) + assertRouteMethods(t, routes, disposal, http.MethodGet, http.MethodPost) if routes[disposal][http.MethodDelete] { t.Fatal("safety event disposals must be append-only") } + + for _, resource := range []string{"/safety/saf_rule", "/safety/saf_event", "/safety/saf_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/safety/saf_event/:identity/disposals", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete) } func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T) { @@ -170,6 +178,13 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T) } 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"} { + 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/audit/aud_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/frontend/platform_admin/scripts/audit-check.test.mjs b/frontend/platform_admin/scripts/audit-check.test.mjs index 329ac8f..544e391 100644 --- a/frontend/platform_admin/scripts/audit-check.test.mjs +++ b/frontend/platform_admin/scripts/audit-check.test.mjs @@ -6,8 +6,18 @@ import { auditPlatform, scanInternalIdLeaks } from './audit-check.mjs'; const resourcesSource = readFileSync('src/api/resources.ts', 'utf8'); test('资源定义使用 safe 和 audit 前缀', () => { + assert.match(resourcesSource, /define\('safe_rule', '\/safety\/safe_rule'/); assert.match(resourcesSource, /define\('safe_event', '\/safety\/safe_event'/); + assert.match(resourcesSource, /define\('safe_inspection', '\/safety\/safe_inspection'/); + assert.match(resourcesSource, /define\('safe_event_disposal', '\/safety\/safe_event\/:identity\/disposals'/); + assert.match(resourcesSource, /define\('audit_operation_log', '\/audit\/audit_operation_log'/); + assert.match(resourcesSource, /define\('audit_export_log', '\/audit\/audit_export_log'/); assert.match(resourcesSource, /define\('audit_approval', '\/audit\/audit_approval'/); + + assert.doesNotMatch(resourcesSource, /define\('saf_(?:rule|event|inspection|event_disposal)',/); + assert.doesNotMatch(resourcesSource, /action\('saf_event_disposal',/); + assert.doesNotMatch(resourcesSource, /define\('aud_(?:operation_log|export_log|approval)',/); + assert.doesNotMatch(resourcesSource, /\/audit\/aud_approval\/:identity\/approve/); }); test('只读页面将状态变更视为违规写操作', () => { From ee7facaa4fcef70c026ea9fa9ba882508f72e636 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 27 Jul 2026 14:28:23 +0800 Subject: [PATCH 3/5] refactor: rename safe and audit backend resources --- .../task-2-report.md | 97 +++++++++++++++++++ backend/api/internal/logic/platform/audit.go | 8 +- .../api/internal/logic/platform/audit_test.go | 26 ++--- .../internal/logic/platform/health_test.go | 2 +- .../api/internal/logic/platform/resource.go | 8 +- .../internal/logic/platform/resource_test.go | 28 +++--- .../logic/platform/task4_resources.go | 22 ++--- .../{aud_approval.go => audit_approval.go} | 8 +- ...{aud_export_log.go => audit_export_log.go} | 8 +- ...peration_log.go => audit_operation_log.go} | 8 +- backend/api/internal/models/query.go | 2 +- .../api/internal/models/saf_event_disposal.go | 15 --- .../models/{saf_event.go => safe_event.go} | 8 +- .../internal/models/safe_event_disposal.go | 15 +++ .../{saf_inspection.go => safe_inspection.go} | 8 +- .../models/{saf_rule.go => safe_rule.go} | 8 +- backend/api/internal/routers/platform.go | 18 ++-- 17 files changed, 193 insertions(+), 96 deletions(-) create mode 100644 .superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-2-report.md rename backend/api/internal/models/{aud_approval.go => audit_approval.go} (80%) rename backend/api/internal/models/{aud_export_log.go => audit_export_log.go} (76%) rename backend/api/internal/models/{aud_operation_log.go => audit_operation_log.go} (76%) delete mode 100644 backend/api/internal/models/saf_event_disposal.go rename backend/api/internal/models/{saf_event.go => safe_event.go} (82%) create mode 100644 backend/api/internal/models/safe_event_disposal.go rename backend/api/internal/models/{saf_inspection.go => safe_inspection.go} (70%) rename backend/api/internal/models/{saf_rule.go => safe_rule.go} (76%) 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/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 e6113c9..0c2dbed 100644 --- a/backend/api/internal/logic/platform/resource_test.go +++ b/backend/api/internal/logic/platform/resource_test.go @@ -250,8 +250,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) } @@ -284,8 +284,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") } }) @@ -341,7 +341,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{}}, } @@ -648,19 +648,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) @@ -815,19 +815,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) { From 74e4529a1c1a97a2655cfb21b85665ae28406265 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 27 Jul 2026 14:42:14 +0800 Subject: [PATCH 4/5] refactor: rename safe and audit frontend resources --- .../task-3-report.md | 95 +++++++++++++++++++ .../platform_admin/scripts/audit-check.mjs | 4 +- .../scripts/audit-check.test.mjs | 38 ++++---- .../scripts/final-important.test.mjs | 4 +- frontend/platform_admin/src/App.vue | 6 +- frontend/platform_admin/src/api/resources.ts | 40 ++++---- .../src/router/routes/modules/audit.ts | 6 +- .../src/router/routes/modules/safety.ts | 6 +- .../ListPage.vue | 2 +- .../ListPage.vue | 2 +- .../ListPage.vue | 2 +- .../{saf_event => safe_event}/ListPage.vue | 2 +- .../ListPage.vue | 2 +- .../{saf_rule => safe_rule}/ListPage.vue | 2 +- 14 files changed, 154 insertions(+), 57 deletions(-) create mode 100644 .superpowers/sdd/2026-07-27-safe-audit-model-prefix-rename/task-3-report.md rename frontend/platform_admin/src/views/audit/{aud_approval => audit_approval}/ListPage.vue (79%) rename frontend/platform_admin/src/views/audit/{aud_export_log => audit_export_log}/ListPage.vue (78%) rename frontend/platform_admin/src/views/audit/{aud_operation_log => audit_operation_log}/ListPage.vue (77%) rename frontend/platform_admin/src/views/safety/{saf_event => safe_event}/ListPage.vue (79%) rename frontend/platform_admin/src/views/safety/{saf_inspection => safe_inspection}/ListPage.vue (77%) rename frontend/platform_admin/src/views/safety/{saf_rule => safe_rule}/ListPage.vue (79%) 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/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 544e391..bf666f7 100644 --- a/frontend/platform_admin/scripts/audit-check.test.mjs +++ b/frontend/platform_admin/scripts/audit-check.test.mjs @@ -4,20 +4,22 @@ 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\('safe_rule', '\/safety\/safe_rule'/); - assert.match(resourcesSource, /define\('safe_event', '\/safety\/safe_event'/); - assert.match(resourcesSource, /define\('safe_inspection', '\/safety\/safe_inspection'/); - assert.match(resourcesSource, /define\('safe_event_disposal', '\/safety\/safe_event\/:identity\/disposals'/); - assert.match(resourcesSource, /define\('audit_operation_log', '\/audit\/audit_operation_log'/); - assert.match(resourcesSource, /define\('audit_export_log', '\/audit\/audit_export_log'/); - assert.match(resourcesSource, /define\('audit_approval', '\/audit\/audit_approval'/); + 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, /define\('saf_(?:rule|event|inspection|event_disposal)',/); - assert.doesNotMatch(resourcesSource, /action\('saf_event_disposal',/); - assert.doesNotMatch(resourcesSource, /define\('aud_(?:operation_log|export_log|approval)',/); - assert.doesNotMatch(resourcesSource, /\/audit\/aud_approval\/:identity\/approve/); + 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('只读页面将状态变更视为违规写操作', () => { @@ -79,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', '