From 8739cfd4d6e9307d3dd5850f719eceb5bf26ded1 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 27 Jul 2026 15:49:51 +0800 Subject: [PATCH] refactor(platform): remove safety management module --- backend/api/internal/initdb/platform.go | 1 - backend/api/internal/initdb/platform_test.go | 2 +- .../internal/logic/platform/health_test.go | 1 - .../api/internal/logic/platform/resource.go | 3 - .../internal/logic/platform/resource_test.go | 80 ++----------------- .../logic/platform/task4_resources.go | 68 ---------------- backend/api/internal/models/query.go | 4 - backend/api/internal/models/safe_event.go | 19 ----- .../internal/models/safe_event_disposal.go | 15 ---- .../api/internal/models/safe_inspection.go | 15 ---- backend/api/internal/models/safe_rule.go | 16 ---- backend/api/internal/routers/platform.go | 9 --- backend/api/internal/routers/platform_test.go | 16 ++-- docs/05-平台总后台需求-分析.md | 15 +--- docs/10-技术实现规划.md | 2 - .../platform_admin/scripts/audit-check.mjs | 12 +-- .../scripts/audit-check.test.mjs | 26 +----- .../scripts/final-important.test.mjs | 16 +--- frontend/platform_admin/src/App.vue | 14 ++-- frontend/platform_admin/src/api/resources.ts | 55 ------------- .../src/router/routes/modules/platform.ts | 3 +- .../src/router/routes/modules/safety.ts | 8 -- .../src/views/safety/safe_event/ListPage.vue | 6 -- .../views/safety/safe_inspection/ListPage.vue | 6 -- .../src/views/safety/safe_rule/ListPage.vue | 6 -- 25 files changed, 27 insertions(+), 391 deletions(-) delete mode 100644 backend/api/internal/models/safe_event.go delete mode 100644 backend/api/internal/models/safe_event_disposal.go delete mode 100644 backend/api/internal/models/safe_inspection.go delete mode 100644 backend/api/internal/models/safe_rule.go delete mode 100644 frontend/platform_admin/src/router/routes/modules/safety.ts delete mode 100644 frontend/platform_admin/src/views/safety/safe_event/ListPage.vue delete mode 100644 frontend/platform_admin/src/views/safety/safe_inspection/ListPage.vue delete mode 100644 frontend/platform_admin/src/views/safety/safe_rule/ListPage.vue diff --git a/backend/api/internal/initdb/platform.go b/backend/api/internal/initdb/platform.go index 99a6ea6..67903b5 100644 --- a/backend/api/internal/initdb/platform.go +++ b/backend/api/internal/initdb/platform.go @@ -38,7 +38,6 @@ func InitPlatformAccess(database *gorm.DB) error { {Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "staff", Name: "服务人员", Icon: "icon-user", Path: "/staff/list", SortNo: 40}, {Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "user", Name: "业主客户", Icon: "icon-user-group", Path: "/user/list", SortNo: 50}, {Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "device", Name: "设备管理", Icon: "icon-storage", Path: "/device", SortNo: 60}, - {Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "safety", Name: "安全治理", Icon: "icon-safe", Path: "/safety", SortNo: 70}, {Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "ec", Name: "电商管理", Icon: "icon-shopping", Path: "/ec/product", SortNo: 80}, {Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "finance", Name: "财务管理", Icon: "icon-safe", Path: "/finance/payment", SortNo: 90}, {Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "wallet", Name: "钱包中心", Icon: "icon-wallet", Path: "/wallet/list", SortNo: 100}, diff --git a/backend/api/internal/initdb/platform_test.go b/backend/api/internal/initdb/platform_test.go index 5b79e7a..2eb6e68 100644 --- a/backend/api/internal/initdb/platform_test.go +++ b/backend/api/internal/initdb/platform_test.go @@ -26,7 +26,7 @@ func TestInitPlatformAccessSeedsEveryProtectedFrontendDomain(t *testing.T) { AddRow(uint64(1), "root-role", "enabled", 1, "root", "Root", "global", true)) domains := []string{ - "dashboard", "gas", "delivery", "staff", "user", "device", "safety", + "dashboard", "gas", "delivery", "staff", "user", "device", "ec", "finance", "wallet", "content", "customer_service", "platform", } for index, domain := range domains { diff --git a/backend/api/internal/logic/platform/health_test.go b/backend/api/internal/logic/platform/health_test.go index 3e5aeb6..c21a68b 100644 --- a/backend/api/internal/logic/platform/health_test.go +++ b/backend/api/internal/logic/platform/health_test.go @@ -15,7 +15,6 @@ 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 "safe_event" WHERE status = \$1`, } { mock.ExpectQuery(regexp.MustCompile(query).String()).WithArgs(sqlmock.AnyArg()).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) } diff --git a/backend/api/internal/logic/platform/resource.go b/backend/api/internal/logic/platform/resource.go index 61c1fc0..a014440 100644 --- a/backend/api/internal/logic/platform/resource.go +++ b/backend/api/internal/logic/platform/resource.go @@ -75,7 +75,6 @@ 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", "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"), @@ -101,8 +100,6 @@ func resourcePath(domain, name string) string { return "/user/address" case "user_service_relation": return "/user/service_relation" - case "safe_event_disposal": - return "/safety/safe_event/:identity/disposals" default: return "/" + domain + "/" + name } diff --git a/backend/api/internal/logic/platform/resource_test.go b/backend/api/internal/logic/platform/resource_test.go index 67a6caa..75ea221 100644 --- a/backend/api/internal/logic/platform/resource_test.go +++ b/backend/api/internal/logic/platform/resource_test.go @@ -42,16 +42,13 @@ func TestContentUsesCMSResourceAndRemovedDomainsAreAbsent(t *testing.T) { } } -func TestSafeResourceContracts(t *testing.T) { - legacySafetyPrefix := "sa" + "f_" - - 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") +func TestSafetyResourcesAreAbsent(t *testing.T) { + removed := map[string]struct{}{ + "safe_rule": {}, "safe_event": {}, "safe_inspection": {}, "safe_event_disposal": {}, + } for _, contract := range ExpectedResources() { - if contract.Domain == "safety" && strings.HasPrefix(contract.Name, legacySafetyPrefix) { - t.Fatalf("legacy resource contract %s/%s must not be registered", contract.Domain, contract.Name) + if _, exists := removed[contract.Name]; exists || contract.Domain == "safety" { + t.Fatalf("removed safety resource %s/%s remains registered", contract.Domain, contract.Name) } } } @@ -258,17 +255,6 @@ func TestPrepareResourceValuesResolvesRequiredIdentityRelationsAndRejectsInvalid } func TestPrepareResourceValuesAcceptsArbitraryTextFields(t *testing.T) { - t.Run("safety rule", func(t *testing.T) { - ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":"{invalid}","action":"close-valve","gray_scope":"north region"}`)) - values, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action", "gray_scope"}, nil) - if err != nil { - t.Fatal(err) - } - if values["threshold"] != `{invalid}` || values["gray_scope"] != `north region` { - t.Fatalf("text values were not preserved: %#v", values) - } - }) - t.Run("order item", func(t *testing.T) { _, mock := setupPlatformRoleDatabase(t) mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id" FROM "ec_order" WHERE identity = $1 ORDER BY "ec_order"."id" LIMIT $2`)). @@ -292,12 +278,6 @@ func TestPrepareResourceValuesAcceptsArbitraryTextFields(t *testing.T) { assertMockExpectations(t, mock) }) - t.Run("arbitrary text", func(t *testing.T) { - 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.Fatalf("arbitrary text was rejected: %v", err) - } - }) } func TestCreateGasAccountResolvesGasBasicIdentityBeforePersisting(t *testing.T) { @@ -350,7 +330,6 @@ func TestKeywordColumnsUseSafeTextAllowlist(t *testing.T) { model any want []string }{ - {"safety rule excludes arbitrary text fields", &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{}}, } @@ -655,26 +634,6 @@ func TestNonRootCannotAssignPlatformAccountRole(t *testing.T) { } } -func TestListSafetyEventDisposalsReturnsOnlyTheRequestedEventHistory(t *testing.T) { - _, mock := setupPlatformRoleDatabase(t) - 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 "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", "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/safe_event/event-a/disposals", "event-a", nil) - ListSafetyEventDisposals(ctx) - - assertResponseCode(t, recorder, 0) - 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) -} - func TestListGasAccountProjectsGasBasicIdentityAndNeverReturnsRelationID(t *testing.T) { _, mock := setupPlatformRoleDatabase(t) mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "gas_account"`)). @@ -820,33 +779,6 @@ func TestGetDeliveryTrackPointReturnsCoordinatesWithPreciseLocationScope(t *test assertMockExpectations(t, mock) } -func TestDisposeSafetyEventUpdatesEventAndAppendsOperatorActionTransactionally(t *testing.T) { - _, mock := setupPlatformRoleDatabase(t) - now := time.Now().UTC() - mock.ExpectBegin() - 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 "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 "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/safe_event/event-a/disposals", "event-a", []byte(`{"action":"close","reason":"resolved"}`)) - ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"}) - DisposeSafetyEvent(ctx) - - assertResponseCode(t, recorder, 0) - if !strings.Contains(recorder.Body.String(), `"operator_identity":"operator-a"`) { - t.Fatalf("disposal omitted its operator: %s", recorder.Body.String()) - } - assertMockExpectations(t, mock) -} - func TestReplacePlatformRoleMenusAllowsAnEmptySetToClearAssignmentsTransactionally(t *testing.T) { _, mock := setupPlatformRoleDatabase(t) mock.ExpectBegin() diff --git a/backend/api/internal/logic/platform/task4_resources.go b/backend/api/internal/logic/platform/task4_resources.go index c1d4f55..29c30c5 100644 --- a/backend/api/internal/logic/platform/task4_resources.go +++ b/backend/api/internal/logic/platform/task4_resources.go @@ -15,7 +15,6 @@ import ( "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) // ResourceRelation accepts a stable external identity while retaining the @@ -530,73 +529,6 @@ func stripInternalIDs(value any) any { return value } -// DisposeSafetyEvent atomically updates an event and appends its operator-owned -// action record. Disposal records deliberately have no update or delete route. -func ListSafetyEventDisposals(ctx *gin.Context) { - page, size := pageSize(ctx) - 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) - return - } - if err := query.Order("created_at asc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil { - infra.Response.Error(ctx, err) - return - } - infra.Response.Success(ctx, gin.H{"total": total, "list": resourceResponse(list)}) -} - -func DisposeSafetyEvent(ctx *gin.Context) { - claims, err := middleware.ParseAuth(ctx) - if err != nil { - infra.Response.Error(ctx, err) - return - } - var request struct { - Action string `json:"action" binding:"required,max=64"` - Reason string `json:"reason" binding:"max=2000"` - Status string `json:"status" binding:"max=32"` - } - if err := ctx.ShouldBindJSON(&request); err != nil { - infra.Response.Error(ctx, errcode.ErrInvalidArgument) - return - } - if request.Status == "" { - request.Status = "disposed" - } - var disposal models.SafeEventDisposal - err = impl.DBService.Transaction(func(transaction *gorm.DB) error { - var event models.SafeEvent - if err := transaction.Where("identity = ?", ctx.Param("identity")).First(&event).Error; err != nil { - return err - } - 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.SafeEventDisposal{ - Entity: newEntity("enabled"), - SafeEventIdentity: event.Identity, - Action: request.Action, - Reason: request.Reason, - OperatorIdentity: claims.Identity, - } - return transaction.Create(&disposal).Error - }) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - infra.Response.Error(ctx, errcode.ErrRecordNotFound) - return - } - infra.Response.Error(ctx, err) - return - } - infra.Response.Success(ctx, resourceResponse(disposal)) -} - // GetEcOrder returns the order together with its immutable item snapshots. func GetEcOrder(ctx *gin.Context) { var order models.EcOrder diff --git a/backend/api/internal/models/query.go b/backend/api/internal/models/query.go index bca8db8..dff973f 100644 --- a/backend/api/internal/models/query.go +++ b/backend/api/internal/models/query.go @@ -8,7 +8,6 @@ type DashboardOverview struct { DeliveryBasicCount int64 `json:"delivery_basic_count"` // 启用配送点数量 StaffCount int64 `json:"staff_count"` // 在岗服务人员数量 UserCount int64 `json:"user_count"` // 启用业主客户数量 - PendingSafetyCount int64 `json:"pending_safety_count"` // 待处理安全事件数量 } // GetDashboardOverview 通过独立查询返回首页概览指标。 @@ -26,9 +25,6 @@ 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(&SafeEvent{}).Where("status = ?", "pending").Count(&overview.PendingSafetyCount).Error; err != nil { - return DashboardOverview{}, err - } return overview, nil } diff --git a/backend/api/internal/models/safe_event.go b/backend/api/internal/models/safe_event.go deleted file mode 100644 index cb08a59..0000000 --- a/backend/api/internal/models/safe_event.go +++ /dev/null @@ -1,19 +0,0 @@ -package models - -import ( - "git.apinb.com/bsm-sdk/core/database" - "time" -) - -// 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 业务字段 - Title string `gorm:"column:title;type:varchar(256);not null;default:''" json:"title"` // title 业务字段 - SmartCylinderValveIdentity string `gorm:"column:smart_cylinder_valve_identity;type:varchar(36);not null;default:'';index" json:"smart_cylinder_valve_identity"` // smart_cylinder_valve_identity 业务字段 - SLAAt *time.Time `gorm:"column:sla_at;type:timestamptz" json:"sla_at"` // sla_at 业务字段 -} - -func init() { database.AppendMigrate(&SafeEvent{}) } -func (table *SafeEvent) TableName() string { return "safe_event" } diff --git a/backend/api/internal/models/safe_event_disposal.go b/backend/api/internal/models/safe_event_disposal.go deleted file mode 100644 index 1583736..0000000 --- a/backend/api/internal/models/safe_event_disposal.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import "git.apinb.com/bsm-sdk/core/database" - -// SafeEventDisposal 对应 safe_event_disposal,保存安全处置记录。 -type SafeEventDisposal struct { - Entity // 公共实体字段 - SafeEventIdentity string `gorm:"column:safe_event_identity;type:varchar(36);not null;index" json:"safe_event_identity"` // safe_event_identity 业务字段 - Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` // action 业务字段 - Reason string `gorm:"column:reason;type:text;not null;default:''" json:"reason"` // reason 业务字段 - OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:''" json:"operator_identity"` // operator_identity 业务字段 -} - -func init() { database.AppendMigrate(&SafeEventDisposal{}) } -func (table *SafeEventDisposal) TableName() string { return "safe_event_disposal" } diff --git a/backend/api/internal/models/safe_inspection.go b/backend/api/internal/models/safe_inspection.go deleted file mode 100644 index 797fe21..0000000 --- a/backend/api/internal/models/safe_inspection.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import "git.apinb.com/bsm-sdk/core/database" - -// 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 业务字段 - Result string `gorm:"column:result;type:varchar(32);not null" json:"result"` // result 业务字段 - EvidenceURI string `gorm:"column:evidence_uri;type:varchar(512);not null;default:''" json:"evidence_uri"` // evidence_uri 业务字段 -} - -func init() { database.AppendMigrate(&SafeInspection{}) } -func (table *SafeInspection) TableName() string { return "safe_inspection" } diff --git a/backend/api/internal/models/safe_rule.go b/backend/api/internal/models/safe_rule.go deleted file mode 100644 index c9d3de4..0000000 --- a/backend/api/internal/models/safe_rule.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -import "git.apinb.com/bsm-sdk/core/database" - -// 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 业务字段 - Threshold string `gorm:"column:threshold;type:text;not null;default:''" json:"threshold"` // threshold 业务字段 - Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` // action 业务字段 - GrayScope string `gorm:"column:gray_scope;type:text;not null;default:''" json:"gray_scope"` // gray_scope 业务字段 -} - -func init() { database.AppendMigrate(&SafeRule{}) } -func (table *SafeRule) TableName() string { return "safe_rule" } diff --git a/backend/api/internal/routers/platform.go b/backend/api/internal/routers/platform.go index 8d162a8..29b96c0 100644 --- a/backend/api/internal/routers/platform.go +++ b/backend/api/internal/routers/platform.go @@ -28,7 +28,6 @@ func RegisterPlatform(serviceKey string, engine *gin.Engine) { registerStaffRoute(protected) registerUserRoute(protected) registerDeviceRoute(protected) - registerSafetyRoute(protected) registerCommerceRoute(protected) registerFinanceRoute(protected) registerContentRoute(protected) @@ -60,14 +59,6 @@ func registerDeviceRoute(group *gin.RouterGroup) { telemetry.GET("/:identity", get) } -func registerSafetyRoute(group *gin.RouterGroup) { - 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) { categoryRelations := []platform.ResourceRelation{optionalRelation("parent_identity", "parent_id", &models.EcCategory{})} _, categoryCreate, _, categoryUpdate := platform.ResourceHandlers(&models.EcCategory{}, []string{"name", "sort_no"}, []string{"name", "sort_no"}, categoryRelations...) diff --git a/backend/api/internal/routers/platform_test.go b/backend/api/internal/routers/platform_test.go index e8c9f4a..b1e8f7c 100644 --- a/backend/api/internal/routers/platform_test.go +++ b/backend/api/internal/routers/platform_test.go @@ -87,7 +87,7 @@ func TestPlatformOrganizationAndAccountRoutesExposeResourceCRUD(t *testing.T) { assertRouteMethods(t, routes, "/heqi/platform/v1/platform/platform_role/:identity/menus", http.MethodPut) } -func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *testing.T) { +func TestPlatformDeviceCommerceAndDeliveryRoutesFollowTheirContracts(t *testing.T) { engine := gin.New() RegisterPlatform("heqi", engine) @@ -101,7 +101,6 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te for _, resource := range []string{ "/device/dev_smart_cylinder_valve", "/device/dev_device_binding", - "/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,20 +125,15 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te } } - 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"} { + for _, resource := range []string{ + "/safety/safe_rule", "/safety/safe_event", "/safety/safe_inspection", + "/safety/safe_event/:identity/disposals", + } { 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 TestPlatformFinanceContentRoutesFollowTheirContracts(t *testing.T) { diff --git a/docs/05-平台总后台需求-分析.md b/docs/05-平台总后台需求-分析.md index 65c1c6f..1297417 100644 --- a/docs/05-平台总后台需求-分析.md +++ b/docs/05-平台总后台需求-分析.md @@ -59,17 +59,13 @@ | 用户地址 | `user_address` / `UserAddress` / `user_address.go` | `id`、`identity`、`user_id`、`address`、`longitude`、`latitude`、`is_default` | 地址历史和订单快照来源 | | 用户服务关系 | `user_service_relation` / `UserServiceRelation` / `user_service_relation.go` | `id`、`identity`、`user_id`、`gas_basic_id`、`delivery_basic_id`、`staff_id` | 用户与气站/配送点的服务归属 | -### 4.3 设备与安全 +### 4.3 设备 | 实体 | 表 / 模型 / 文件 | 关键字段 | 说明 | | --- | --- | --- | --- | | 智能瓶阀 | `dev_smart_cylinder_valve` / `DevSmartCylinderValve` / `dev_smart_cylinder_valve.go` | `device_no`、`model`、`online_status`、`owner_identity` | 智能瓶阀全生命周期档案 | | 设备绑定 | `dev_device_binding` / `DevDeviceBinding` / `dev_device_binding.go` | `id`、`identity`、`smart_cylinder_valve_id`、`user_id`、`effective_at`、`expired_at` | 用户/家庭与智能瓶阀授权关系 | | 遥测 | `dev_telemetry` / `DevTelemetry` / `dev_telemetry.go` | `smart_cylinder_valve_identity`、`reported_at`、`payload`、`quality_flag` | 遥测摘要与质量标记 | -| 安全规则 | `saf_rule` / `SafRule` / `saf_rule.go` | `rule_code`、`version_no`、`threshold`、`action`、`gray_scope` | 告警、自动关阀、静默和灰度规则 | -| 安全事件 | `saf_event` / `SafEvent` / `saf_event.go` | `event_code`、`level`、`smart_cylinder_valve_identity`、`status`、`sla_at` | 告警、安检和人工发现事件统一入口 | -| 安全处置 | `saf_event_disposal` / `SafEventDisposal` / `saf_event_disposal.go` | `saf_event_identity`、`action`、`reason`、`operator_identity` | 派发、升级、关阀、复核和结案记录 | -| 安检记录 | `saf_inspection` / `SafInspection` / `saf_inspection.go` | `id`、`identity`、`user_id`、`staff_id`、`result`、`evidence_uri` | 安检、整改和复检证据 | ### 4.4 电商、订单与配送轨迹 @@ -149,16 +145,12 @@ | `PATCH` | `/user/:identity/status` | 冻结登录、限制下单或设备控制 | | `POST` | `/user/service_relation` | 建立或变更用户服务关系 | -### 5.4 智能瓶阀安全、订单与配送轨迹 +### 5.4 智能瓶阀、订单与配送轨迹 | 方法 | 路由 | 作用 | | --- | --- | --- | | `GET/POST` | `/device/dev_smart_cylinder_valve` | 智能瓶阀查询、建档 | | `GET/PUT/DELETE` | `/device/dev_smart_cylinder_valve/:identity` | 智能瓶阀详情、更新、逻辑删除/归档 | -| `GET/POST` | `/safety/saf_event` | 安全事件查询、创建 | -| `GET/PUT/DELETE` | `/safety/saf_event/:identity` | 安全事件详情、更新、逻辑删除/归档 | -| `GET/POST` | `/safety/saf_rule` | 安全规则 CRUD 的列表与创建 | -| `GET/PUT/DELETE` | `/safety/saf_rule/:identity` | 安全规则详情、更新、逻辑删除 | | `GET/POST` | `/ec/ec_order` | 电商订单查询、创建 | | `GET/PUT/DELETE` | `/ec/ec_order/:identity` | 电商订单详情、更新、逻辑删除/归档 | | `GET/POST` | `/delivery/delivery_track` | 配送轨迹查询、创建 | @@ -237,9 +229,6 @@ | 业主客户 `UsersRound` | 服务关系 `GitFork` | `/user/service-relation` | `views/user/service-relation` | `ListPage.vue` | `user_service_relation` CRUD、气站/配送点归属 | | 设备管理 `ShieldAlert` | 智能瓶阀 `Gauge` | `/device/valve` | `views/device/valve` | `ListPage.vue` | `dev_smart_cylinder_valve` CRUD、型号、在线状态、归属 | | 设备管理 `ShieldAlert` | 设备绑定 `Link2` | `/device/binding` | `views/device/binding` | `ListPage.vue` | `dev_device_binding` CRUD、用户与智能瓶阀绑定 | -| 设备管理 `ShieldAlert` | 安全规则 `Siren` | `/safety/rule` | `views/safety/rule` | `ListPage.vue` | `saf_rule` CRUD、阈值、规则版本和状态 | -| 设备管理 `ShieldAlert` | 安全事件 `TriangleAlert` | `/safety/event` | `views/safety/event` | `ListPage.vue` | `saf_event` CRUD、等级、设备、用户和状态 | -| 设备管理 `ShieldAlert` | 安检记录 `ClipboardCheck` | `/safety/inspection` | `views/safety/inspection` | `ListPage.vue` | `saf_inspection` CRUD、人员、结果和证据 | | 电商管理 `ShoppingBag` | 商品分类 `FolderTree` | `/ec/category` | `views/ec/category` | `TreePage.vue` | `ec_category` 分类树 CRUD、排序和状态 | | 电商管理 `ShoppingBag` | 商品管理 `PackageSearch` | `/ec/product` | `views/ec/product` | `ListPage.vue` | `ec_product` CRUD、分类、价格、库存和状态 | | 电商管理 `ShoppingBag` | 商品属性 `Tags` | `/ec/product-attribute` | `views/ec/product-attribute` | `ListPage.vue` | `ec_product_attribute` CRUD、商品规格与属性 | diff --git a/docs/10-技术实现规划.md b/docs/10-技术实现规划.md index a0b9c2d..bad5650 100644 --- a/docs/10-技术实现规划.md +++ b/docs/10-技术实现规划.md @@ -113,7 +113,6 @@ platforms/ - `identity`:登录、验证码、账号、RBAC、组织、数据范围、协议同意。 - `organization`:气站、配送点、服务区域、人员归属、用户服务关系和邀请注册二维码。 - `device`:设备注册/绑定、设备模型、遥测、命令、在线状态、固件。 -- `safety`:规则、告警事件、分级、SLA、整改、安检、复检、升级。 - `catalog`:分类、商品、SKU、库存、优惠券、收藏、购物车。 - `order`:订单、支付、合同、配送、安装、售后、评价。 - `dispatch`:服务人员、区域、班次、任务、派单、导航、配送轨迹与现场取证。 @@ -136,7 +135,6 @@ platforms/ | 身份与权限 | `idn_` | `idn_account`、`idn_role`、`idn_permission` | | 组织与服务关系 | `org_` | `org_gas_station`、`org_delivery_point`、`org_service_person`、`org_user_service_relation`、`org_invitation_qr_code` | | 设备与物联网 | `dev_` | `dev_device`、`dev_telemetry`、`dev_command` | -| 安全与安检 | `saf_` | `saf_event`、`saf_rule`、`saf_inspection`、`saf_rectification` | | 商品与营销 | `cat_` | `cat_product`、`cat_sku`、`cat_coupon`、`cat_cart` | | 订单与履约 | `ord_` | `ord_order`、`ord_payment`、`ord_service_task`、`ord_delivery_track` | | 调度与配送 | `dsp_` | `dsp_assignment`、`dsp_shift`、`dsp_delivery_track_point` | diff --git a/frontend/platform_admin/scripts/audit-check.mjs b/frontend/platform_admin/scripts/audit-check.mjs index 7863db0..336f0c7 100644 --- a/frontend/platform_admin/scripts/audit-check.mjs +++ b/frontend/platform_admin/scripts/audit-check.mjs @@ -73,15 +73,9 @@ 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 === '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); - if (!coverage.hasPage) failures.push(`${label}: missing page`); - if (!coverage.hasMenu) failures.push(`${label}: missing menu route`); - } + const coverage = routeCoverage(contract, routeSources, viewSources); + if (!coverage.hasPage) failures.push(`${label}: missing page`); + if (!coverage.hasMenu) failures.push(`${label}: missing menu route`); if (contract.mode === 'readonly') { const statusCall = new RegExp(`resourceApi\\.updateStatus\\(\\s*['\"]${escapeRegExp(contract.path)}['\"]`); for (const [file, source] of [...sourceEntries(routeSources, 'src/router'), ...sourceEntries(apiSources, 'src/api'), ...sourceEntries(viewSources, 'src/views')]) { diff --git a/frontend/platform_admin/scripts/audit-check.test.mjs b/frontend/platform_admin/scripts/audit-check.test.mjs index 710e222..4d02987 100644 --- a/frontend/platform_admin/scripts/audit-check.test.mjs +++ b/frontend/platform_admin/scripts/audit-check.test.mjs @@ -7,11 +7,8 @@ const resourcesSource = readFileSync('src/api/resources.ts', 'utf8'); const legacySafetyPrefix = ['sa', 'f_'].join(''); const legacyAuditPrefix = ['au', 'd_'].join(''); -test('资源定义使用 safe 前缀且不保留已删除审计资源', () => { - 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'/); +test('安全模块资源定义已完全删除', () => { + assert.doesNotMatch(resourcesSource, /safe_(?:rule|event|inspection|event_disposal)|\/safety\//); assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacySafetyPrefix}(?:rule|event|inspection|event_disposal)',`)); assert.doesNotMatch(resourcesSource, new RegExp(`action\\('${legacySafetyPrefix}event_disposal',`)); @@ -76,25 +73,6 @@ test('每个资源必须由带菜单元数据的路由实际加载对应页面', assert.deepEqual(failures, ['gas/gas_basic: missing menu route']); }); -test('仅追加处置必须挂在安全事件详情动作且不得有独立页面', () => { - const failures = auditPlatform({ - 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: '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/safe_event_disposal/ListPage.vue', '