Merge branch 'codex/safe-audit-prefix-rename'

This commit is contained in:
2026-07-27 15:03:13 +08:00
35 changed files with 541 additions and 150 deletions

View File

@@ -0,0 +1,52 @@
# Task 1 Report: Safe and Audit Resource Rename Contracts
## Scope
Added only RED contract tests. No production source, database table, or data migration code was changed.
## Contract Coverage
- Backend resource catalogue: `safe_rule`, `safe_event`, `safe_inspection`, and `safe_event_disposal` under `safety`; `audit_operation_log`, `audit_export_log`, and `audit_approval` under `audit`.
- Backend routes: `/safety/safe_*`, the safe-event disposal action, `/audit/audit_*`, and the audit-approval action.
- Frontend resource definitions: required `safe_event` and `audit_approval` resource/path declarations.
## RED Evidence
### Backend
Command run from `backend/api`:
```powershell
$env:GIN_MODE='release'; go test ./internal/logic/platform ./internal/routers -run 'Test.*(Safe|Audit)' -v
```
Result: **failed as expected** (exit code 1).
- `TestSafeAndAuditResourceContracts` reports the missing `safety/safe_rule` contract; the current catalogue still defines `saf_rule` (and the other historical `saf_*`/`aud_*` names).
- `TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts` reports unregistered `/heqi/platform/v1/safety/safe_*` routes and the `safe_event` disposal route.
- `TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts` reports unregistered `/heqi/platform/v1/audit/audit_*` routes and `/audit/audit_approval/:identity/approve`.
### Frontend
Command run from `frontend/platform_admin`:
```powershell
node --test scripts/audit-check.test.mjs
```
Result: **failed as expected** (exit code 1; 6 passing, 1 failing).
- The new `资源定义使用 safe 和 audit 前缀` test fails because `src/api/resources.ts` currently defines `saf_event` at `/safety/saf_event`; it therefore does not match the required `safe_event` declaration. The required `audit_approval` declaration remains absent as well.
## Handoff
The red baseline is intentional. The next task should rename production resource contracts, backend routes, and frontend definitions without preserving the historical public names.
## Review Follow-up
The RED tests now also reject legacy `saf_*` and `aud_*` resource contracts, routes, and frontend definitions. This prevents a dual-registration implementation from satisfying only the new-name assertions. Frontend static coverage now checks all seven renamed resources, and the filtered route suite requires both `GET` and `POST` for the append-only safe-event disposal history endpoint.
Focused verification was rerun after these additions:
- Backend: the filtered suite remains RED (exit code 1), reporting both absent `safe_*`/`audit_*` routes and currently registered legacy `saf_*`/`aud_*` routes.
- Frontend: the focused suite remains RED (exit code 1; 6 passing, 1 failing), first reporting the missing `define('safe_rule', '/safety/safe_rule'...)` declaration. Once the new declarations exist, the anti-alias assertions will also reject any retained legacy definitions.

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

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

View File

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

View File

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

