refactor: rename safe and audit backend resources

This commit is contained in:
2026-07-27 14:28:23 +08:00
parent 068b4e0ed9
commit 4e1f8a13e3
17 changed files with 193 additions and 96 deletions

View File

@@ -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.

View File

@@ -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),

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
})

View File

@@ -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" }

View File

@@ -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" }

View File

@@ -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" }

View File

@@ -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

View File

@@ -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" }

View File

@@ -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" }

View File

@@ -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" }

View File

@@ -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" }

View File

@@ -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" }

View File

@@ -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) {