From 4e1f8a13e309f760bd0179229cf4a856c83c6127 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 27 Jul 2026 14:28:23 +0800 Subject: [PATCH] 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) {