@@ -25,12 +25,31 @@ import (
func TestExpectedResources(t *testing.T) {
assertContract(t, ExpectedResources(), "gas", "gas_basic", Writable, "list")
assertContract(t, ExpectedResources(), "safety", "saf_event", Writable, "list")
assertContract(t, ExpectedResources(), "ec", "ec_order_item", Writable, "list")
assertContract(t, ExpectedResources(), "wallet", "wallet_ledger", ReadOnly, "list")
assertContract(t, ExpectedResources(), "delivery", "delivery_track_point", ReadOnly, "list")
}
func TestSafeAndAuditResourceContracts(t *testing.T) {
legacySafetyPrefix := "sa" + "f_"
legacyAuditPrefix := "au" + "d_"
assertContract(t, ExpectedResources(), "safety", "safe_rule", Writable, "list")
assertContract(t, ExpectedResources(), "safety", "safe_event", Writable, "list")
assertContract(t, ExpectedResources(), "safety", "safe_inspection", Writable, "list")
assertContract(t, ExpectedResources(), "safety", "safe_event_disposal", AppendOnly, "list")
assertContract(t, ExpectedResources(), "audit", "audit_operation_log", ReadOnly, "list")
assertContract(t, ExpectedResources(), "audit", "audit_export_log", ReadOnly, "list")
assertContract(t, ExpectedResources(), "audit", "audit_approval", ReadOnly, "list")
for _, contract := range ExpectedResources() {
if (contract.Domain == "safety" && strings.HasPrefix(contract.Name, legacySafetyPrefix)) ||
(contract.Domain == "audit" && strings.HasPrefix(contract.Name, legacyAuditPrefix)) {
t.Fatalf("legacy resource contract %s/%s must not be registered", contract.Domain, contract.Name)
}
}
}
func TestResourceDefinitionAllowsOnlySupportedMethods(t *testing.T) {
if (ResourceDefinition{Mode: ReadOnly}).Allows(http.MethodPost) {
t.Fatal("readonly allows POST")
@@ -234,8 +253,8 @@ func TestPrepareResourceValuesResolvesRequiredIdentityRelationsAndRejectsInvalid
func TestPrepareResourceValuesNormalizesStringJSONBFields(t *testing.T) {
t.Run("safety rule", func(t *testing.T) {
ctx, _ := updateContext(http.MethodPost, "/safety/saf_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":{"max":10},"action":"close-valve","gray_scope":["north"]}`))
values, err := prepareResourceValues(ctx, &models.SafRule{}, []string{"rule_code", "threshold", "action", "gray_scope"}, nil)
ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":{"max":10},"action":"close-valve","gray_scope":["north"]}`))
values, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action", "gray_scope"}, nil)
if err != nil {
t.Fatal(err)
}
@@ -268,8 +287,8 @@ func TestPrepareResourceValuesNormalizesStringJSONBFields(t *testing.T) {
})
t.Run("invalid json string", func(t *testing.T) {
ctx, _ := updateContext(http.MethodPost, "/safety/saf_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":"{invalid}","action":"close-valve"}`))
if _, err := prepareResourceValues(ctx, &models.SafRule{}, []string{"rule_code", "threshold", "action"}, nil); err == nil {
ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":"{invalid}","action":"close-valve"}`))
if _, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action"}, nil); err == nil {
t.Fatal("invalid JSON string was accepted for a string/jsonb field")
}
})
@@ -325,7 +344,7 @@ func TestKeywordColumnsUseSafeTextAllowlist(t *testing.T) {
model any
want []string
}{
{"safety rule excludes jsonb", &models.SafRule{}, []string{"rule_code", "action"}},
{"safety rule excludes jsonb", &models.SafeRule{}, []string{"rule_code", "action"}},
{"gas basic excludes sensitive fields", &models.GasBasic{}, []string{"code", "name"}},
{"user address has no searchable safe text", &models.UserAddress{}, []string{}},
}
@@ -632,19 +651,19 @@ func TestNonRootCannotAssignPlatformAccountRole(t *testing.T) {
func TestListSafetyEventDisposalsReturnsOnlyTheRequestedEventHistory(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "saf_event_disposal" WHERE saf_event_identity = $1`)).
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "safe_event_disposal" WHERE safe_event_identity = $1`)).
WithArgs("event-a").
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "saf_event_disposal" WHERE saf_event_identity = $1 ORDER BY created_at asc LIMIT $2`)).
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "safe_event_disposal" WHERE safe_event_identity = $1 ORDER BY created_at asc LIMIT $2`)).
WithArgs("event-a", 20).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "saf_event_identity", "action", "reason", "operator_identity"}).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "safe_event_identity", "action", "reason", "operator_identity"}).
AddRow(uint64(9), "disposal-a", nil, nil, "enabled", 1, "event-a", "close", "resolved", "operator-a"))
ctx, recorder := updateContext(http.MethodGet, "/safety/saf_event/event-a/disposals", "event-a", nil)
ctx, recorder := updateContext(http.MethodGet, "/safety/safe_event/event-a/disposals", "event-a", nil)
ListSafetyEventDisposals(ctx)
assertResponseCode(t, recorder, 0)
if !strings.Contains(recorder.Body.String(), `"saf_event_identity":"event-a"`) || strings.Contains(recorder.Body.String(), `"id":`) {
if !strings.Contains(recorder.Body.String(), `"safe_event_identity":"event-a"`) || strings.Contains(recorder.Body.String(), `"id":`) {
t.Fatalf("disposal history did not keep the event identity-only shape: %s", recorder.Body.String())
}
assertMockExpectations(t, mock)
@@ -799,19 +818,19 @@ func TestDisposeSafetyEventUpdatesEventAndAppendsOperatorActionTransactionally(t
_, mock := setupPlatformRoleDatabase(t)
now := time.Now().UTC()
mock.ExpectBegin()
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "saf_event" WHERE identity = $1 ORDER BY "saf_event"."id" LIMIT $2`)).
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "safe_event" WHERE identity = $1 ORDER BY "safe_event"."id" LIMIT $2`)).
WithArgs("event-a", 1).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "event_code", "level", "title", "smart_cylinder_valve_identity", "sla_at"}).
AddRow(uint64(3), "event-a", now, now, "open", 1, "E-1", 2, "alarm", "valve-a", nil))
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "saf_event" SET "status"=$1,"updated_at"=$2 WHERE identity = $3`)).
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "safe_event" SET "status"=$1,"updated_at"=$2 WHERE identity = $3`)).
WithArgs("disposed", sqlmock.AnyArg(), "event-a").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "saf_event_disposal" ("identity","created_at","updated_at","status","version","saf_event_identity","action","reason","operator_identity") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING "id"`)).
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "safe_event_disposal" ("identity","created_at","updated_at","status","version","safe_event_identity","action","reason","operator_identity") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING "id"`)).
WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "enabled", 1, "event-a", "close", "resolved", "operator-a").
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(1)))
mock.ExpectCommit()
ctx, recorder := updateContext(http.MethodPost, "/safety/saf_event/event-a/disposals", "event-a", []byte(`{"action":"close","reason":"resolved"}`))
ctx, recorder := updateContext(http.MethodPost, "/safety/safe_event/event-a/disposals", "event-a", []byte(`{"action":"close","reason":"resolved"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
DisposeSafetyEvent(ctx)

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

View File

@@ -101,7 +101,7 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te
for _, resource := range []string{
"/device/dev_smart_cylinder_valve", "/device/dev_device_binding",
"/safety/saf_rule", "/safety/saf_event", "/safety/saf_inspection",
"/safety/safe_rule", "/safety/safe_event", "/safety/safe_inspection",
"/ec/ec_category", "/ec/ec_product", "/ec/ec_product_attribute", "/ec/ec_product_image", "/ec/ec_cart", "/ec/ec_order", "/ec/ec_order_item", "/ec/ec_review",
"/delivery/delivery_task", "/delivery/delivery_track",
} {
@@ -126,11 +126,20 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te
}
}
disposal := "/heqi/platform/v1/safety/saf_event/:identity/disposals"
assertRouteMethods(t, routes, disposal, http.MethodPost)
disposal := "/heqi/platform/v1/safety/safe_event/:identity/disposals"
assertRouteMethods(t, routes, disposal, http.MethodGet, http.MethodPost)
if routes[disposal][http.MethodDelete] {
t.Fatal("safety event disposals must be append-only")
}
legacySafetyPrefix := "/safety/" + "sa" + "f_"
for _, resource := range []string{legacySafetyPrefix + "rule", legacySafetyPrefix + "event", legacySafetyPrefix + "inspection"} {
path := "/heqi/platform/v1" + resource
assertNoRouteMethods(t, routes, path, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, path+"/:identity/status", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
}
assertNoRouteMethods(t, routes, "/heqi/platform/v1"+legacySafetyPrefix+"event/:identity/disposals", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
}
func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T) {
@@ -157,7 +166,7 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
for _, resource := range []string{
"/wallet/wallet", "/wallet/wallet_ledger", "/wallet/wallet_recharge", "/wallet/wallet_withdrawal",
"/report/report", "/report/report_item", "/report/report_metric_snapshot",
"/audit/aud_operation_log", "/audit/aud_export_log", "/audit/aud_approval",
"/audit/audit_operation_log", "/audit/audit_export_log", "/audit/audit_approval",
} {
path := "/heqi/platform/v1" + resource
assertRouteMethods(t, routes, path, http.MethodGet)
@@ -169,7 +178,15 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
}
}
assertRouteMethods(t, routes, "/heqi/platform/v1/audit/aud_approval/:identity/approve", http.MethodPost)
assertRouteMethods(t, routes, "/heqi/platform/v1/audit/audit_approval/:identity/approve", http.MethodPost)
legacyAuditPrefix := "/audit/" + "au" + "d_"
for _, resource := range []string{legacyAuditPrefix + "operation_log", legacyAuditPrefix + "export_log", legacyAuditPrefix + "approval"} {
path := "/heqi/platform/v1" + resource
assertNoRouteMethods(t, routes, path, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
}
assertNoRouteMethods(t, routes, "/heqi/platform/v1"+legacyAuditPrefix+"approval/:identity/approve", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
}
func assertRouteMethods(t *testing.T, routes map[string]map[string]bool, path string, methods ...string) {

View File

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

View File

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

View File

@@ -1,7 +1,27 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { auditPlatform, scanInternalIdLeaks } from './audit-check.mjs';
const resourcesSource = readFileSync('src/api/resources.ts', 'utf8');
const legacySafetyPrefix = ['sa', 'f_'].join('');
const legacyAuditPrefix = ['au', 'd_'].join('');
test('资源定义使用 safe 和 audit 前缀', () => {
assert.match(resourcesSource, /define\(\s*'safe_rule',\s*'\/safety\/safe_rule'/);
assert.match(resourcesSource, /define\(\s*'safe_event',\s*'\/safety\/safe_event'/);
assert.match(resourcesSource, /define\(\s*'safe_inspection',\s*'\/safety\/safe_inspection'/);
assert.match(resourcesSource, /define\(\s*'safe_event_disposal',\s*'\/safety\/safe_event\/:identity\/disposals'/);
assert.match(resourcesSource, /define\(\s*'audit_operation_log',\s*'\/audit\/audit_operation_log'/);
assert.match(resourcesSource, /define\(\s*'audit_export_log',\s*'\/audit\/audit_export_log'/);
assert.match(resourcesSource, /define\(\s*'audit_approval',\s*'\/audit\/audit_approval'/);
assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacySafetyPrefix}(?:rule|event|inspection|event_disposal)',`));
assert.doesNotMatch(resourcesSource, new RegExp(`action\\('${legacySafetyPrefix}event_disposal',`));
assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacyAuditPrefix}(?:operation_log|export_log|approval)',`));
assert.doesNotMatch(resourcesSource, new RegExp(`/audit/${legacyAuditPrefix}approval/:identity/approve`));
});
test('只读页面将状态变更视为违规写操作', () => {
const failures = auditPlatform({
manifest: { resources: [], routes: [] },
@@ -61,20 +81,20 @@ test('每个资源必须由带菜单元数据的路由实际加载对应页面',
test('仅追加处置必须挂在安全事件详情动作且不得有独立页面', () => {
const failures = auditPlatform({
manifest: { resources: [{ domain: 'safety', name: 'saf_event_disposal', path: '/safety/saf_event/:identity/disposals', mode: 'append_only', pageKind: 'list' }], routes: [
{ method: 'GET', path: '/safety/saf_event/:identity/disposals' },
{ method: 'POST', path: '/safety/saf_event/:identity/disposals' },
manifest: { resources: [{ domain: 'safety', name: 'safe_event_disposal', path: '/safety/safe_event/:identity/disposals', mode: 'append_only', pageKind: 'list' }], routes: [
{ method: 'GET', path: '/safety/safe_event/:identity/disposals' },
{ method: 'POST', path: '/safety/safe_event/:identity/disposals' },
] },
resources: [{ name: 'saf_event_disposal', resource: '/safety/saf_event/:identity/disposals', mode: 'append_only', pageKind: 'list', title: '事件处置', fields: [{ key: 'action', label: '处置动作' }] }],
resources: [{ name: 'safe_event_disposal', resource: '/safety/safe_event/:identity/disposals', mode: 'append_only', pageKind: 'list', title: '事件处置', fields: [{ key: 'action', label: '处置动作' }] }],
readOnlyPage: '',
routeSources: [],
viewSources: new Map([['src/views/safety/saf_event_disposal/ListPage.vue', '<template />']]),
viewSources: new Map([['src/views/safety/safe_event_disposal/ListPage.vue', '<template />']]),
apiSources: new Map(),
});
assert.deepEqual(failures, [
'safety/saf_event_disposal: missing saf_event detail action',
'safety/saf_event_disposal: independent page exposed',
'safety/safe_event_disposal: missing safe_event detail action',
'safety/safe_event_disposal: independent page exposed',
]);
});

View File

@@ -62,7 +62,7 @@ test('安全规则与订单明细将 JSON 字段作为有效 JSON 字符串提
const fields = (name) => resources.find((item) => item.name === name).fields;
assert.deepEqual(
JSON.parse(JSON.stringify(buildResourcePayload(fields('saf_rule'), {
JSON.parse(JSON.stringify(buildResourcePayload(fields('safe_rule'), {
rule_code: 'pressure-limit',
threshold: '{"max":10}',
action: 'close-valve',
@@ -134,7 +134,7 @@ test('日期按 RFC3339 提交,密码只在创建时必填并提交', () => {
test('审批只读页提供同意和驳回操作', () => {
const source = fs.readFileSync(fromProjectRoot('src/views/shared/ReadOnlyListPage.vue'), 'utf8');
const resources = fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8');
assert.match(resources, /aud_approval[\s\S]*\/audit\/aud_approval\/:identity\/approve/);
assert.match(resources, /audit_approval[\s\S]*\/audit\/audit_approval\/:identity\/approve/);
assert.match(source, /submitDetailAction/);
});

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';
import { platformAPI, type DashboardOverview, type OrgDeliveryPoint, type OrgGasStation, type OrgServicePerson, type PlatfromAccount, type Profile, type SafEvent } from './api/platform';
import { platformAPI, type DashboardOverview, type OrgDeliveryPoint, type OrgGasStation, type OrgServicePerson, type PlatfromAccount, type Profile, type SafeEvent } from './api/platform';
type Tab = 'dashboard' | 'station' | 'delivery' | 'person' | 'user' | 'safety' | 'trade' | 'finance' | 'content' | 'track' | 'operation' | 'audit';
type CatalogItem = { title: string; description: string; operations: string[] };
@@ -14,7 +14,7 @@ const stations = ref<OrgGasStation[]>([]);
const deliveryPoints = ref<OrgDeliveryPoint[]>([]);
const servicePeople = ref<OrgServicePerson[]>([]);
const users = ref<PlatfromAccount[]>([]);
const safetyEvents = ref<SafEvent[]>([]);
const safetyEvents = ref<SafeEvent[]>([]);
const profile = ref<Profile>();
const loginForm = reactive({ username: 'root', password: '' });
const stationForm = reactive({ stationCode: '', name: '', principal: '', serviceArea: '' });
@@ -48,7 +48,7 @@ async function loadData() {
try {
[profile.value, overview.value, stations.value, deliveryPoints.value, servicePeople.value, users.value, safetyEvents.value] = await Promise.all([
platformAPI.getProfile(), platformAPI.getDashboard(), platformAPI.listOrgGasStation(), platformAPI.listOrgDeliveryPoint(),
platformAPI.listOrgServicePerson(), platformAPI.listPlatfromAccount(), platformAPI.listSafEvent(),
platformAPI.listOrgServicePerson(), platformAPI.listPlatfromAccount(), platformAPI.listSafeEvent(),
]);
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '加载数据失败';

View File

@@ -222,10 +222,10 @@ const titles: Record<string, string> = {
dev_smart_cylinder_valve: '智能钢瓶阀',
dev_device_binding: '设备绑定',
dev_telemetry: '设备遥测',
saf_rule: '安全规则',
saf_event: '安全事件',
saf_inspection: '安全检查',
saf_event_disposal: '事件处置',
safe_rule: '安全规则',
safe_event: '安全事件',
safe_inspection: '安全检查',
safe_event_disposal: '事件处置',
ec_category: '商品分类',
ec_product: '商品管理',
ec_product_attribute: '商品属性',
@@ -250,9 +250,9 @@ const titles: Record<string, string> = {
report: '报表',
report_item: '报表项目',
report_metric_snapshot: '指标快照',
aud_operation_log: '操作审计',
aud_export_log: '导出审计',
aud_approval: '审批审计',
audit_operation_log: '操作审计',
audit_export_log: '导出审计',
audit_approval: '审批审计',
};
const define = (
name: string,
@@ -412,7 +412,7 @@ export const resources: ResourceUiDefinition[] = [
'reported_at',
'payload',
]),
define('saf_rule', '/safety/saf_rule', 'writable', 'list', [
define('safe_rule', '/safety/safe_rule', 'writable', 'list', [
'rule_code!',
'version_no',
'threshold',
@@ -420,8 +420,8 @@ export const resources: ResourceUiDefinition[] = [
'gray_scope',
]),
define(
'saf_event',
'/safety/saf_event',
'safe_event',
'/safety/safe_event',
'writable',
'list',
[
@@ -432,21 +432,21 @@ export const resources: ResourceUiDefinition[] = [
'sla_at',
],
[
action('saf_event_disposal', '/safety/saf_event/:identity/disposals', [
action('safe_event_disposal', '/safety/safe_event/:identity/disposals', [
'action!',
'reason!',
]),
],
),
define('saf_inspection', '/safety/saf_inspection', 'writable', 'list', [
define('safe_inspection', '/safety/safe_inspection', 'writable', 'list', [
'user_account_identity!',
'staff_account_identity!',
'result!',
'evidence_uri',
]),
define(
'saf_event_disposal',
'/safety/saf_event/:identity/disposals',
'safe_event_disposal',
'/safety/safe_event/:identity/disposals',
'append_only',
'list',
['action!', 'reason!'],
@@ -612,13 +612,13 @@ export const resources: ResourceUiDefinition[] = [
'list',
['metric_code', 'scope_type', 'stat_at', 'metric_value'],
),
define('aud_operation_log', '/audit/aud_operation_log', 'readonly', 'list', [
define('audit_operation_log', '/audit/audit_operation_log', 'readonly', 'list', [
'operator_identity',
'action',
'object_identity',
'created_at',
]),
define('aud_export_log', '/audit/aud_export_log', 'readonly', 'list', [
define('audit_export_log', '/audit/audit_export_log', 'readonly', 'list', [
'applicant_identity',
'purpose',
'field_scope',
@@ -626,8 +626,8 @@ export const resources: ResourceUiDefinition[] = [
'file_uri',
]),
define(
'aud_approval',
'/audit/aud_approval',
'audit_approval',
'/audit/audit_approval',
'readonly',
'list',
[
@@ -640,10 +640,10 @@ export const resources: ResourceUiDefinition[] = [
'handled_at',
],
[
action('同意', '/audit/aud_approval/:identity/approve', ['opinion'], {
action('同意', '/audit/audit_approval/:identity/approve', ['opinion'], {
status: 'approved',
}),
action('驳回', '/audit/aud_approval/:identity/approve', ['opinion!'], {
action('驳回', '/audit/audit_approval/:identity/approve', ['opinion!'], {
status: 'rejected',
}),
],

View File

@@ -4,9 +4,9 @@ const routes: AppRouteRecordRaw[] = [{
path: '/audit', name: 'audit', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.audit', requiresAuth: true, icon: 'icon-apps', order: 22 },
children: [
{ path: 'aud-operation-log', name: 'audit-aud-operation-log', component: () => import('@/views/audit/aud_operation_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.aud_operation_log', requiresAuth: true, menuCode: 'audit' } },
{ path: 'aud-export-log', name: 'audit-aud-export-log', component: () => import('@/views/audit/aud_export_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.aud_export_log', requiresAuth: true, menuCode: 'audit' } },
{ path: 'aud-approval', name: 'audit-aud-approval', component: () => import('@/views/audit/aud_approval/ListPage.vue'), meta: { locale: 'menu.platform.audit.aud_approval', requiresAuth: true, menuCode: 'audit' } }
{ path: 'audit-operation-log', name: 'audit-audit-operation-log', component: () => import('@/views/audit/audit_operation_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.audit_operation_log', requiresAuth: true, menuCode: 'audit' } },
{ path: 'audit-export-log', name: 'audit-audit-export-log', component: () => import('@/views/audit/audit_export_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.audit_export_log', requiresAuth: true, menuCode: 'audit' } },
{ path: 'audit-approval', name: 'audit-audit-approval', component: () => import('@/views/audit/audit_approval/ListPage.vue'), meta: { locale: 'menu.platform.audit.audit_approval', requiresAuth: true, menuCode: 'audit' } }
],
}];
export default routes;

View File

@@ -1,8 +1,8 @@
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const routes: AppRouteRecordRaw[] = [{ path: '/safety', name: 'safety', component: DEFAULT_LAYOUT, meta: { locale: 'menu.platform.safety', requiresAuth: true, icon: 'icon-apps', order: 15 }, children: [
{ path: 'saf-rule', name: 'safety-saf-rule', component: () => import('@/views/safety/saf_rule/ListPage.vue'), meta: { locale: 'menu.platform.safety.saf_rule', requiresAuth: true, menuCode: 'safety' } },
{ path: 'saf-event', name: 'safety-saf-event', component: () => import('@/views/safety/saf_event/ListPage.vue'), meta: { locale: 'menu.platform.safety.saf_event', requiresAuth: true, menuCode: 'safety' } },
{ path: 'saf-inspection', name: 'safety-saf-inspection', component: () => import('@/views/safety/saf_inspection/ListPage.vue'), meta: { locale: 'menu.platform.safety.saf_inspection', requiresAuth: true, menuCode: 'safety' } }
{ path: 'safe-rule', name: 'safety-safe-rule', component: () => import('@/views/safety/safe_rule/ListPage.vue'), meta: { locale: 'menu.platform.safety.safe_rule', requiresAuth: true, menuCode: 'safety' } },
{ path: 'safe-event', name: 'safety-safe-event', component: () => import('@/views/safety/safe_event/ListPage.vue'), meta: { locale: 'menu.platform.safety.safe_event', requiresAuth: true, menuCode: 'safety' } },
{ path: 'safe-inspection', name: 'safety-safe-inspection', component: () => import('@/views/safety/safe_inspection/ListPage.vue'), meta: { locale: 'menu.platform.safety.safe_inspection', requiresAuth: true, menuCode: 'safety' } }
] }];
export default routes;

View File

@@ -2,5 +2,5 @@
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/audit/aud_approval');
const definition = getResource('/audit/audit_approval');
</script>

View File

@@ -2,5 +2,5 @@
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/audit/aud_export_log');
const definition = getResource('/audit/audit_export_log');
</script>

View File

@@ -2,5 +2,5 @@
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/audit/aud_operation_log');
const definition = getResource('/audit/audit_operation_log');
</script>

View File

@@ -2,5 +2,5 @@
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/safety/saf_event');
const definition = getResource('/safety/safe_event');
</script>

View File

@@ -2,5 +2,5 @@
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/safety/saf_inspection');
const definition = getResource('/safety/safe_inspection');
</script>

View File

@@ -2,5 +2,5 @@
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/safety/saf_rule');
const definition = getResource('/safety/safe_rule');
</script>