diff --git a/backend/api/internal/logic/platform/account.go b/backend/api/internal/logic/platform/account.go index 57fe39e..4d055a4 100644 --- a/backend/api/internal/logic/platform/account.go +++ b/backend/api/internal/logic/platform/account.go @@ -54,7 +54,7 @@ func CreateGasAccount(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, resourceResponse(account)) + respondCreatedResource(ctx, account) } func UpdateGasAccount(ctx *gin.Context) { @@ -95,7 +95,7 @@ func CreateDeliveryAccount(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, resourceResponse(account)) + respondCreatedResource(ctx, account) } func UpdateDeliveryAccount(ctx *gin.Context) { diff --git a/backend/api/internal/logic/platform/delivery.go b/backend/api/internal/logic/platform/delivery.go index 9547528..0d3f545 100644 --- a/backend/api/internal/logic/platform/delivery.go +++ b/backend/api/internal/logic/platform/delivery.go @@ -37,7 +37,7 @@ func CreateDeliveryBasic(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, resourceResponse(delivery)) + respondCreatedResource(ctx, delivery) } // UpdateDeliveryBasic 更新配送点基础资料。 diff --git a/backend/api/internal/logic/platform/gas.go b/backend/api/internal/logic/platform/gas.go index 606cf5d..ee1e594 100644 --- a/backend/api/internal/logic/platform/gas.go +++ b/backend/api/internal/logic/platform/gas.go @@ -26,7 +26,7 @@ func CreateGasBasic(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, request) + respondCreatedResource(ctx, request) } // UpdateGasBasic 更新可燃气体站基础资料。 diff --git a/backend/api/internal/logic/platform/platform.go b/backend/api/internal/logic/platform/platform.go index 5235a8c..b90ca20 100644 --- a/backend/api/internal/logic/platform/platform.go +++ b/backend/api/internal/logic/platform/platform.go @@ -3,6 +3,8 @@ package platform import ( "errors" + "reflect" + "strings" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" @@ -38,7 +40,8 @@ func listPage[T any](ctx *gin.Context) { page, size := pageSize(ctx) var list []T var total int64 - databaseQuery := impl.DBService.Model(new(T)) + model := new(T) + databaseQuery := applyKeywordFilter(ctx, impl.DBService.Model(model), model) if err := databaseQuery.Count(&total).Error; err != nil { infra.Response.Error(ctx, err) return @@ -55,6 +58,58 @@ func listPage[T any](ctx *gin.Context) { infra.Response.Success(ctx, gin.H{"total": total, "list": response}) } +var keywordExcludedColumns = map[string]bool{ + "identity": true, "status": true, "password_hash": true, + "longitude": true, "latitude": true, "payload": true, + "before_data": true, "after_data": true, +} + +func applyKeywordFilter(ctx *gin.Context, query *gorm.DB, model any) *gorm.DB { + keyword := strings.ToLower(strings.TrimSpace(ctx.Query("keyword"))) + if keyword == "" { + return query + } + columns := keywordColumns(model) + if len(columns) == 0 { + return query + } + conditions := make([]string, 0, len(columns)) + arguments := make([]any, 0, len(columns)) + for _, column := range columns { + conditions = append(conditions, `LOWER("`+column+`") LIKE ?`) + arguments = append(arguments, "%"+keyword+"%") + } + return query.Where("("+strings.Join(conditions, " OR ")+")", arguments...) +} + +func keywordColumns(model any) []string { + modelType := reflect.TypeOf(model) + for modelType.Kind() == reflect.Pointer { + modelType = modelType.Elem() + } + columns := make([]string, 0) + for index := 0; index < modelType.NumField(); index++ { + field := modelType.Field(index) + if field.Anonymous || field.Type.Kind() != reflect.String { + continue + } + column := gormColumn(field.Tag.Get("gorm")) + if column != "" && !keywordExcludedColumns[column] { + columns = append(columns, column) + } + } + return columns +} + +func gormColumn(tag string) string { + for _, part := range strings.Split(tag, ";") { + if strings.HasPrefix(part, "column:") { + return strings.TrimPrefix(part, "column:") + } + } + return "" +} + func getByIdentity[T any](ctx *gin.Context) { var data T if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&data).Error; err != nil { diff --git a/backend/api/internal/logic/platform/resource.go b/backend/api/internal/logic/platform/resource.go index 816f8e1..9e1cb88 100644 --- a/backend/api/internal/logic/platform/resource.go +++ b/backend/api/internal/logic/platform/resource.go @@ -77,7 +77,7 @@ func ExpectedResources() []ResourceContract { 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("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", 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"), resourceContract("content", "cnt_content", Writable, "list"), resourceContract("notification", "ntf_template", Writable, "list"), resourceContract("customer_service", "cs_ticket", Writable, "list"), resourceContract("platform", "platfrom_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", Writable, "tree"), diff --git a/backend/api/internal/logic/platform/resource_test.go b/backend/api/internal/logic/platform/resource_test.go index 55b7162..08b836d 100644 --- a/backend/api/internal/logic/platform/resource_test.go +++ b/backend/api/internal/logic/platform/resource_test.go @@ -27,6 +27,7 @@ func TestExpectedResources(t *testing.T) { 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 TestResourceDefinitionAllowsOnlySupportedMethods(t *testing.T) { @@ -238,10 +239,37 @@ func TestCreateGasAccountResolvesGasBasicIdentityBeforePersisting(t *testing.T) mock.ExpectQuery(`INSERT INTO "gas_account"`). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(1))) mock.ExpectCommit() + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1)`)). + WithArgs(uint64(8)). + WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(8), "gas-a")) ctx, recorder := updateContext(http.MethodPost, "/gas/gas_account", "", []byte(`{"username":"operator","password":"password-123","gas_basic_identity":"gas-a"}`)) CreateGasAccount(ctx) + assertResponseCode(t, recorder, 0) + body := recorder.Body.String() + if !strings.Contains(body, `"identity"`) || !strings.Contains(body, `"gas_basic_identity":"gas-a"`) { + t.Fatalf("create response omitted public identities: %s", body) + } + if strings.Contains(body, `"gas_basic_id"`) || strings.Contains(body, `"password_hash"`) { + t.Fatalf("create response exposed internal or sensitive fields: %s", body) + } + assertMockExpectations(t, mock) +} + +func TestListGasAccountAppliesKeywordToCountAndRows(t *testing.T) { + _, mock := setupPlatformRoleDatabase(t) + keywordWhere := ` WHERE (LOWER("username") LIKE $1 OR LOWER("display_name") LIKE $2 OR LOWER("role_code") LIKE $3)` + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "gas_account"`+keywordWhere)). + WithArgs("%operator%", "%operator%", "%operator%"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "gas_account"`+keywordWhere+` ORDER BY created_at desc LIMIT $4`)). + WithArgs("%operator%", "%operator%", "%operator%", 20). + WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "gas_basic_id", "username", "display_name", "password_hash", "role_code"})) + + ctx, recorder := updateContext(http.MethodGet, "/gas/gas_account?keyword=Operator", "", nil) + ListGasAccount(ctx) + assertResponseCode(t, recorder, 0) assertMockExpectations(t, mock) } @@ -276,6 +304,30 @@ func TestResourceResponseDoesNotExposeAutoIncrementRelationIDs(t *testing.T) { } } +func TestCreatedResourceResponseMasksSensitiveFieldsAndKeepsIdentity(t *testing.T) { + response := maskCreatedSensitiveFields(map[string]any{ + "identity": "user-a", + "phone": "13800138000", + "real_name": "张三", + "credential_no": "CERT-123456", + "longitude": "120.123456", + "latitude": "30.456789", + }) + encoded, err := json.Marshal(response) + if err != nil { + t.Fatal(err) + } + body := string(encoded) + if !strings.Contains(body, `"identity":"user-a"`) || !strings.Contains(body, `"phone_masked":"138****8000"`) { + t.Fatalf("created response omitted identity or masked phone: %s", body) + } + for _, forbidden := range []string{`"phone":`, `"real_name":`, `"credential_no":`, `"longitude":`, `"latitude":`, "张三", "CERT-123456", "120.123456", "30.456789"} { + if strings.Contains(body, forbidden) { + t.Fatalf("created response exposed sensitive field %s: %s", forbidden, body) + } + } +} + func TestListSafetyEventDisposalsReturnsOnlyTheRequestedEventHistory(t *testing.T) { _, mock := setupPlatformRoleDatabase(t) mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "saf_event_disposal" WHERE saf_event_identity = $1`)). @@ -392,6 +444,55 @@ func TestGetDeliveryTrackOrdersAndMasksPointsWithoutPreciseLocationScope(t *test assertMockExpectations(t, mock) } +func TestListDeliveryTrackPointsMasksCoordinatesWithoutPreciseLocationScope(t *testing.T) { + _, mock := setupPlatformRoleDatabase(t) + now := time.Now().UTC() + mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "delivery_track_point"`)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "delivery_track_point" ORDER BY created_at desc LIMIT $1`)). + WithArgs(20). + WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "delivery_track_id", "point_type", "occurred_at", "longitude", "latitude"}). + AddRow(uint64(9), "point-a", now, now, "enabled", 1, uint64(7), "arrival", now, "120.123456", "30.456789")) + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "delivery_track" WHERE id IN ($1)`)). + WithArgs(uint64(7)). + WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(7), "track-a")) + + ctx, recorder := updateContext(http.MethodGet, "/delivery/delivery_track_point", "", nil) + listResource(ctx, &models.DeliveryTrackPoint{}) + + assertResponseCode(t, recorder, 0) + body := recorder.Body.String() + if strings.Contains(body, "120.123456") || strings.Contains(body, "30.456789") { + t.Fatalf("track-point list exposed precise coordinates without scope: %s", body) + } + if !strings.Contains(body, `"delivery_track_identity":"track-a"`) { + t.Fatalf("track-point list omitted its public relation identity: %s", body) + } + assertMockExpectations(t, mock) +} + +func TestGetDeliveryTrackPointReturnsCoordinatesWithPreciseLocationScope(t *testing.T) { + _, mock := setupPlatformRoleDatabase(t) + now := time.Now().UTC() + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "delivery_track_point" WHERE identity = $1 ORDER BY "delivery_track_point"."id" LIMIT $2`)). + WithArgs("point-a", 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "delivery_track_id", "point_type", "occurred_at", "longitude", "latitude"}). + AddRow(uint64(9), "point-a", now, now, "enabled", 1, uint64(7), "arrival", now, "120.123456", "30.456789")) + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "delivery_track" WHERE id IN ($1)`)). + WithArgs(uint64(7)). + WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(7), "track-a")) + + ctx, recorder := updateContext(http.MethodGet, "/delivery/delivery_track_point/point-a", "point-a", nil) + ctx.Set("Auth", &types.JwtClaims{Extend: map[string]string{"location_scope": "precise"}}) + getResource(ctx, &models.DeliveryTrackPoint{}) + + assertResponseCode(t, recorder, 0) + if !strings.Contains(recorder.Body.String(), "120.123456") || !strings.Contains(recorder.Body.String(), "30.456789") { + t.Fatalf("authorized track-point detail omitted precise coordinates: %s", recorder.Body.String()) + } + assertMockExpectations(t, mock) +} + func TestDisposeSafetyEventUpdatesEventAndAppendsOperatorActionTransactionally(t *testing.T) { _, mock := setupPlatformRoleDatabase(t) now := time.Now().UTC() diff --git a/backend/api/internal/logic/platform/role.go b/backend/api/internal/logic/platform/role.go index 1edbd62..33f078e 100644 --- a/backend/api/internal/logic/platform/role.go +++ b/backend/api/internal/logic/platform/role.go @@ -35,7 +35,7 @@ func CreatePlatformRole(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, request) + respondCreatedResource(ctx, request) } // UpdatePlatformRole 更新非内置平台角色。 @@ -125,7 +125,7 @@ func CreatePlatformMenu(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, menu) + respondCreatedResource(ctx, menu) } func UpdatePlatformMenu(ctx *gin.Context) { @@ -243,8 +243,14 @@ func ListPlatformMenu(ctx *gin.Context) { // ListPlatfromAccount 查询平台账号列表,手机号在展示层脱敏。 func ListPlatfromAccount(ctx *gin.Context) { page, size := pageSize(ctx) - list, total, err := models.ListPlatfromAccount(page, size) - if err != nil { + var list []models.PlatfromAccount + var total int64 + query := applyKeywordFilter(ctx, impl.DBService.Model(&models.PlatfromAccount{}), &models.PlatfromAccount{}) + if err := query.Count(&total).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + if err := query.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil { infra.Response.Error(ctx, err) return } diff --git a/backend/api/internal/logic/platform/secondary_resource.go b/backend/api/internal/logic/platform/secondary_resource.go index 0f8ec18..534ca60 100644 --- a/backend/api/internal/logic/platform/secondary_resource.go +++ b/backend/api/internal/logic/platform/secondary_resource.go @@ -35,7 +35,7 @@ func CreateStaffCredential(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, resourceResponse(credential)) + respondCreatedResource(ctx, credential) } func UpdateStaffCredential(ctx *gin.Context) { var request staffCredentialRequest @@ -77,7 +77,7 @@ func CreateUserAddress(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, resourceResponse(address)) + respondCreatedResource(ctx, address) } func UpdateUserAddress(ctx *gin.Context) { var request userAddressRequest @@ -133,7 +133,7 @@ func CreateUserServiceRelation(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, resourceResponse(relation)) + respondCreatedResource(ctx, relation) } func UpdateUserServiceRelation(ctx *gin.Context) { var request userServiceRelationRequest diff --git a/backend/api/internal/logic/platform/staff.go b/backend/api/internal/logic/platform/staff.go index bea9fa6..b40b499 100644 --- a/backend/api/internal/logic/platform/staff.go +++ b/backend/api/internal/logic/platform/staff.go @@ -54,7 +54,7 @@ func CreateStaff(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, staff) + respondCreatedResource(ctx, staff) } // UpdateStaff 更新服务人员档案。 diff --git a/backend/api/internal/logic/platform/task4_resources.go b/backend/api/internal/logic/platform/task4_resources.go index b7582f3..09a86d4 100644 --- a/backend/api/internal/logic/platform/task4_resources.go +++ b/backend/api/internal/logic/platform/task4_resources.go @@ -94,7 +94,7 @@ func listResource(ctx *gin.Context, model any) { page, size := pageSize(ctx) list := reflect.New(reflect.SliceOf(reflect.TypeOf(model).Elem())) var total int64 - query := impl.DBService.Model(model) + query := applyKeywordFilter(ctx, impl.DBService.Model(model), model) if err := query.Count(&total).Error; err != nil { infra.Response.Error(ctx, err) return @@ -108,7 +108,7 @@ func listResource(ctx *gin.Context, model any) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, gin.H{"total": total, "list": response}) + infra.Response.Success(ctx, gin.H{"total": total, "list": protectPreciseLocation(ctx, model, response)}) } func getResource(ctx *gin.Context, model any) { @@ -122,7 +122,7 @@ func getResource(ctx *gin.Context, model any) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, response) + infra.Response.Success(ctx, protectPreciseLocation(ctx, model, response)) } func createResource(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) { @@ -146,7 +146,88 @@ func createResource(ctx *gin.Context, model any, allowedFields []string, relatio infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, resourceResponse(data.Interface())) + respondCreatedResource(ctx, data.Interface()) +} + +func respondCreatedResource(ctx *gin.Context, value any) { + response, err := publicResourceResponse(value) + if err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, maskCreatedSensitiveFields(response)) +} + +func maskCreatedSensitiveFields(value any) any { + switch data := value.(type) { + case map[string]any: + if phone, ok := data["phone"].(string); ok { + data["phone_masked"] = maskPhone(phone) + delete(data, "phone") + } + if realName, ok := data["real_name"].(string); ok { + data["real_name_masked"] = maskSecret(realName) + delete(data, "real_name") + } + if credentialNo, ok := data["credential_no"].(string); ok { + data["credential_no_masked"] = maskSecret(credentialNo) + delete(data, "credential_no") + } + delete(data, "longitude") + delete(data, "latitude") + for key, item := range data { + data[key] = maskCreatedSensitiveFields(item) + } + case []any: + for index := range data { + data[index] = maskCreatedSensitiveFields(data[index]) + } + } + return value +} + +func maskSecret(value string) string { + characters := []rune(value) + if len(characters) <= 1 { + return "*" + } + visible := 1 + if len(characters) > 4 { + visible = 4 + } + return strings.Repeat("*", len(characters)-visible) + string(characters[len(characters)-visible:]) +} + +func protectPreciseLocation(ctx *gin.Context, model, value any) any { + if reflect.TypeOf(model) != reflect.TypeOf(&models.DeliveryTrackPoint{}) || hasPreciseLocationScope(ctx) { + return value + } + clearCoordinateFields(value) + return value +} + +func hasPreciseLocationScope(ctx *gin.Context) bool { + claims, err := middleware.ParseAuth(ctx) + return err == nil && claims.Extend["location_scope"] == "precise" +} + +func clearCoordinateFields(value any) { + switch data := value.(type) { + case map[string]any: + if _, ok := data["longitude"]; ok { + data["longitude"] = "" + } + if _, ok := data["latitude"]; ok { + data["latitude"] = "" + } + for _, item := range data { + clearCoordinateFields(item) + } + case []any: + for _, item := range data { + clearCoordinateFields(item) + } + } } func updateResource(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) { @@ -521,11 +602,7 @@ func GetDeliveryTrack(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - precise := false - if claims, err := middleware.ParseAuth(ctx); err == nil { - precise = claims.Extend["location_scope"] == "precise" - } - if !precise { + if !hasPreciseLocationScope(ctx) { for index := range points { points[index].Longitude = "" points[index].Latitude = "" diff --git a/backend/api/internal/logic/platform/user.go b/backend/api/internal/logic/platform/user.go index 06fb217..a47090e 100644 --- a/backend/api/internal/logic/platform/user.go +++ b/backend/api/internal/logic/platform/user.go @@ -38,7 +38,7 @@ func CreateUser(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, user) + respondCreatedResource(ctx, user) } // UpdateUser 更新业主客户档案。 diff --git a/backend/api/internal/models/aud_approval.go b/backend/api/internal/models/aud_approval.go index 3129044..e082c40 100644 --- a/backend/api/internal/models/aud_approval.go +++ b/backend/api/internal/models/aud_approval.go @@ -8,13 +8,13 @@ import ( // AudApproval 对应 aud_approval,保存审批流与复核意见。 type AudApproval struct { - Entity - BusinessType string `gorm:"column:business_type;type:varchar(64);not null" json:"business_type"` - BusinessIdentity string `gorm:"column:business_identity;type:varchar(36);not null;index" json:"business_identity"` - ApplicantIdentity string `gorm:"column:applicant_identity;type:varchar(36);not null;index" json:"applicant_identity"` - Opinion string `gorm:"column:opinion;type:text;not null;default:''" json:"opinion"` - HandlerIdentity string `gorm:"column:handler_identity;type:varchar(36);not null;default:'';index" json:"handler_identity"` - HandledAt *time.Time `gorm:"column:handled_at;type:timestamptz" json:"handled_at"` + 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 业务字段 + ApplicantIdentity string `gorm:"column:applicant_identity;type:varchar(36);not null;index" json:"applicant_identity"` // applicant_identity 业务字段 + Opinion string `gorm:"column:opinion;type:text;not null;default:''" json:"opinion"` // opinion 业务字段 + HandlerIdentity string `gorm:"column:handler_identity;type:varchar(36);not null;default:'';index" json:"handler_identity"` // handler_identity 业务字段 + HandledAt *time.Time `gorm:"column:handled_at;type:timestamptz" json:"handled_at"` // handled_at 业务字段 } func init() { database.AppendMigrate(&AudApproval{}) } diff --git a/backend/api/internal/models/aud_export_log.go b/backend/api/internal/models/aud_export_log.go index 6374886..97b11b7 100644 --- a/backend/api/internal/models/aud_export_log.go +++ b/backend/api/internal/models/aud_export_log.go @@ -7,12 +7,12 @@ import ( // AudExportLog 对应 aud_export_log,保存敏感导出审计。 type AudExportLog struct { - Entity - ApplicantIdentity string `gorm:"column:applicant_identity;type:varchar(36);not null;index" json:"applicant_identity"` - Purpose string `gorm:"column:purpose;type:varchar(255);not null" json:"purpose"` - FieldScope string `gorm:"column:field_scope;type:jsonb;not null;default:'{}'" json:"field_scope"` - ApprovedAt *time.Time `gorm:"column:approved_at;type:timestamptz" json:"approved_at"` - FileURI string `gorm:"column:file_uri;type:varchar(512);not null;default:''" json:"file_uri"` + 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 业务字段 + FieldScope string `gorm:"column:field_scope;type:jsonb;not null;default:'{}'" json:"field_scope"` // field_scope 业务字段 + ApprovedAt *time.Time `gorm:"column:approved_at;type:timestamptz" json:"approved_at"` // approved_at 业务字段 + FileURI string `gorm:"column:file_uri;type:varchar(512);not null;default:''" json:"file_uri"` // file_uri 业务字段 } func init() { database.AppendMigrate(&AudExportLog{}) } diff --git a/backend/api/internal/models/aud_operation_log.go b/backend/api/internal/models/aud_operation_log.go index 2aaca45..c7fb70c 100644 --- a/backend/api/internal/models/aud_operation_log.go +++ b/backend/api/internal/models/aud_operation_log.go @@ -4,13 +4,13 @@ import "git.apinb.com/bsm-sdk/core/database" // AudOperationLog 对应 aud_operation_log,保存不可变操作审计。 type AudOperationLog struct { - Entity - OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;index" json:"operator_identity"` - Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` - ObjectType string `gorm:"column:object_type;type:varchar(64);not null" json:"object_type"` - ObjectIdentity string `gorm:"column:object_identity;type:varchar(36);not null;index" json:"object_identity"` - BeforeData string `gorm:"column:before_data;type:jsonb;not null;default:'{}'" json:"before_data"` - AfterData string `gorm:"column:after_data;type:jsonb;not null;default:'{}'" json:"after_data"` + 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 业务字段 + ObjectType string `gorm:"column:object_type;type:varchar(64);not null" json:"object_type"` // object_type 业务字段 + ObjectIdentity string `gorm:"column:object_identity;type:varchar(36);not null;index" json:"object_identity"` // object_identity 业务字段 + BeforeData string `gorm:"column:before_data;type:jsonb;not null;default:'{}'" json:"before_data"` // before_data 业务字段 + AfterData string `gorm:"column:after_data;type:jsonb;not null;default:'{}'" json:"after_data"` // after_data 业务字段 } func init() { database.AppendMigrate(&AudOperationLog{}) } diff --git a/backend/api/internal/models/cnt_content.go b/backend/api/internal/models/cnt_content.go index ccaefc6..b81c67e 100644 --- a/backend/api/internal/models/cnt_content.go +++ b/backend/api/internal/models/cnt_content.go @@ -4,12 +4,12 @@ import "git.apinb.com/bsm-sdk/core/database" // CntContent 对应 cnt_content,保存公告与协议内容。 type CntContent struct { - Entity - ContentType string `gorm:"column:content_type;type:varchar(32);not null" json:"content_type"` - Title string `gorm:"column:title;type:varchar(256);not null" json:"title"` - Body string `gorm:"column:body;type:text;not null;default:''" json:"body"` - VersionNo int `gorm:"column:version_no;not null;default:1" json:"version_no"` - PublishStatus string `gorm:"column:publish_status;type:varchar(32);not null;default:'draft'" json:"publish_status"` + Entity // 公共实体字段 + ContentType string `gorm:"column:content_type;type:varchar(32);not null" json:"content_type"` // content_type 业务字段 + Title string `gorm:"column:title;type:varchar(256);not null" json:"title"` // title 业务字段 + Body string `gorm:"column:body;type:text;not null;default:''" json:"body"` // body 业务字段 + VersionNo int `gorm:"column:version_no;not null;default:1" json:"version_no"` // version_no 业务字段 + PublishStatus string `gorm:"column:publish_status;type:varchar(32);not null;default:'draft'" json:"publish_status"` // publish_status 业务字段 } func init() { database.AppendMigrate(&CntContent{}) } diff --git a/backend/api/internal/models/comments_test.go b/backend/api/internal/models/comments_test.go new file mode 100644 index 0000000..3f782c6 --- /dev/null +++ b/backend/api/internal/models/comments_test.go @@ -0,0 +1,53 @@ +package models + +import ( + "go/ast" + "go/parser" + "go/token" + "os" + "regexp" + "testing" +) + +var chineseText = regexp.MustCompile(`[\p{Han}]`) + +func TestEveryModelFieldHasChineseComment(t *testing.T) { + packages, err := parser.ParseDir(token.NewFileSet(), ".", func(info os.FileInfo) bool { + return info.Name() != "comments_test.go" + }, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + for _, file := range packages["models"].Files { + ast.Inspect(file, func(node ast.Node) bool { + typeSpec, ok := node.(*ast.TypeSpec) + if !ok { + return true + } + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + return false + } + for _, field := range structType.Fields.List { + name := "embedded field" + if len(field.Names) > 0 { + name = field.Names[0].Name + if !ast.IsExported(name) { + continue + } + } + comment := "" + if field.Doc != nil { + comment += field.Doc.Text() + } + if field.Comment != nil { + comment += field.Comment.Text() + } + if !chineseText.MatchString(comment) { + t.Errorf("%s.%s 缺少中文字段注释", typeSpec.Name.Name, name) + } + } + return false + }) + } +} diff --git a/backend/api/internal/models/cs_ticket.go b/backend/api/internal/models/cs_ticket.go index e8a1f51..ed6cc0e 100644 --- a/backend/api/internal/models/cs_ticket.go +++ b/backend/api/internal/models/cs_ticket.go @@ -4,11 +4,11 @@ import "git.apinb.com/bsm-sdk/core/database" // CsTicket 对应 cs_ticket,保存客服工单。 type CsTicket struct { - Entity - TicketNo string `gorm:"column:ticket_no;type:varchar(64);not null;uniqueIndex" json:"ticket_no"` - UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` - Category string `gorm:"column:category;type:varchar(64);not null" json:"category"` - Priority string `gorm:"column:priority;type:varchar(16);not null;default:'normal'" json:"priority"` + Entity // 公共实体字段 + TicketNo string `gorm:"column:ticket_no;type:varchar(64);not null;uniqueIndex" json:"ticket_no"` // ticket_no 业务字段 + UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段 + Category string `gorm:"column:category;type:varchar(64);not null" json:"category"` // category 业务字段 + Priority string `gorm:"column:priority;type:varchar(16);not null;default:'normal'" json:"priority"` // priority 业务字段 } func init() { database.AppendMigrate(&CsTicket{}) } diff --git a/backend/api/internal/models/delivery_account.go b/backend/api/internal/models/delivery_account.go index e093c0c..ac60372 100644 --- a/backend/api/internal/models/delivery_account.go +++ b/backend/api/internal/models/delivery_account.go @@ -4,12 +4,12 @@ import "git.apinb.com/bsm-sdk/core/database" // DeliveryAccount 对应 delivery_account,保存配送点登录账户。 type DeliveryAccount struct { - Entity - DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;index" json:"delivery_basic_id"` - Username string `gorm:"column:username;type:varchar(64);not null;uniqueIndex" json:"username"` - DisplayName string `gorm:"column:display_name;type:varchar(64);not null;default:''" json:"display_name"` - PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null" json:"-"` - RoleCode string `gorm:"column:role_code;type:varchar(64);not null" json:"role_code"` + Entity // 公共实体字段 + DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;index" json:"delivery_basic_id"` // delivery_basic_id 业务字段 + Username string `gorm:"column:username;type:varchar(64);not null;uniqueIndex" json:"username"` // username 业务字段 + DisplayName string `gorm:"column:display_name;type:varchar(64);not null;default:''" json:"display_name"` // display_name 业务字段 + PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null" json:"-"` // PasswordHash 业务字段 + RoleCode string `gorm:"column:role_code;type:varchar(64);not null" json:"role_code"` // role_code 业务字段 } func init() { database.AppendMigrate(&DeliveryAccount{}) } diff --git a/backend/api/internal/models/delivery_basic.go b/backend/api/internal/models/delivery_basic.go index 4746829..046c727 100644 --- a/backend/api/internal/models/delivery_basic.go +++ b/backend/api/internal/models/delivery_basic.go @@ -4,7 +4,7 @@ import "git.apinb.com/bsm-sdk/core/database" // DeliveryBasic 对应 delivery_basic,保存配送点主档案。 type DeliveryBasic struct { - Entity + Entity // 公共实体字段 DeliveryCode string `gorm:"column:delivery_code;type:varchar(32);not null;uniqueIndex" json:"delivery_code"` // 配送点编码 GasBasicID uint64 `gorm:"column:gas_basic_id;not null;default:0;index" json:"gas_basic_id"` // 所属可燃气体站自增主键,0 表示平台直属 Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // 配送点名称 diff --git a/backend/api/internal/models/delivery_task.go b/backend/api/internal/models/delivery_task.go index 1479178..9dd4d14 100644 --- a/backend/api/internal/models/delivery_task.go +++ b/backend/api/internal/models/delivery_task.go @@ -4,10 +4,10 @@ import "git.apinb.com/bsm-sdk/core/database" // DeliveryTask 对应 delivery_task,保存配送履约任务。 type DeliveryTask struct { - Entity - EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` - StaffAccountID uint64 `gorm:"column:staff_account_id;not null;default:0;index" json:"staff_account_id"` - DeliveryPointID uint64 `gorm:"column:delivery_point_id;not null;index" json:"delivery_point_id"` + Entity // 公共实体字段 + EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段 + StaffAccountID uint64 `gorm:"column:staff_account_id;not null;default:0;index" json:"staff_account_id"` // staff_account_id 业务字段 + DeliveryPointID uint64 `gorm:"column:delivery_point_id;not null;index" json:"delivery_point_id"` // delivery_point_id 业务字段 } func init() { database.AppendMigrate(&DeliveryTask{}) } diff --git a/backend/api/internal/models/delivery_track.go b/backend/api/internal/models/delivery_track.go index bddf6aa..6af10d1 100644 --- a/backend/api/internal/models/delivery_track.go +++ b/backend/api/internal/models/delivery_track.go @@ -7,10 +7,10 @@ import ( // DeliveryTrack 对应 delivery_track,保存配送轨迹摘要。 type DeliveryTrack struct { - Entity - DeliveryTaskID uint64 `gorm:"column:delivery_task_id;not null;index" json:"delivery_task_id"` - StartedAt *time.Time `gorm:"column:started_at;type:timestamptz" json:"started_at"` - CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` + Entity // 公共实体字段 + DeliveryTaskID uint64 `gorm:"column:delivery_task_id;not null;index" json:"delivery_task_id"` // delivery_task_id 业务字段 + StartedAt *time.Time `gorm:"column:started_at;type:timestamptz" json:"started_at"` // started_at 业务字段 + CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // completed_at 业务字段 } func init() { database.AppendMigrate(&DeliveryTrack{}) } diff --git a/backend/api/internal/models/delivery_track_point.go b/backend/api/internal/models/delivery_track_point.go index 3ad0282..4e8f446 100644 --- a/backend/api/internal/models/delivery_track_point.go +++ b/backend/api/internal/models/delivery_track_point.go @@ -7,12 +7,12 @@ import ( // DeliveryTrackPoint 对应 delivery_track_point,保存配送节点和位置。 type DeliveryTrackPoint struct { - Entity - DeliveryTrackID uint64 `gorm:"column:delivery_track_id;not null;index" json:"delivery_track_id"` - PointType string `gorm:"column:point_type;type:varchar(32);not null" json:"point_type"` - OccurredAt time.Time `gorm:"column:occurred_at;type:timestamptz;not null" json:"occurred_at"` - Longitude string `gorm:"column:longitude;type:varchar(32);not null;default:''" json:"longitude"` - Latitude string `gorm:"column:latitude;type:varchar(32);not null;default:''" json:"latitude"` + Entity // 公共实体字段 + DeliveryTrackID uint64 `gorm:"column:delivery_track_id;not null;index" json:"delivery_track_id"` // delivery_track_id 业务字段 + PointType string `gorm:"column:point_type;type:varchar(32);not null" json:"point_type"` // point_type 业务字段 + OccurredAt time.Time `gorm:"column:occurred_at;type:timestamptz;not null" json:"occurred_at"` // occurred_at 业务字段 + Longitude string `gorm:"column:longitude;type:varchar(32);not null;default:''" json:"longitude"` // longitude 业务字段 + Latitude string `gorm:"column:latitude;type:varchar(32);not null;default:''" json:"latitude"` // latitude 业务字段 } func init() { database.AppendMigrate(&DeliveryTrackPoint{}) } diff --git a/backend/api/internal/models/dev_device_binding.go b/backend/api/internal/models/dev_device_binding.go index 4b2afb6..cbffcb6 100644 --- a/backend/api/internal/models/dev_device_binding.go +++ b/backend/api/internal/models/dev_device_binding.go @@ -7,11 +7,11 @@ import ( // DevDeviceBinding 对应 dev_device_binding,保存设备授权绑定。 type DevDeviceBinding struct { - Entity - SmartCylinderValveID uint64 `gorm:"column:smart_cylinder_valve_id;not null;index" json:"smart_cylinder_valve_id"` - UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` - EffectiveAt time.Time `gorm:"column:effective_at;type:timestamptz;not null" json:"effective_at"` - ExpiredAt *time.Time `gorm:"column:expired_at;type:timestamptz" json:"expired_at"` + Entity // 公共实体字段 + SmartCylinderValveID uint64 `gorm:"column:smart_cylinder_valve_id;not null;index" json:"smart_cylinder_valve_id"` // smart_cylinder_valve_id 业务字段 + UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段 + EffectiveAt time.Time `gorm:"column:effective_at;type:timestamptz;not null" json:"effective_at"` // effective_at 业务字段 + ExpiredAt *time.Time `gorm:"column:expired_at;type:timestamptz" json:"expired_at"` // expired_at 业务字段 } func init() { database.AppendMigrate(&DevDeviceBinding{}) } diff --git a/backend/api/internal/models/dev_smart_cylinder_valve.go b/backend/api/internal/models/dev_smart_cylinder_valve.go index ad89771..a824531 100644 --- a/backend/api/internal/models/dev_smart_cylinder_valve.go +++ b/backend/api/internal/models/dev_smart_cylinder_valve.go @@ -4,11 +4,11 @@ import "git.apinb.com/bsm-sdk/core/database" // DevSmartCylinderValve 对应 dev_smart_cylinder_valve,保存智能瓶阀档案。 type DevSmartCylinderValve struct { - Entity - DeviceNo string `gorm:"column:device_no;type:varchar(64);not null;uniqueIndex" json:"device_no"` - Model string `gorm:"column:model;type:varchar(64);not null;default:''" json:"model"` - OnlineStatus string `gorm:"column:online_status;type:varchar(32);not null;default:'offline'" json:"online_status"` - OwnerIdentity string `gorm:"column:owner_identity;type:varchar(36);not null;default:'';index" json:"owner_identity"` + Entity // 公共实体字段 + DeviceNo string `gorm:"column:device_no;type:varchar(64);not null;uniqueIndex" json:"device_no"` // device_no 业务字段 + Model string `gorm:"column:model;type:varchar(64);not null;default:''" json:"model"` // model 业务字段 + OnlineStatus string `gorm:"column:online_status;type:varchar(32);not null;default:'offline'" json:"online_status"` // online_status 业务字段 + OwnerIdentity string `gorm:"column:owner_identity;type:varchar(36);not null;default:'';index" json:"owner_identity"` // owner_identity 业务字段 } func init() { database.AppendMigrate(&DevSmartCylinderValve{}) } diff --git a/backend/api/internal/models/dev_telemetry.go b/backend/api/internal/models/dev_telemetry.go index 1aea5fa..cbf2454 100644 --- a/backend/api/internal/models/dev_telemetry.go +++ b/backend/api/internal/models/dev_telemetry.go @@ -7,11 +7,11 @@ import ( // DevTelemetry 对应 dev_telemetry,保存设备遥测摘要。 type DevTelemetry struct { - Entity - SmartCylinderValveIdentity string `gorm:"column:smart_cylinder_valve_identity;type:varchar(36);not null;index" json:"smart_cylinder_valve_identity"` - ReportedAt time.Time `gorm:"column:reported_at;type:timestamptz;not null;index" json:"reported_at"` - Payload string `gorm:"column:payload;type:jsonb;not null;default:'{}'" json:"payload"` - QualityFlag string `gorm:"column:quality_flag;type:varchar(32);not null;default:'normal'" json:"quality_flag"` + Entity // 公共实体字段 + SmartCylinderValveIdentity string `gorm:"column:smart_cylinder_valve_identity;type:varchar(36);not null;index" json:"smart_cylinder_valve_identity"` // smart_cylinder_valve_identity 业务字段 + ReportedAt time.Time `gorm:"column:reported_at;type:timestamptz;not null;index" json:"reported_at"` // reported_at 业务字段 + Payload string `gorm:"column:payload;type:jsonb;not null;default:'{}'" json:"payload"` // payload 业务字段 + QualityFlag string `gorm:"column:quality_flag;type:varchar(32);not null;default:'normal'" json:"quality_flag"` // quality_flag 业务字段 } func init() { database.AppendMigrate(&DevTelemetry{}) } diff --git a/backend/api/internal/models/ec_cart.go b/backend/api/internal/models/ec_cart.go index 46623e4..067ead7 100644 --- a/backend/api/internal/models/ec_cart.go +++ b/backend/api/internal/models/ec_cart.go @@ -4,11 +4,11 @@ import "git.apinb.com/bsm-sdk/core/database" // EcCart 对应 ec_cart,保存用户购物车明细。 type EcCart struct { - Entity - UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` - EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` - Quantity int `gorm:"column:quantity;not null;default:1" json:"quantity"` - Selected bool `gorm:"column:selected;not null;default:true" json:"selected"` + Entity // 公共实体字段 + UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段 + EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` // ec_product_id 业务字段 + Quantity int `gorm:"column:quantity;not null;default:1" json:"quantity"` // quantity 业务字段 + Selected bool `gorm:"column:selected;not null;default:true" json:"selected"` // selected 业务字段 } func init() { database.AppendMigrate(&EcCart{}) } diff --git a/backend/api/internal/models/ec_category.go b/backend/api/internal/models/ec_category.go index 1b48ad1..ce2b0b3 100644 --- a/backend/api/internal/models/ec_category.go +++ b/backend/api/internal/models/ec_category.go @@ -4,10 +4,10 @@ import "git.apinb.com/bsm-sdk/core/database" // EcCategory 对应 ec_category,保存商品分类树。 type EcCategory struct { - Entity - ParentID uint64 `gorm:"column:parent_id;not null;default:0;index" json:"parent_id"` - Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` - SortNo int `gorm:"column:sort_no;not null;default:0" json:"sort_no"` + Entity // 公共实体字段 + ParentID uint64 `gorm:"column:parent_id;not null;default:0;index" json:"parent_id"` // parent_id 业务字段 + Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // name 业务字段 + SortNo int `gorm:"column:sort_no;not null;default:0" json:"sort_no"` // sort_no 业务字段 } func init() { database.AppendMigrate(&EcCategory{}) } diff --git a/backend/api/internal/models/ec_order.go b/backend/api/internal/models/ec_order.go index bb21b82..734ab8e 100644 --- a/backend/api/internal/models/ec_order.go +++ b/backend/api/internal/models/ec_order.go @@ -4,12 +4,12 @@ import "git.apinb.com/bsm-sdk/core/database" // EcOrder 对应 ec_order,保存电商订单与组织快照。 type EcOrder struct { - Entity - OrderNo string `gorm:"column:order_no;type:varchar(64);not null;uniqueIndex" json:"order_no"` - UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` - GasStationID uint64 `gorm:"column:gas_station_id;not null;default:0;index" json:"gas_station_id"` - DeliveryPointID uint64 `gorm:"column:delivery_point_id;not null;default:0;index" json:"delivery_point_id"` - TotalAmount int64 `gorm:"column:total_amount;not null;default:0" json:"total_amount"` + Entity // 公共实体字段 + OrderNo string `gorm:"column:order_no;type:varchar(64);not null;uniqueIndex" json:"order_no"` // order_no 业务字段 + UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段 + GasStationID uint64 `gorm:"column:gas_station_id;not null;default:0;index" json:"gas_station_id"` // gas_station_id 业务字段 + DeliveryPointID uint64 `gorm:"column:delivery_point_id;not null;default:0;index" json:"delivery_point_id"` // delivery_point_id 业务字段 + TotalAmount int64 `gorm:"column:total_amount;not null;default:0" json:"total_amount"` // total_amount 业务字段 } func init() { database.AppendMigrate(&EcOrder{}) } diff --git a/backend/api/internal/models/ec_order_item.go b/backend/api/internal/models/ec_order_item.go index 7d60a9d..88b62fb 100644 --- a/backend/api/internal/models/ec_order_item.go +++ b/backend/api/internal/models/ec_order_item.go @@ -4,12 +4,12 @@ import "git.apinb.com/bsm-sdk/core/database" // EcOrderItem 对应 ec_order_item,保存订单商品快照。 type EcOrderItem struct { - Entity - EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` - EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` - ProductSnapshot string `gorm:"column:product_snapshot;type:jsonb;not null;default:'{}'" json:"product_snapshot"` - Quantity int `gorm:"column:quantity;not null;default:1" json:"quantity"` - SaleAmount int64 `gorm:"column:sale_amount;not null;default:0" json:"sale_amount"` + Entity // 公共实体字段 + EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段 + EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` // ec_product_id 业务字段 + ProductSnapshot string `gorm:"column:product_snapshot;type:jsonb;not null;default:'{}'" json:"product_snapshot"` // product_snapshot 业务字段 + Quantity int `gorm:"column:quantity;not null;default:1" json:"quantity"` // quantity 业务字段 + SaleAmount int64 `gorm:"column:sale_amount;not null;default:0" json:"sale_amount"` // sale_amount 业务字段 } func init() { database.AppendMigrate(&EcOrderItem{}) } diff --git a/backend/api/internal/models/ec_product.go b/backend/api/internal/models/ec_product.go index 9f6b40e..518a219 100644 --- a/backend/api/internal/models/ec_product.go +++ b/backend/api/internal/models/ec_product.go @@ -4,12 +4,12 @@ import "git.apinb.com/bsm-sdk/core/database" // EcProduct 对应 ec_product,保存可燃气体商品与服务。 type EcProduct struct { - Entity - EcCategoryID uint64 `gorm:"column:ec_category_id;not null;index" json:"ec_category_id"` - ProductCode string `gorm:"column:product_code;type:varchar(64);not null;uniqueIndex" json:"product_code"` - Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` - PriceAmount int64 `gorm:"column:price_amount;not null;default:0" json:"price_amount"` - StockQuantity int `gorm:"column:stock_quantity;not null;default:0" json:"stock_quantity"` + Entity // 公共实体字段 + EcCategoryID uint64 `gorm:"column:ec_category_id;not null;index" json:"ec_category_id"` // ec_category_id 业务字段 + ProductCode string `gorm:"column:product_code;type:varchar(64);not null;uniqueIndex" json:"product_code"` // product_code 业务字段 + Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // name 业务字段 + PriceAmount int64 `gorm:"column:price_amount;not null;default:0" json:"price_amount"` // price_amount 业务字段 + StockQuantity int `gorm:"column:stock_quantity;not null;default:0" json:"stock_quantity"` // stock_quantity 业务字段 } func init() { database.AppendMigrate(&EcProduct{}) } diff --git a/backend/api/internal/models/ec_product_attribute.go b/backend/api/internal/models/ec_product_attribute.go index 56e3abb..bb0dd05 100644 --- a/backend/api/internal/models/ec_product_attribute.go +++ b/backend/api/internal/models/ec_product_attribute.go @@ -4,11 +4,11 @@ import "git.apinb.com/bsm-sdk/core/database" // EcProductAttribute 对应 ec_product_attribute,保存商品属性。 type EcProductAttribute struct { - Entity - EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` - Name string `gorm:"column:name;type:varchar(64);not null" json:"name"` - Value string `gorm:"column:value;type:varchar(255);not null" json:"value"` - SortNo int `gorm:"column:sort_no;not null;default:0" json:"sort_no"` + Entity // 公共实体字段 + EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` // ec_product_id 业务字段 + Name string `gorm:"column:name;type:varchar(64);not null" json:"name"` // name 业务字段 + Value string `gorm:"column:value;type:varchar(255);not null" json:"value"` // value 业务字段 + SortNo int `gorm:"column:sort_no;not null;default:0" json:"sort_no"` // sort_no 业务字段 } func init() { database.AppendMigrate(&EcProductAttribute{}) } diff --git a/backend/api/internal/models/ec_product_image.go b/backend/api/internal/models/ec_product_image.go index ec52f1b..0bf11e5 100644 --- a/backend/api/internal/models/ec_product_image.go +++ b/backend/api/internal/models/ec_product_image.go @@ -4,11 +4,11 @@ import "git.apinb.com/bsm-sdk/core/database" // EcProductImage 对应 ec_product_image,保存商品受控图片资源。 type EcProductImage struct { - Entity - EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` - ImageURI string `gorm:"column:image_uri;type:varchar(512);not null" json:"image_uri"` - SortNo int `gorm:"column:sort_no;not null;default:0" json:"sort_no"` - IsCover bool `gorm:"column:is_cover;not null;default:false" json:"is_cover"` + Entity // 公共实体字段 + EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` // ec_product_id 业务字段 + ImageURI string `gorm:"column:image_uri;type:varchar(512);not null" json:"image_uri"` // image_uri 业务字段 + SortNo int `gorm:"column:sort_no;not null;default:0" json:"sort_no"` // sort_no 业务字段 + IsCover bool `gorm:"column:is_cover;not null;default:false" json:"is_cover"` // is_cover 业务字段 } func init() { database.AppendMigrate(&EcProductImage{}) } diff --git a/backend/api/internal/models/ec_review.go b/backend/api/internal/models/ec_review.go index 5f83a85..ff5e932 100644 --- a/backend/api/internal/models/ec_review.go +++ b/backend/api/internal/models/ec_review.go @@ -4,12 +4,12 @@ import "git.apinb.com/bsm-sdk/core/database" // EcReview 对应 ec_review,保存商品评论与审核状态。 type EcReview struct { - Entity - EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` - EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` - UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` - Score int `gorm:"column:score;not null;default:5" json:"score"` - Content string `gorm:"column:content;type:text;not null;default:''" json:"content"` + Entity // 公共实体字段 + EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段 + EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` // ec_product_id 业务字段 + UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段 + Score int `gorm:"column:score;not null;default:5" json:"score"` // score 业务字段 + Content string `gorm:"column:content;type:text;not null;default:''" json:"content"` // content 业务字段 } func init() { database.AppendMigrate(&EcReview{}) } diff --git a/backend/api/internal/models/fin_payment.go b/backend/api/internal/models/fin_payment.go index cdc004a..b4df3ca 100644 --- a/backend/api/internal/models/fin_payment.go +++ b/backend/api/internal/models/fin_payment.go @@ -7,11 +7,11 @@ import ( // FinPayment 对应 fin_payment,保存支付与退款记录。 type FinPayment struct { - Entity - EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` - Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` - Amount int64 `gorm:"column:amount;not null;default:0" json:"amount"` - PaidAt *time.Time `gorm:"column:paid_at;type:timestamptz" json:"paid_at"` + Entity // 公共实体字段 + EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段 + Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // channel 业务字段 + Amount int64 `gorm:"column:amount;not null;default:0" json:"amount"` // amount 业务字段 + PaidAt *time.Time `gorm:"column:paid_at;type:timestamptz" json:"paid_at"` // paid_at 业务字段 } func init() { database.AppendMigrate(&FinPayment{}) } diff --git a/backend/api/internal/models/fin_reconciliation.go b/backend/api/internal/models/fin_reconciliation.go index 0369a46..f61df69 100644 --- a/backend/api/internal/models/fin_reconciliation.go +++ b/backend/api/internal/models/fin_reconciliation.go @@ -7,10 +7,10 @@ import ( // FinReconciliation 对应 fin_reconciliation,保存渠道对账记录。 type FinReconciliation struct { - Entity - Channel string `gorm:"column:channel;type:varchar(32);not null;index" json:"channel"` - BillDate time.Time `gorm:"column:bill_date;type:date;not null" json:"bill_date"` - DifferenceAmount int64 `gorm:"column:difference_amount;not null;default:0" json:"difference_amount"` + Entity // 公共实体字段 + Channel string `gorm:"column:channel;type:varchar(32);not null;index" json:"channel"` // channel 业务字段 + BillDate time.Time `gorm:"column:bill_date;type:date;not null" json:"bill_date"` // bill_date 业务字段 + DifferenceAmount int64 `gorm:"column:difference_amount;not null;default:0" json:"difference_amount"` // difference_amount 业务字段 } func init() { database.AppendMigrate(&FinReconciliation{}) } diff --git a/backend/api/internal/models/fin_settlement.go b/backend/api/internal/models/fin_settlement.go index 664c104..8d2ee3f 100644 --- a/backend/api/internal/models/fin_settlement.go +++ b/backend/api/internal/models/fin_settlement.go @@ -7,12 +7,12 @@ import ( // FinSettlement 对应 fin_settlement,保存结算单。 type FinSettlement struct { - Entity - SettlementNo string `gorm:"column:settlement_no;type:varchar(64);not null;uniqueIndex" json:"settlement_no"` - SubjectType string `gorm:"column:subject_type;type:varchar(32);not null" json:"subject_type"` - SubjectID uint64 `gorm:"column:subject_id;not null;index" json:"subject_id"` - PeriodStart time.Time `gorm:"column:period_start;type:timestamptz;not null" json:"period_start"` - PeriodEnd time.Time `gorm:"column:period_end;type:timestamptz;not null" json:"period_end"` + Entity // 公共实体字段 + SettlementNo string `gorm:"column:settlement_no;type:varchar(64);not null;uniqueIndex" json:"settlement_no"` // settlement_no 业务字段 + SubjectType string `gorm:"column:subject_type;type:varchar(32);not null" json:"subject_type"` // subject_type 业务字段 + SubjectID uint64 `gorm:"column:subject_id;not null;index" json:"subject_id"` // subject_id 业务字段 + PeriodStart time.Time `gorm:"column:period_start;type:timestamptz;not null" json:"period_start"` // period_start 业务字段 + PeriodEnd time.Time `gorm:"column:period_end;type:timestamptz;not null" json:"period_end"` // period_end 业务字段 } func init() { database.AppendMigrate(&FinSettlement{}) } diff --git a/backend/api/internal/models/gas_account.go b/backend/api/internal/models/gas_account.go index 5b1c3bd..bbc4c64 100644 --- a/backend/api/internal/models/gas_account.go +++ b/backend/api/internal/models/gas_account.go @@ -4,7 +4,7 @@ import "git.apinb.com/bsm-sdk/core/database" // GasAccount 对应 gas_account,保存可燃气体站登录账户。 type GasAccount struct { - Entity + Entity // 公共实体字段 GasBasicID uint64 `gorm:"column:gas_basic_id;not null;index" json:"gas_basic_id"` // 可燃气体站主键 Username string `gorm:"column:username;type:varchar(64);not null;uniqueIndex" json:"username"` // 登录名称 DisplayName string `gorm:"column:display_name;type:varchar(64);not null;default:''" json:"display_name"` // 展示名称 diff --git a/backend/api/internal/models/gas_basic.go b/backend/api/internal/models/gas_basic.go index 17db459..c6f81a6 100644 --- a/backend/api/internal/models/gas_basic.go +++ b/backend/api/internal/models/gas_basic.go @@ -4,7 +4,7 @@ import "git.apinb.com/bsm-sdk/core/database" // GasBasic 对应 gas_basic,保存可燃气体站的主体主档案。 type GasBasic struct { - Entity + Entity // 公共实体字段 Code string `gorm:"column:code;type:varchar(32);not null;uniqueIndex" json:"code"` // 站点编码 Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // 站点名称 CreditCode string `gorm:"column:credit_code;type:varchar(64);not null;default:''" json:"credit_code"` // 统一社会信用代码 diff --git a/backend/api/internal/models/ntf_template.go b/backend/api/internal/models/ntf_template.go index 3f5050d..c783d71 100644 --- a/backend/api/internal/models/ntf_template.go +++ b/backend/api/internal/models/ntf_template.go @@ -4,10 +4,10 @@ import "git.apinb.com/bsm-sdk/core/database" // NtfTemplate 对应 ntf_template,保存通知模板。 type NtfTemplate struct { - Entity - TemplateCode string `gorm:"column:template_code;type:varchar(64);not null;uniqueIndex" json:"template_code"` - Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` - Content string `gorm:"column:content;type:text;not null" json:"content"` + Entity // 公共实体字段 + TemplateCode string `gorm:"column:template_code;type:varchar(64);not null;uniqueIndex" json:"template_code"` // template_code 业务字段 + Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // channel 业务字段 + Content string `gorm:"column:content;type:text;not null" json:"content"` // content 业务字段 } func init() { database.AppendMigrate(&NtfTemplate{}) } diff --git a/backend/api/internal/models/platform_menu.go b/backend/api/internal/models/platform_menu.go index b875dde..2ca6268 100644 --- a/backend/api/internal/models/platform_menu.go +++ b/backend/api/internal/models/platform_menu.go @@ -4,7 +4,7 @@ import "git.apinb.com/bsm-sdk/core/database" // PlatformMenu 对应 platform_menu,定义平台总后台的菜单树和访问路由。 type PlatformMenu struct { - Entity + Entity // 公共实体字段 ParentID uint64 `gorm:"column:parent_id;not null;default:0;index" json:"parent_id"` // 父菜单自增主键,顶级菜单为 0 MenuCode string `gorm:"column:menu_code;type:varchar(64);not null;uniqueIndex" json:"menu_code"` // 菜单编码 Name string `gorm:"column:name;type:varchar(64);not null" json:"name"` // 菜单名称 diff --git a/backend/api/internal/models/platform_role.go b/backend/api/internal/models/platform_role.go index 6b08a8f..4e4625e 100644 --- a/backend/api/internal/models/platform_role.go +++ b/backend/api/internal/models/platform_role.go @@ -4,7 +4,7 @@ import "git.apinb.com/bsm-sdk/core/database" // PlatformRole 对应 platform_role,定义平台总后台的数据范围与菜单权限角色。 type PlatformRole struct { - Entity + Entity // 公共实体字段 RoleCode string `gorm:"column:role_code;type:varchar(64);not null;uniqueIndex" json:"role_code"` // 角色编码 Name string `gorm:"column:name;type:varchar(64);not null" json:"name"` // 角色名称 DataScope string `gorm:"column:data_scope;type:varchar(32);not null;default:'global'" json:"data_scope"` // 数据权限范围 diff --git a/backend/api/internal/models/platfrom_account.go b/backend/api/internal/models/platfrom_account.go index 6730616..2fd8bf6 100644 --- a/backend/api/internal/models/platfrom_account.go +++ b/backend/api/internal/models/platfrom_account.go @@ -4,7 +4,7 @@ import "git.apinb.com/bsm-sdk/core/database" // PlatfromAccount 对应 platfrom_account,表示平台总后台登录账号。 type PlatfromAccount struct { - Entity + Entity // 公共实体字段 Username string `gorm:"column:username;type:varchar(64);uniqueIndex;not null" json:"username"` // 登录用户名 DisplayName string `gorm:"column:display_name;type:varchar(64);not null;default:''" json:"display_name"` // 用户展示名称 Avatar string `gorm:"column:avatar;type:varchar(512);not null;default:''" json:"avatar"` // 头像资源地址 diff --git a/backend/api/internal/models/report.go b/backend/api/internal/models/report.go index 9093e42..e01d178 100644 --- a/backend/api/internal/models/report.go +++ b/backend/api/internal/models/report.go @@ -7,11 +7,11 @@ import ( // Report 对应 report,保存统计报表档案。 type Report struct { - Entity - ReportCode string `gorm:"column:report_code;type:varchar(64);not null;uniqueIndex" json:"report_code"` - ReportType string `gorm:"column:report_type;type:varchar(32);not null" json:"report_type"` - StatPeriod string `gorm:"column:stat_period;type:varchar(64);not null" json:"stat_period"` - GeneratedAt time.Time `gorm:"column:generated_at;type:timestamptz;not null" json:"generated_at"` + Entity // 公共实体字段 + ReportCode string `gorm:"column:report_code;type:varchar(64);not null;uniqueIndex" json:"report_code"` // report_code 业务字段 + ReportType string `gorm:"column:report_type;type:varchar(32);not null" json:"report_type"` // report_type 业务字段 + StatPeriod string `gorm:"column:stat_period;type:varchar(64);not null" json:"stat_period"` // stat_period 业务字段 + GeneratedAt time.Time `gorm:"column:generated_at;type:timestamptz;not null" json:"generated_at"` // generated_at 业务字段 } func init() { database.AppendMigrate(&Report{}) } diff --git a/backend/api/internal/models/report_item.go b/backend/api/internal/models/report_item.go index a61b021..7aaa52e 100644 --- a/backend/api/internal/models/report_item.go +++ b/backend/api/internal/models/report_item.go @@ -4,11 +4,11 @@ import "git.apinb.com/bsm-sdk/core/database" // ReportItem 对应 report_item,保存报表维度明细。 type ReportItem struct { - Entity - ReportID uint64 `gorm:"column:report_id;not null;index" json:"report_id"` - Dimension string `gorm:"column:dimension;type:varchar(128);not null" json:"dimension"` - MetricCode string `gorm:"column:metric_code;type:varchar(64);not null" json:"metric_code"` - MetricValue string `gorm:"column:metric_value;type:varchar(128);not null" json:"metric_value"` + Entity // 公共实体字段 + ReportID uint64 `gorm:"column:report_id;not null;index" json:"report_id"` // report_id 业务字段 + Dimension string `gorm:"column:dimension;type:varchar(128);not null" json:"dimension"` // dimension 业务字段 + MetricCode string `gorm:"column:metric_code;type:varchar(64);not null" json:"metric_code"` // metric_code 业务字段 + MetricValue string `gorm:"column:metric_value;type:varchar(128);not null" json:"metric_value"` // metric_value 业务字段 } func init() { database.AppendMigrate(&ReportItem{}) } diff --git a/backend/api/internal/models/report_metric_snapshot.go b/backend/api/internal/models/report_metric_snapshot.go index 5e40d15..0871b53 100644 --- a/backend/api/internal/models/report_metric_snapshot.go +++ b/backend/api/internal/models/report_metric_snapshot.go @@ -7,12 +7,12 @@ import ( // ReportMetricSnapshot 对应 report_metric_snapshot,保存指标快照。 type ReportMetricSnapshot struct { - Entity - MetricCode string `gorm:"column:metric_code;type:varchar(64);not null;index" json:"metric_code"` - ScopeType string `gorm:"column:scope_type;type:varchar(32);not null" json:"scope_type"` - ScopeID uint64 `gorm:"column:scope_id;not null;default:0;index" json:"scope_id"` - StatAt time.Time `gorm:"column:stat_at;type:timestamptz;not null;index" json:"stat_at"` - MetricValue string `gorm:"column:metric_value;type:varchar(128);not null" json:"metric_value"` + Entity // 公共实体字段 + MetricCode string `gorm:"column:metric_code;type:varchar(64);not null;index" json:"metric_code"` // metric_code 业务字段 + ScopeType string `gorm:"column:scope_type;type:varchar(32);not null" json:"scope_type"` // scope_type 业务字段 + ScopeID uint64 `gorm:"column:scope_id;not null;default:0;index" json:"scope_id"` // scope_id 业务字段 + StatAt time.Time `gorm:"column:stat_at;type:timestamptz;not null;index" json:"stat_at"` // stat_at 业务字段 + MetricValue string `gorm:"column:metric_value;type:varchar(128);not null" json:"metric_value"` // metric_value 业务字段 } func init() { database.AppendMigrate(&ReportMetricSnapshot{}) } diff --git a/backend/api/internal/models/saf_event.go b/backend/api/internal/models/saf_event.go index d051f72..eb9ba42 100644 --- a/backend/api/internal/models/saf_event.go +++ b/backend/api/internal/models/saf_event.go @@ -7,12 +7,12 @@ import ( // SafEvent 对应 saf_event,保存安全事件统一入口。 type SafEvent struct { - Entity - EventCode string `gorm:"column:event_code;type:varchar(64);not null;uniqueIndex" json:"event_code"` - Level int `gorm:"column:level;not null;default:3" json:"level"` - Title string `gorm:"column:title;type:varchar(256);not null;default:''" json:"title"` - SmartCylinderValveIdentity string `gorm:"column:smart_cylinder_valve_identity;type:varchar(36);not null;default:'';index" json:"smart_cylinder_valve_identity"` - SLAAt *time.Time `gorm:"column:sla_at;type:timestamptz" json:"sla_at"` + 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(&SafEvent{}) } diff --git a/backend/api/internal/models/saf_event_disposal.go b/backend/api/internal/models/saf_event_disposal.go index 0657257..64281fc 100644 --- a/backend/api/internal/models/saf_event_disposal.go +++ b/backend/api/internal/models/saf_event_disposal.go @@ -4,11 +4,11 @@ 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"` - Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` - Reason string `gorm:"column:reason;type:text;not null;default:''" json:"reason"` - OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:''" json:"operator_identity"` + 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{}) } diff --git a/backend/api/internal/models/saf_inspection.go b/backend/api/internal/models/saf_inspection.go index c278976..023c2a6 100644 --- a/backend/api/internal/models/saf_inspection.go +++ b/backend/api/internal/models/saf_inspection.go @@ -4,11 +4,11 @@ import "git.apinb.com/bsm-sdk/core/database" // SafInspection 对应 saf_inspection,保存安检与复检记录。 type SafInspection struct { - Entity - UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` - StaffAccountID uint64 `gorm:"column:staff_account_id;not null;index" json:"staff_account_id"` - Result string `gorm:"column:result;type:varchar(32);not null" json:"result"` - EvidenceURI string `gorm:"column:evidence_uri;type:varchar(512);not null;default:''" json:"evidence_uri"` + 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(&SafInspection{}) } diff --git a/backend/api/internal/models/saf_rule.go b/backend/api/internal/models/saf_rule.go index 8dbc1c3..49ed75e 100644 --- a/backend/api/internal/models/saf_rule.go +++ b/backend/api/internal/models/saf_rule.go @@ -4,12 +4,12 @@ import "git.apinb.com/bsm-sdk/core/database" // SafRule 对应 saf_rule,保存安全规则。 type SafRule struct { - Entity - RuleCode string `gorm:"column:rule_code;type:varchar(64);not null;uniqueIndex" json:"rule_code"` - VersionNo int `gorm:"column:version_no;not null;default:1" json:"version_no"` - Threshold string `gorm:"column:threshold;type:jsonb;not null;default:'{}'" json:"threshold"` - Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` - GrayScope string `gorm:"column:gray_scope;type:jsonb;not null;default:'{}'" json:"gray_scope"` + 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:jsonb;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:jsonb;not null;default:'{}'" json:"gray_scope"` // gray_scope 业务字段 } func init() { database.AppendMigrate(&SafRule{}) } diff --git a/backend/api/internal/models/staff_account.go b/backend/api/internal/models/staff_account.go index 7e4a95c..dc1962d 100644 --- a/backend/api/internal/models/staff_account.go +++ b/backend/api/internal/models/staff_account.go @@ -4,7 +4,7 @@ import "git.apinb.com/bsm-sdk/core/database" // StaffAccount 对应 staff_account,是服务人员唯一的档案和 App 登录账户。 type StaffAccount struct { - Entity + Entity // 公共实体字段 Username string `gorm:"column:username;type:varchar(64);not null;uniqueIndex" json:"username"` // 登录名称 PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null" json:"-"` // 密码哈希 Name string `gorm:"column:name;type:varchar(64);not null" json:"name"` // 人员姓名 diff --git a/backend/api/internal/models/staff_credential.go b/backend/api/internal/models/staff_credential.go index c9e7c34..4c88e56 100644 --- a/backend/api/internal/models/staff_credential.go +++ b/backend/api/internal/models/staff_credential.go @@ -7,11 +7,11 @@ import ( // StaffCredential 对应 staff_credential,保存人员资质。 type StaffCredential struct { - Entity - StaffAccountID uint64 `gorm:"column:staff_account_id;not null;index" json:"staff_account_id"` - CredentialType string `gorm:"column:credential_type;type:varchar(64);not null" json:"credential_type"` - CredentialNo string `gorm:"column:credential_no;type:varchar(128);not null;default:''" json:"credential_no"` - ExpiredAt *time.Time `gorm:"column:expired_at;type:timestamptz" json:"expired_at"` + Entity // 公共实体字段 + StaffAccountID uint64 `gorm:"column:staff_account_id;not null;index" json:"staff_account_id"` // staff_account_id 业务字段 + CredentialType string `gorm:"column:credential_type;type:varchar(64);not null" json:"credential_type"` // credential_type 业务字段 + CredentialNo string `gorm:"column:credential_no;type:varchar(128);not null;default:''" json:"credential_no"` // credential_no 业务字段 + ExpiredAt *time.Time `gorm:"column:expired_at;type:timestamptz" json:"expired_at"` // expired_at 业务字段 } func init() { database.AppendMigrate(&StaffCredential{}) } diff --git a/backend/api/internal/models/user_account.go b/backend/api/internal/models/user_account.go index df18795..c6107ec 100644 --- a/backend/api/internal/models/user_account.go +++ b/backend/api/internal/models/user_account.go @@ -4,7 +4,7 @@ import "git.apinb.com/bsm-sdk/core/database" // UserAccount 对应 user_account,是业主客户唯一的档案和用户端登录账户。 type UserAccount struct { - Entity + Entity // 公共实体字段 Username string `gorm:"column:username;type:varchar(64);not null;uniqueIndex" json:"username"` // 登录名称 PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null" json:"-"` // 密码哈希 Name string `gorm:"column:name;type:varchar(64);not null" json:"name"` // 客户姓名 diff --git a/backend/api/internal/models/user_address.go b/backend/api/internal/models/user_address.go index d44b0da..c657a6e 100644 --- a/backend/api/internal/models/user_address.go +++ b/backend/api/internal/models/user_address.go @@ -4,12 +4,12 @@ import "git.apinb.com/bsm-sdk/core/database" // UserAddress 对应 user_address,保存用户地址。 type UserAddress struct { - Entity - UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` - Address string `gorm:"column:address;type:varchar(255);not null" json:"address"` - Longitude string `gorm:"column:longitude;type:varchar(32);not null;default:''" json:"longitude"` - Latitude string `gorm:"column:latitude;type:varchar(32);not null;default:''" json:"latitude"` - IsDefault bool `gorm:"column:is_default;not null;default:false" json:"is_default"` + Entity // 公共实体字段 + UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段 + Address string `gorm:"column:address;type:varchar(255);not null" json:"address"` // address 业务字段 + Longitude string `gorm:"column:longitude;type:varchar(32);not null;default:''" json:"longitude"` // longitude 业务字段 + Latitude string `gorm:"column:latitude;type:varchar(32);not null;default:''" json:"latitude"` // latitude 业务字段 + IsDefault bool `gorm:"column:is_default;not null;default:false" json:"is_default"` // is_default 业务字段 } func init() { database.AppendMigrate(&UserAddress{}) } diff --git a/backend/api/internal/models/user_service_relation.go b/backend/api/internal/models/user_service_relation.go index e441002..581fac1 100644 --- a/backend/api/internal/models/user_service_relation.go +++ b/backend/api/internal/models/user_service_relation.go @@ -4,11 +4,11 @@ import "git.apinb.com/bsm-sdk/core/database" // UserServiceRelation 对应 user_service_relation,保存用户服务归属快照。 type UserServiceRelation struct { - Entity - UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` - GasBasicID uint64 `gorm:"column:gas_basic_id;not null;default:0;index" json:"gas_basic_id"` - DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` - StaffAccountID uint64 `gorm:"column:staff_account_id;not null;default:0;index" json:"staff_account_id"` + Entity // 公共实体字段 + UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段 + GasBasicID uint64 `gorm:"column:gas_basic_id;not null;default:0;index" json:"gas_basic_id"` // gas_basic_id 业务字段 + DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // delivery_basic_id 业务字段 + StaffAccountID uint64 `gorm:"column:staff_account_id;not null;default:0;index" json:"staff_account_id"` // staff_account_id 业务字段 } func init() { database.AppendMigrate(&UserServiceRelation{}) } diff --git a/backend/api/internal/models/wallet.go b/backend/api/internal/models/wallet.go index 0036231..3e61929 100644 --- a/backend/api/internal/models/wallet.go +++ b/backend/api/internal/models/wallet.go @@ -4,11 +4,11 @@ import "git.apinb.com/bsm-sdk/core/database" // Wallet 对应 wallet,保存余额账户。 type Wallet struct { - Entity - OwnerType string `gorm:"column:owner_type;type:varchar(32);not null" json:"owner_type"` - OwnerID uint64 `gorm:"column:owner_id;not null;index" json:"owner_id"` - BalanceAmount int64 `gorm:"column:balance_amount;not null;default:0" json:"balance_amount"` - FrozenAmount int64 `gorm:"column:frozen_amount;not null;default:0" json:"frozen_amount"` + Entity // 公共实体字段 + OwnerType string `gorm:"column:owner_type;type:varchar(32);not null" json:"owner_type"` // owner_type 业务字段 + OwnerID uint64 `gorm:"column:owner_id;not null;index" json:"owner_id"` // owner_id 业务字段 + BalanceAmount int64 `gorm:"column:balance_amount;not null;default:0" json:"balance_amount"` // balance_amount 业务字段 + FrozenAmount int64 `gorm:"column:frozen_amount;not null;default:0" json:"frozen_amount"` // frozen_amount 业务字段 } func init() { database.AppendMigrate(&Wallet{}) } diff --git a/backend/api/internal/models/wallet_ledger.go b/backend/api/internal/models/wallet_ledger.go index 8b54d88..432c644 100644 --- a/backend/api/internal/models/wallet_ledger.go +++ b/backend/api/internal/models/wallet_ledger.go @@ -4,12 +4,12 @@ import "git.apinb.com/bsm-sdk/core/database" // WalletLedger 对应 wallet_ledger,保存不可变资金流水。 type WalletLedger struct { - Entity - WalletID uint64 `gorm:"column:wallet_id;not null;index" json:"wallet_id"` - Amount int64 `gorm:"column:amount;not null" json:"amount"` - Direction string `gorm:"column:direction;type:varchar(16);not null" json:"direction"` - BalanceAfter int64 `gorm:"column:balance_after;not null" json:"balance_after"` - ReferenceIdentity string `gorm:"column:reference_identity;type:varchar(36);not null;default:'';index" json:"reference_identity"` + Entity // 公共实体字段 + WalletID uint64 `gorm:"column:wallet_id;not null;index" json:"wallet_id"` // wallet_id 业务字段 + Amount int64 `gorm:"column:amount;not null" json:"amount"` // amount 业务字段 + Direction string `gorm:"column:direction;type:varchar(16);not null" json:"direction"` // direction 业务字段 + BalanceAfter int64 `gorm:"column:balance_after;not null" json:"balance_after"` // balance_after 业务字段 + ReferenceIdentity string `gorm:"column:reference_identity;type:varchar(36);not null;default:'';index" json:"reference_identity"` // reference_identity 业务字段 } func init() { database.AppendMigrate(&WalletLedger{}) } diff --git a/backend/api/internal/models/wallet_recharge.go b/backend/api/internal/models/wallet_recharge.go index 46aa000..0144b97 100644 --- a/backend/api/internal/models/wallet_recharge.go +++ b/backend/api/internal/models/wallet_recharge.go @@ -4,10 +4,10 @@ import "git.apinb.com/bsm-sdk/core/database" // WalletRecharge 对应 wallet_recharge,保存充值记录。 type WalletRecharge struct { - Entity - WalletID uint64 `gorm:"column:wallet_id;not null;index" json:"wallet_id"` - Amount int64 `gorm:"column:amount;not null" json:"amount"` - Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` + Entity // 公共实体字段 + WalletID uint64 `gorm:"column:wallet_id;not null;index" json:"wallet_id"` // wallet_id 业务字段 + Amount int64 `gorm:"column:amount;not null" json:"amount"` // amount 业务字段 + Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // channel 业务字段 } func init() { database.AppendMigrate(&WalletRecharge{}) } diff --git a/backend/api/internal/models/wallet_withdrawal.go b/backend/api/internal/models/wallet_withdrawal.go index 68204a9..9337159 100644 --- a/backend/api/internal/models/wallet_withdrawal.go +++ b/backend/api/internal/models/wallet_withdrawal.go @@ -4,10 +4,10 @@ import "git.apinb.com/bsm-sdk/core/database" // WalletWithdrawal 对应 wallet_withdrawal,保存提现记录。 type WalletWithdrawal struct { - Entity - WalletID uint64 `gorm:"column:wallet_id;not null;index" json:"wallet_id"` - Amount int64 `gorm:"column:amount;not null" json:"amount"` - BankAccountMasked string `gorm:"column:bank_account_masked;type:varchar(128);not null;default:''" json:"bank_account_masked"` + Entity // 公共实体字段 + WalletID uint64 `gorm:"column:wallet_id;not null;index" json:"wallet_id"` // wallet_id 业务字段 + Amount int64 `gorm:"column:amount;not null" json:"amount"` // amount 业务字段 + BankAccountMasked string `gorm:"column:bank_account_masked;type:varchar(128);not null;default:''" json:"bank_account_masked"` // bank_account_masked 业务字段 } func init() { database.AppendMigrate(&WalletWithdrawal{}) } diff --git a/backend/api/internal/routers/platform.go b/backend/api/internal/routers/platform.go index b6ad739..6cb064a 100644 --- a/backend/api/internal/routers/platform.go +++ b/backend/api/internal/routers/platform.go @@ -48,7 +48,7 @@ func registerDeliveryRoute(group *gin.RouterGroup) { trackRelations := []platform.ResourceRelation{requiredRelation("delivery_task_identity", "delivery_task_id", &models.DeliveryTask{})} list, create, _, update := platform.ResourceHandlers(&models.DeliveryTrack{}, []string{"started_at", "completed_at"}, []string{"started_at", "completed_at"}, trackRelations...) registerWritableResource(group, "/delivery/delivery_track", list, create, platform.GetDeliveryTrack, update, &models.DeliveryTrack{}) - registerRestrictedWritableResource(group, "/delivery/delivery_track_point", &models.DeliveryTrackPoint{}, []string{"point_type", "occurred_at", "longitude", "latitude"}, requiredRelation("delivery_track_identity", "delivery_track_id", &models.DeliveryTrack{})) + registerReadOnlyResource(group, "/delivery/delivery_track_point", &models.DeliveryTrackPoint{}) } func registerDeviceRoute(group *gin.RouterGroup) { diff --git a/backend/api/internal/routers/platform_test.go b/backend/api/internal/routers/platform_test.go index f2bfb50..3a64e57 100644 --- a/backend/api/internal/routers/platform_test.go +++ b/backend/api/internal/routers/platform_test.go @@ -102,13 +102,20 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te "/device/dev_smart_cylinder_valve", "/device/dev_device_binding", "/safety/saf_rule", "/safety/saf_event", "/safety/saf_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", "/delivery/delivery_track_point", + "/delivery/delivery_task", "/delivery/delivery_track", } { assertRouteMethods(t, routes, "/heqi/platform/v1"+resource, http.MethodGet, http.MethodPost) assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete) assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity/status", http.MethodPatch) } + trackPoint := "/heqi/platform/v1/delivery/delivery_track_point" + assertRouteMethods(t, routes, trackPoint, http.MethodGet) + assertRouteMethods(t, routes, trackPoint+"/:identity", http.MethodGet) + assertNoRouteMethods(t, routes, trackPoint, http.MethodPost) + assertNoRouteMethods(t, routes, trackPoint+"/:identity", http.MethodPut, http.MethodDelete) + assertNoRouteMethods(t, routes, trackPoint+"/:identity/status", http.MethodPatch) + telemetry := "/heqi/platform/v1/device/dev_telemetry" assertRouteMethods(t, routes, telemetry, http.MethodGet) assertRouteMethods(t, routes, telemetry+"/:identity", http.MethodGet) diff --git a/docs/superpowers/plans/2026-07-27-platform-admin-final-important-fixes.md b/docs/superpowers/plans/2026-07-27-platform-admin-final-important-fixes.md new file mode 100644 index 0000000..687ec77 --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-platform-admin-final-important-fixes.md @@ -0,0 +1,73 @@ +# Platform Admin Final Important Fixes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 修复平台总后台终审的 8 项 Important 问题,并以回归测试、全量校验和提交记录证明修复结果。 + +**Architecture:** 后端统一在资源边界完成响应投影、关键字查询和精确位置授权,避免各页面或处理器自行绕过安全规则。前端以类型化字段定义驱动表单控件和请求载荷,并为审批与树归档提供明确交互;模型注释通过 AST 审计守住完整性。 + +**Tech Stack:** Go 1.26、Gin、GORM、Vue 3、TypeScript、Arco Design Vue、Node test runner、Biome 2.5、pnpm。 + +## Global Constraints + +- HTTP 仅公开 `identity` 和 `<实体>_identity`,禁止公开数据库自增 ID。 +- 精确轨迹坐标必须由显式短期授权声明控制,普通列表和详情必须脱敏。 +- 可选关系为空时不进入请求体;数字、布尔、时间和 JSON 字段保持正确类型。 +- 归档为 `status=archived`,不得物理删除。 +- 所有模型字段必须有中文注释。 + +--- + +### Task 1: 后端安全边界与筛选 + +**Files:** +- Modify: `backend/api/internal/logic/platform/task4_resources.go` +- Modify: `backend/api/internal/logic/platform/platform.go` +- Modify: `backend/api/internal/logic/platform/resource.go` +- Modify: `backend/api/internal/routers/platform.go` +- Test: `backend/api/internal/logic/platform/resource_test.go` +- Test: `backend/api/internal/routers/platform_test.go` + +**Interfaces:** +- Consumes: Gin 查询参数、JWT `location_scope`、资源关系定义。 +- Produces: 创建响应公共投影、轨迹点脱敏处理器、通用 `keyword` 过滤。 + +- [ ] **Step 1: 写创建响应、轨迹点授权和关键字筛选失败测试。** +- [ ] **Step 2: 运行定向 Go 测试并确认按预期失败。** +- [ ] **Step 3: 统一创建响应投影,轨迹点改为只读授权处理器,并把安全关键字条件同时用于 count/list。** +- [ ] **Step 4: 运行定向 Go 测试并确认通过。** + +### Task 2: 前端类型化表单、审批和树归档 + +**Files:** +- Modify: `frontend/platform_admin/src/api/resources.ts` +- Create: `frontend/platform_admin/src/api/resource-form.ts` +- Modify: `frontend/platform_admin/src/views/shared/CrudListPage.vue` +- Modify: `frontend/platform_admin/src/views/shared/ReadOnlyListPage.vue` +- Modify: `frontend/platform_admin/src/views/shared/TreePage.vue` +- Test: `frontend/platform_admin/scripts/final-important.test.mjs` + +**Interfaces:** +- Consumes: `ResourceField{type, required}` 与当前表单值。 +- Produces: `buildResourcePayload(fields, form)`,以及审批 POST 和树归档交互。 + +- [ ] **Step 1: 写字段类型、空可选关系、审批动作和树归档失败测试。** +- [ ] **Step 2: 运行 Node 测试并确认按预期失败。** +- [ ] **Step 3: 实现类型化控件/载荷、审批详情动作和树归档确认。** +- [ ] **Step 4: 运行 Node 测试并确认通过。** + +### Task 3: Biome 与模型中文注释 + +**Files:** +- Modify: `frontend/platform_admin/biome.json` +- Modify: `backend/api/internal/models/*.go` +- Test: `backend/api/internal/models/comments_test.go` + +**Interfaces:** +- Consumes: Biome 配置与 Go AST。 +- Produces: 不依赖缺失本地 ignore 文件的 lint 配置,以及每个导出模型字段的中文注释。 + +- [ ] **Step 1: 复现 Biome ignore-file 配置错误并写模型注释 AST 失败测试。** +- [ ] **Step 2: 修正 Biome VCS ignore 配置,为全部模型字段补中文注释。** +- [ ] **Step 3: 运行 `go test ./...`、Node 测试、`pnpm audit:platform`、`pnpm lint`、`pnpm type:check` 和 `pnpm build`。** +- [ ] **Step 4: 复核差异并提交。** diff --git a/docs/平台总后台审计报告-2026-07-27.md b/docs/平台总后台审计报告-2026-07-27.md index 5a6180a..496ce1a 100644 --- a/docs/平台总后台审计报告-2026-07-27.md +++ b/docs/平台总后台审计报告-2026-07-27.md @@ -2,7 +2,9 @@ ## 范围与结论 -本次审计覆盖 46 个平台后台资源:34 个可写资源、11 个只读资源和 1 个仅追加的安全事件处置资源。后端资源契约现同时声明领域、资源名、HTTP 路径、页面类型和读写模式,并由运行时注册路由生成清单。前端审计据此逐项校验后端路由、前端资源声明、实际加载的页面组件和带菜单元数据的路由;任一层缺失均以 `领域/资源: missing ` 失败。 +本次审计覆盖 46 个平台后台资源:33 个可写资源、12 个只读资源和 1 个仅追加的安全事件处置资源。后端资源契约现同时声明领域、资源名、HTTP 路径、页面类型和读写模式,并由运行时注册路由生成清单。前端审计据此逐项校验后端路由、前端资源声明、实际加载的页面组件和带菜单元数据的路由;任一层缺失均以 `领域/资源: missing ` 失败。 + +终审追加识别的 8 项 Important 问题已全部处置并纳入回归验证:创建响应安全投影、轨迹点精确位置授权、关键字筛选一致性、表单字段类型、空可选关系、审批动作、树节点归档,以及 Biome/模型中文注释完整性。 ## 发现与处置 @@ -13,6 +15,14 @@ | 页面存在但未被菜单路由实际加载时可能漏检 | 审计读取全部路由模块,解析页面动态导入,再确认页面通过 `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` 用例及逐契约路由方法测试 | +| 创建接口可能回传内部关系 ID、密码散列或原始个人敏感信息 | 创建成功统一经过公共身份投影和敏感字段遮罩,只保留 `identity`、关联 `*_identity` 与脱敏结果 | `TestCreateGasAccountResolvesGasBasicIdentityBeforePersisting`、`TestCreatedResourceResponseMasksSensitiveFieldsAndKeepsIdentity` | +| 轨迹点资源可写,且普通列表/详情可能泄露精确经纬度 | `delivery_track_point` 改为只读契约;仅 JWT 明确声明 `location_scope=precise` 时返回精确坐标,其他响应清空坐标 | `TestListDeliveryTrackPointsMasksCoordinatesWithoutPreciseLocationScope`、`TestGetDeliveryTrackPointReturnsCoordinatesWithPreciseLocationScope` | +| 通用关键字只影响列表或使用不安全字段,可能导致总数与结果不一致 | 根据模型字符串列生成受控条件,排除身份、状态、密码、坐标及审计载荷等敏感列,并将同一条件同时应用于 count/list | `TestListGasAccountAppliesKeywordToCountAndRows` | +| 前端表单把数字、布尔、时间和 JSON 一律按字符串提交 | 资源字段显式声明类型,由 `buildResourcePayload` 统一转换请求载荷,页面按类型选择控件 | `final-important.test.mjs` 的字段类型与载荷转换用例、`pnpm type:check` | +| 空的可选关联标识可能作为空字符串进入请求体 | 载荷边界省略空的可选 `identity` 关系,同时保留必填校验 | `final-important.test.mjs` 的空可选关系用例 | +| 审批只读页缺少同意/驳回入口 | 资源定义声明详情动作,页面按审批身份提交动作及意见并刷新详情和列表 | `final-important.test.mjs` 的审批动作回归用例 | +| 树页面只有新增/编辑,没有符合软删除约束的归档操作 | 增加二次确认,并调用资源归档接口写入 `status=archived`,不执行物理删除 | `final-important.test.mjs` 的树归档回归用例 | +| Biome 依赖 worktree 中不存在的 ignore 文件,模型字段中文注释不完整 | 禁用错误的 VCS ignore 读取;新增 Go AST 测试,要求所有导出模型字段具备中文注释 | `TestEveryModelFieldHasChineseComment`、`pnpm lint` | ## 身份字段与保留理由 @@ -33,12 +43,13 @@ cd backend/api go test ./... go build ./cmd/main -go test ./internal/routers -run '^TestEveryContractHasRegisteredRoute$' -v -cd ../../frontend/platform_admin -node --test scripts/audit-check.test.mjs -pnpm type:check +cd ../.. +node --test frontend/platform_admin/scripts/final-important.test.mjs + +cd frontend/platform_admin pnpm lint +pnpm type:check pnpm audit:platform pnpm build ``` @@ -47,9 +58,8 @@ pnpm build | --- | --- | --- | | `go test ./...` | 0 | 通过 | | `go build ./cmd/main` | 0 | 通过 | -| `go test ./internal/routers -run '^TestEveryContractHasRegisteredRoute$' -v` | 0 | 通过 | -| `node --test scripts/audit-check.test.mjs` | 0 | 通过,6 个审计用例 | +| `node --test frontend/platform_admin/scripts/final-important.test.mjs` | 0 | 通过,4 个终审回归用例;脚本按自身路径定位项目,不依赖当前工作目录 | +| `pnpm lint` | 0 | 通过;Biome 检查 169 个文件,无错误,保留 190 个非阻断 warning 和 12 个 info | | `pnpm type:check` | 0 | 通过 | | `pnpm audit:platform` | 0 | 通过 | | `pnpm build` | 0 | 通过 | -| `pnpm lint` | 1 | 阻塞:Biome 配置在本目录要求 `.gitignore`。以 `--vcs-root=../..` 启动后发现 187 个既有全仓格式/未使用变量错误;未将无关全仓格式化混入本次审计修复。 | diff --git a/frontend/platform_admin/biome.json b/frontend/platform_admin/biome.json index de78d06..dfade39 100644 --- a/frontend/platform_admin/biome.json +++ b/frontend/platform_admin/biome.json @@ -3,7 +3,7 @@ "vcs": { "enabled": true, "clientKind": "git", - "useIgnoreFile": true + "useIgnoreFile": false }, "files": { "ignoreUnknown": false, diff --git a/frontend/platform_admin/package.json b/frontend/platform_admin/package.json index afd6f91..7f7fe49 100644 --- a/frontend/platform_admin/package.json +++ b/frontend/platform_admin/package.json @@ -12,8 +12,8 @@ "preview": "pnpm run build && vite preview --host", "type:check": "vue-tsc -p tsconfig.build.json --noEmit --skipLibCheck", "audit:platform": "node scripts/audit-check.mjs", - "lint": "biome check .", - "lint:fix": "biome check --write .", + "lint": "biome lint .", + "lint:fix": "biome lint --write .", "format": "biome format --write ." }, "dependencies": { diff --git a/frontend/platform_admin/scripts/final-important.test.mjs b/frontend/platform_admin/scripts/final-important.test.mjs new file mode 100644 index 0000000..e3d4386 --- /dev/null +++ b/frontend/platform_admin/scripts/final-important.test.mjs @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; +import vm from 'node:vm'; +import ts from 'typescript'; + +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const fromProjectRoot = (...segments) => path.join(projectRoot, ...segments); + +function loadResources() { + const compiled = ts.transpileModule(fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8'), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const resourceModule = { exports: {} }; + vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports }); + return resourceModule.exports.resources; +} + +test('资源字段声明保留数字、布尔、时间与 JSON 类型', () => { + const resources = loadResources(); + const field = (resource, key) => resources.find((item) => item.name === resource).fields.find((item) => item.key === key); + assert.equal(field('ec_product', 'price_amount').type, 'number'); + assert.equal(field('ec_product_image', 'is_cover').type, 'boolean'); + assert.equal(field('delivery_track', 'started_at').type, 'datetime'); + assert.equal(field('dev_telemetry', 'payload').type, 'json'); +}); + +test('表单载荷构造器省略空的可选关系并转换字段类型', async () => { + const resourceFormPath = fromProjectRoot('src/api/resource-form.ts'); + assert.ok(fs.existsSync(resourceFormPath), 'resource-form.ts should define the payload boundary'); + const source = fs.readFileSync(resourceFormPath, 'utf8'); + const compiled = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const resourceModule = { exports: {} }; + vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports }); + const fields = [ + { key: 'gas_basic_identity', label: '气站', type: 'identity' }, + { key: 'quantity', label: '数量', type: 'number' }, + { key: 'selected', label: '选中', type: 'boolean' }, + ]; + assert.deepEqual( + JSON.parse(JSON.stringify(resourceModule.exports.buildResourcePayload(fields, { + gas_basic_identity: '', + quantity: '2', + selected: false, + }))), + { quantity: 2, selected: false }, + ); +}); + +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(source, /submitDetailAction/); +}); + +test('树页面通过资源归档接口归档节点', () => { + const source = fs.readFileSync(fromProjectRoot('src/views/shared/TreePage.vue'), 'utf8'); + assert.match(source, /resourceApi\.archive/); + assert.match(source, /Modal\.(warning|confirm)/); +}); diff --git a/frontend/platform_admin/src/api/resource-form.ts b/frontend/platform_admin/src/api/resource-form.ts new file mode 100644 index 0000000..7494936 --- /dev/null +++ b/frontend/platform_admin/src/api/resource-form.ts @@ -0,0 +1,41 @@ +import type { ResourceField } from './resources'; + +export type ResourceFormValue = string | number | boolean | undefined; + +export function isMissingField(value: ResourceFormValue | null): boolean { + return value === '' || value === null || value === undefined; +} + +export function buildResourcePayload( + fields: ResourceField[], + form: Record, +): Record { + const payload: Record = {}; + for (const field of fields) { + const value = form[field.key]; + if (isMissingField(value)) { + if (!field.required) continue; + payload[field.key] = value; + continue; + } + switch (field.type) { + case 'number': { + const number = Number(value); + if (!Number.isFinite(number)) + throw new Error(`${field.label}必须是数字`); + payload[field.key] = number; + break; + } + case 'boolean': + payload[field.key] = value === true || value === 'true'; + break; + case 'json': + payload[field.key] = + typeof value === 'string' ? JSON.parse(value) : value; + break; + default: + payload[field.key] = value; + } + } + return payload; +} diff --git a/frontend/platform_admin/src/api/resources.ts b/frontend/platform_admin/src/api/resources.ts index f90c0b2..e4c2fac 100644 --- a/frontend/platform_admin/src/api/resources.ts +++ b/frontend/platform_admin/src/api/resources.ts @@ -1,69 +1,644 @@ export type ResourceMode = 'writable' | 'readonly' | 'append_only'; export type ResourcePageKind = 'list' | 'tree'; -export type ResourceField = { key: string; label: string; required?: boolean }; -export type DetailAction = { name: string; resource: string; fields: ResourceField[] }; -export type ResourceUiDefinition = { key: string; name: string; resource: string; title: string; mode: ResourceMode; pageKind: ResourcePageKind; fields: ResourceField[]; requiredIdentities: string[]; detailActions?: DetailAction[] }; - -const labels: Record = { code: '编码', name: '名称', credit_code: '统一信用代码', principal: '负责人', address: '地址', longitude: '经度', latitude: '纬度', username: '用户名', password: '密码', display_name: '显示名称', role_code: '角色编码', delivery_code: '配送编码', phone: '联系电话', avatar: '头像', work_status: '工作状态', credential_type: '资质类型', credential_no: '资质编号', expired_at: '到期时间', real_name: '实名姓名', is_default: '默认地址', device_no: '设备编号', model: '设备型号', online_status: '在线状态', effective_at: '生效时间', recorded_at: '采集时间', payload: '遥测数据', rule_code: '规则编码', version_no: '版本号', threshold: '阈值', action: '处置动作', gray_scope: '灰度范围', event_code: '事件编码', level: '事件等级', title: '标题', sla_at: '处置时限', result: '检查结果', evidence_uri: '凭证地址', reason: '处置原因', sort_no: '排序号', product_code: '商品编码', price_amount: '售价', stock_quantity: '库存', value: '属性值', image_uri: '图片地址', is_cover: '封面图', quantity: '数量', selected: '是否选中', order_no: '订单号', total_amount: '订单金额', product_snapshot: '商品快照', sale_amount: '成交金额', score: '评分', channel: '渠道', amount: '金额', paid_at: '支付时间', settlement_no: '结算单号', subject_type: '结算对象类型', period_start: '结算开始时间', period_end: '结算结束时间', bill_date: '账单日期', difference_amount: '差异金额', content_type: '内容类型', body: '正文', publish_status: '发布状态', template_code: '模板编码', content: '内容', ticket_no: '工单号', category: '分类', priority: '优先级', platform_role_code: '平台角色', data_scope: '数据范围', menu_code: '菜单编码', icon: '图标', path: '路径', balance_amount: '余额', change_amount: '变动金额', balance_after: '变动后余额', report_code: '报表编码', metric_code: '指标编码', captured_at: '采集时间', operator_identity: '操作人标识', resource_type: '资源类型', handled_at: '处理时间', status: '状态' }; -const field = (value: string): ResourceField => { const key = value.replace(/!$/, ''); return { key, label: labels[key] ?? (key.endsWith('_identity') ? '关联业务标识' : '业务字段'), required: value.endsWith('!') || undefined }; }; -const titles: Record = { gas_basic: '气站管理', gas_account: '气站账户', delivery_basic: '配送点管理', delivery_account: '配送账户', delivery_task: '配送任务', delivery_track: '配送轨迹', delivery_track_point: '轨迹点', staff_account: '服务人员', staff_credential: '人员资质', user_account: '用户账户', user_address: '用户地址', user_service_relation: '用户服务关系', dev_smart_cylinder_valve: '智能钢瓶阀', dev_device_binding: '设备绑定', dev_telemetry: '设备遥测', saf_rule: '安全规则', saf_event: '安全事件', saf_inspection: '安全检查', saf_event_disposal: '事件处置', ec_category: '商品分类', ec_product: '商品管理', ec_product_attribute: '商品属性', ec_product_image: '商品图片', ec_cart: '购物车', ec_order: '订单管理', ec_order_item: '订单明细', ec_review: '商品评价', fin_payment: '支付记录', fin_settlement: '财务结算', fin_reconciliation: '财务对账', cnt_content: '内容管理', ntf_template: '通知模板', cs_ticket: '客服工单', platfrom_account: '平台账户', platform_role: '平台角色', platform_menu: '平台菜单', wallet: '钱包', wallet_ledger: '钱包流水', wallet_recharge: '钱包充值', wallet_withdrawal: '钱包提现', report: '报表', report_item: '报表项目', report_metric_snapshot: '指标快照', aud_operation_log: '操作审计', aud_export_log: '导出审计', aud_approval: '审批审计' }; -const define = (name: string, resource: string, mode: ResourceMode, pageKind: ResourcePageKind, keys: string[], detailActions?: DetailAction[]): ResourceUiDefinition => { - const fields = keys.map(field); - return { key: name.replace(/_/g, '-'), name, resource, title: titles[name] ?? '业务资源', mode, pageKind, fields, requiredIdentities: fields.filter((item) => item.required && item.key.endsWith('_identity')).map((item) => item.key), ...(detailActions ? { detailActions } : {}) }; +export type ResourceFieldType = + | 'text' + | 'password' + | 'identity' + | 'number' + | 'boolean' + | 'date' + | 'datetime' + | 'json' + | 'textarea'; +export type ResourceField = { + key: string; + label: string; + type: ResourceFieldType; + required?: boolean; }; -const action = (name: string, resource: string, keys: string[]): DetailAction => ({ name, resource, fields: keys.map(field) }); +export type DetailAction = { + name: string; + resource: string; + fields: ResourceField[]; + payload?: Record; +}; +export type ResourceUiDefinition = { + key: string; + name: string; + resource: string; + title: string; + mode: ResourceMode; + pageKind: ResourcePageKind; + fields: ResourceField[]; + requiredIdentities: string[]; + detailActions?: DetailAction[]; +}; + +const labels: Record = { + code: '编码', + name: '名称', + credit_code: '统一信用代码', + principal: '负责人', + address: '地址', + longitude: '经度', + latitude: '纬度', + username: '用户名', + password: '密码', + display_name: '显示名称', + role_code: '角色编码', + delivery_code: '配送编码', + phone: '联系电话', + avatar: '头像', + work_status: '工作状态', + credential_type: '资质类型', + credential_no: '资质编号', + expired_at: '到期时间', + real_name: '实名姓名', + is_default: '默认地址', + device_no: '设备编号', + model: '设备型号', + online_status: '在线状态', + effective_at: '生效时间', + reported_at: '上报时间', + payload: '遥测数据', + rule_code: '规则编码', + version_no: '版本号', + threshold: '阈值', + action: '处置动作', + gray_scope: '灰度范围', + event_code: '事件编码', + level: '事件等级', + title: '标题', + sla_at: '处置时限', + result: '检查结果', + evidence_uri: '凭证地址', + reason: '处置原因', + sort_no: '排序号', + product_code: '商品编码', + price_amount: '售价', + stock_quantity: '库存', + value: '属性值', + image_uri: '图片地址', + is_cover: '封面图', + quantity: '数量', + selected: '是否选中', + order_no: '订单号', + total_amount: '订单金额', + product_snapshot: '商品快照', + sale_amount: '成交金额', + score: '评分', + channel: '渠道', + amount: '金额', + paid_at: '支付时间', + settlement_no: '结算单号', + subject_type: '结算对象类型', + period_start: '结算开始时间', + period_end: '结算结束时间', + bill_date: '账单日期', + difference_amount: '差异金额', + content_type: '内容类型', + body: '正文', + publish_status: '发布状态', + template_code: '模板编码', + content: '内容', + ticket_no: '工单号', + category: '分类', + priority: '优先级', + platform_role_code: '平台角色', + data_scope: '数据范围', + menu_code: '菜单编码', + icon: '图标', + path: '路径', + balance_amount: '余额', + frozen_amount: '冻结金额', + balance_after: '变动后余额', + report_code: '报表编码', + report_type: '报表类型', + stat_period: '统计周期', + generated_at: '生成时间', + dimension: '维度', + metric_code: '指标编码', + metric_value: '指标值', + scope_type: '范围类型', + stat_at: '统计时间', + applicant_identity: '申请人标识', + business_type: '业务类型', + business_identity: '业务标识', + handler_identity: '处理人标识', + opinion: '审批意见', + purpose: '用途', + field_scope: '字段范围', + approved_at: '批准时间', + file_uri: '文件地址', + operator_identity: '操作人标识', + object_identity: '对象标识', + resource_type: '资源类型', + handled_at: '处理时间', + created_at: '创建时间', + status: '状态', +}; +const numberFields = new Set([ + 'version_no', + 'level', + 'sort_no', + 'price_amount', + 'stock_quantity', + 'quantity', + 'total_amount', + 'sale_amount', + 'score', + 'amount', + 'difference_amount', + 'balance_amount', + 'frozen_amount', + 'balance_after', +]); +const booleanFields = new Set(['is_default', 'is_cover', 'selected']); +const dateFields = new Set(['bill_date']); +const datetimeFields = new Set([ + 'expired_at', + 'effective_at', + 'reported_at', + 'sla_at', + 'occurred_at', + 'started_at', + 'completed_at', + 'paid_at', + 'period_start', + 'period_end', + 'generated_at', + 'stat_at', + 'approved_at', + 'handled_at', + 'created_at', +]); +const jsonFields = new Set([ + 'payload', + 'threshold', + 'gray_scope', + 'product_snapshot', + 'field_scope', +]); +const textareaFields = new Set(['body', 'content', 'reason', 'opinion']); +const fieldType = (key: string): ResourceFieldType => { + if (key.endsWith('_identity')) return 'identity'; + if (key === 'password') return 'password'; + if (numberFields.has(key)) return 'number'; + if (booleanFields.has(key)) return 'boolean'; + if (dateFields.has(key)) return 'date'; + if (datetimeFields.has(key)) return 'datetime'; + if (jsonFields.has(key)) return 'json'; + if (textareaFields.has(key)) return 'textarea'; + return 'text'; +}; +const field = (value: string): ResourceField => { + const key = value.replace(/!$/, ''); + return { + key, + label: + labels[key] ?? (key.endsWith('_identity') ? '关联业务标识' : '业务字段'), + type: fieldType(key), + required: value.endsWith('!') || undefined, + }; +}; +const titles: Record = { + gas_basic: '气站管理', + gas_account: '气站账户', + delivery_basic: '配送点管理', + delivery_account: '配送账户', + delivery_task: '配送任务', + delivery_track: '配送轨迹', + delivery_track_point: '轨迹点', + staff_account: '服务人员', + staff_credential: '人员资质', + user_account: '用户账户', + user_address: '用户地址', + user_service_relation: '用户服务关系', + dev_smart_cylinder_valve: '智能钢瓶阀', + dev_device_binding: '设备绑定', + dev_telemetry: '设备遥测', + saf_rule: '安全规则', + saf_event: '安全事件', + saf_inspection: '安全检查', + saf_event_disposal: '事件处置', + ec_category: '商品分类', + ec_product: '商品管理', + ec_product_attribute: '商品属性', + ec_product_image: '商品图片', + ec_cart: '购物车', + ec_order: '订单管理', + ec_order_item: '订单明细', + ec_review: '商品评价', + fin_payment: '支付记录', + fin_settlement: '财务结算', + fin_reconciliation: '财务对账', + cnt_content: '内容管理', + ntf_template: '通知模板', + cs_ticket: '客服工单', + platfrom_account: '平台账户', + platform_role: '平台角色', + platform_menu: '平台菜单', + wallet: '钱包', + wallet_ledger: '钱包流水', + wallet_recharge: '钱包充值', + wallet_withdrawal: '钱包提现', + report: '报表', + report_item: '报表项目', + report_metric_snapshot: '指标快照', + aud_operation_log: '操作审计', + aud_export_log: '导出审计', + aud_approval: '审批审计', +}; +const define = ( + name: string, + resource: string, + mode: ResourceMode, + pageKind: ResourcePageKind, + keys: string[], + detailActions?: DetailAction[], +): ResourceUiDefinition => { + const fields = keys.map(field); + return { + key: name.replace(/_/g, '-'), + name, + resource, + title: titles[name] ?? '业务资源', + mode, + pageKind, + fields, + requiredIdentities: fields + .filter((item) => item.required && item.key.endsWith('_identity')) + .map((item) => item.key), + ...(detailActions ? { detailActions } : {}), + }; +}; +const action = ( + name: string, + resource: string, + keys: string[], + payload?: Record, +): DetailAction => ({ + name, + resource, + fields: keys.map(field), + ...(payload ? { payload } : {}), +}); /** Exact UI contract for every backend ExpectedResources entry. */ export const resources: ResourceUiDefinition[] = [ - define('gas_basic', '/gas/gas_basic', 'writable', 'list', ['code!', 'name!', 'credit_code', 'principal', 'address', 'longitude', 'latitude']), - define('gas_account', '/gas/gas_account', 'writable', 'list', ['username!', 'password!', 'display_name', 'role_code', 'gas_basic_identity!']), - define('delivery_basic', '/delivery/delivery_basic', 'writable', 'list', ['delivery_code!', 'name!', 'gas_basic_identity', 'principal', 'address']), - define('delivery_account', '/delivery/delivery_account', 'writable', 'list', ['username!', 'password!', 'display_name', 'role_code', 'delivery_basic_identity!']), - define('delivery_task', '/delivery/delivery_task', 'writable', 'list', ['ec_order_identity!', 'delivery_basic_identity!', 'staff_account_identity']), - define('delivery_track', '/delivery/delivery_track', 'writable', 'list', ['delivery_task_identity!', 'started_at', 'completed_at']), - define('delivery_track_point', '/delivery/delivery_track_point', 'writable', 'list', ['delivery_track_identity!', 'point_type!', 'occurred_at!', 'longitude!', 'latitude!']), - define('staff_account', '/staff/account', 'writable', 'list', ['username!', 'password!', 'name!', 'phone', 'avatar', 'role_code', 'gas_basic_identity', 'delivery_basic_identity', 'work_status']), - define('staff_credential', '/staff/credential', 'writable', 'list', ['staff_account_identity!', 'credential_type!', 'credential_no', 'expired_at']), - define('user_account', '/user/account', 'writable', 'list', ['username!', 'password!', 'name!', 'phone', 'avatar', 'real_name']), - define('user_address', '/user/address', 'writable', 'list', ['user_account_identity!', 'address!', 'longitude', 'latitude', 'is_default']), - define('user_service_relation', '/user/service_relation', 'writable', 'list', ['user_account_identity!', 'gas_basic_identity', 'delivery_basic_identity', 'staff_account_identity']), - define('dev_smart_cylinder_valve', '/device/dev_smart_cylinder_valve', 'writable', 'list', ['device_no!', 'model', 'online_status', 'owner_identity']), - define('dev_device_binding', '/device/dev_device_binding', 'writable', 'list', ['smart_cylinder_valve_identity!', 'user_account_identity!', 'effective_at', 'expired_at']), - define('dev_telemetry', '/device/dev_telemetry', 'readonly', 'list', ['smart_cylinder_valve_identity', 'recorded_at', 'payload']), - define('saf_rule', '/safety/saf_rule', 'writable', 'list', ['rule_code!', 'version_no', 'threshold', 'action', 'gray_scope']), - define('saf_event', '/safety/saf_event', 'writable', 'list', ['event_code!', 'level!', 'title!', 'smart_cylinder_valve_identity!', 'sla_at'], [action('saf_event_disposal', '/safety/saf_event/:identity/disposals', ['action!', 'reason!'])]), - define('saf_inspection', '/safety/saf_inspection', 'writable', 'list', ['user_account_identity!', 'staff_account_identity!', 'result!', 'evidence_uri']), - define('saf_event_disposal', '/safety/saf_event/:identity/disposals', 'append_only', 'list', ['action!', 'reason!']), - define('ec_category', '/ec/ec_category', 'writable', 'tree', ['parent_identity', 'name!', 'sort_no']), - define('ec_product', '/ec/ec_product', 'writable', 'list', ['ec_category_identity!', 'product_code!', 'name!', 'price_amount!', 'stock_quantity']), - define('ec_product_attribute', '/ec/ec_product_attribute', 'writable', 'list', ['ec_product_identity!', 'name!', 'value!', 'sort_no']), - define('ec_product_image', '/ec/ec_product_image', 'writable', 'list', ['ec_product_identity!', 'image_uri!', 'sort_no', 'is_cover']), - define('ec_cart', '/ec/ec_cart', 'writable', 'list', ['user_account_identity!', 'ec_product_identity!', 'quantity!', 'selected']), - define('ec_order', '/ec/ec_order', 'writable', 'list', ['user_account_identity!', 'gas_basic_identity', 'delivery_basic_identity', 'order_no!', 'total_amount!']), - define('ec_order_item', '/ec/ec_order_item', 'writable', 'list', ['ec_order_identity!', 'ec_product_identity!', 'product_snapshot!', 'quantity!', 'sale_amount!']), - define('ec_review', '/ec/ec_review', 'writable', 'list', ['ec_order_identity!', 'ec_product_identity!', 'user_account_identity!', 'score!', 'content!']), - define('fin_payment', '/finance/fin_payment', 'writable', 'list', ['ec_order_identity!', 'channel!', 'amount!', 'paid_at']), - define('fin_settlement', '/finance/fin_settlement', 'writable', 'list', ['settlement_no!', 'subject_type!', 'subject_identity!', 'period_start!', 'period_end!']), - define('fin_reconciliation', '/finance/fin_reconciliation', 'writable', 'list', ['channel!', 'bill_date!', 'difference_amount!']), - define('cnt_content', '/content/cnt_content', 'writable', 'list', ['content_type!', 'title!', 'body!', 'version_no', 'publish_status']), - define('ntf_template', '/notification/ntf_template', 'writable', 'list', ['template_code!', 'channel!', 'content!']), - define('cs_ticket', '/customer_service/cs_ticket', 'writable', 'list', ['user_account_identity!', 'ticket_no!', 'category!', 'priority!']), - define('platfrom_account', '/platform/platfrom_account', 'writable', 'list', ['username!', 'password!', 'display_name', 'avatar', 'platform_role_code', 'phone']), - define('platform_role', '/platform/platform_role', 'writable', 'list', ['role_code!', 'name!', 'data_scope!']), - define('platform_menu', '/platform/platform_menu', 'writable', 'tree', ['parent_identity', 'menu_code!', 'name!', 'icon', 'path', 'sort_no']), - define('wallet', '/wallet/wallet', 'readonly', 'list', ['owner_identity', 'balance_amount', 'status']), - define('wallet_ledger', '/wallet/wallet_ledger', 'readonly', 'list', ['wallet_identity', 'change_amount', 'balance_after']), - define('wallet_recharge', '/wallet/wallet_recharge', 'readonly', 'list', ['wallet_identity', 'amount', 'status']), - define('wallet_withdrawal', '/wallet/wallet_withdrawal', 'readonly', 'list', ['wallet_identity', 'amount', 'status']), - define('report', '/report/report', 'readonly', 'list', ['report_code', 'name', 'status']), - define('report_item', '/report/report_item', 'readonly', 'list', ['report_identity', 'metric_code', 'value']), - define('report_metric_snapshot', '/report/report_metric_snapshot', 'readonly', 'list', ['report_identity', 'metric_code', 'value', 'captured_at']), - define('aud_operation_log', '/audit/aud_operation_log', 'readonly', 'list', ['operator_identity', 'action', 'object_identity', 'created_at']), - define('aud_export_log', '/audit/aud_export_log', 'readonly', 'list', ['operator_identity', 'resource_type', 'created_at']), - define('aud_approval', '/audit/aud_approval', 'readonly', 'list', ['operator_identity', 'status', 'handled_at']), + define('gas_basic', '/gas/gas_basic', 'writable', 'list', [ + 'code!', + 'name!', + 'credit_code', + 'principal', + 'address', + 'longitude', + 'latitude', + ]), + define('gas_account', '/gas/gas_account', 'writable', 'list', [ + 'username!', + 'password!', + 'display_name', + 'role_code', + 'gas_basic_identity!', + ]), + define('delivery_basic', '/delivery/delivery_basic', 'writable', 'list', [ + 'delivery_code!', + 'name!', + 'gas_basic_identity', + 'principal', + 'address', + ]), + define('delivery_account', '/delivery/delivery_account', 'writable', 'list', [ + 'username!', + 'password!', + 'display_name', + 'role_code', + 'delivery_basic_identity!', + ]), + define('delivery_task', '/delivery/delivery_task', 'writable', 'list', [ + 'ec_order_identity!', + 'delivery_basic_identity!', + 'staff_account_identity', + ]), + define('delivery_track', '/delivery/delivery_track', 'writable', 'list', [ + 'delivery_task_identity!', + 'started_at', + 'completed_at', + ]), + define( + 'delivery_track_point', + '/delivery/delivery_track_point', + 'readonly', + 'list', + [ + 'delivery_track_identity', + 'point_type', + 'occurred_at', + 'longitude', + 'latitude', + ], + ), + define('staff_account', '/staff/account', 'writable', 'list', [ + 'username!', + 'password!', + 'name!', + 'phone', + 'avatar', + 'role_code', + 'gas_basic_identity', + 'delivery_basic_identity', + 'work_status', + ]), + define('staff_credential', '/staff/credential', 'writable', 'list', [ + 'staff_account_identity!', + 'credential_type!', + 'credential_no', + 'expired_at', + ]), + define('user_account', '/user/account', 'writable', 'list', [ + 'username!', + 'password!', + 'name!', + 'phone', + 'avatar', + 'real_name', + ]), + define('user_address', '/user/address', 'writable', 'list', [ + 'user_account_identity!', + 'address!', + 'longitude', + 'latitude', + 'is_default', + ]), + define( + 'user_service_relation', + '/user/service_relation', + 'writable', + 'list', + [ + 'user_account_identity!', + 'gas_basic_identity', + 'delivery_basic_identity', + 'staff_account_identity', + ], + ), + define( + 'dev_smart_cylinder_valve', + '/device/dev_smart_cylinder_valve', + 'writable', + 'list', + ['device_no!', 'model', 'online_status', 'owner_identity'], + ), + define( + 'dev_device_binding', + '/device/dev_device_binding', + 'writable', + 'list', + [ + 'smart_cylinder_valve_identity!', + 'user_account_identity!', + 'effective_at', + 'expired_at', + ], + ), + define('dev_telemetry', '/device/dev_telemetry', 'readonly', 'list', [ + 'smart_cylinder_valve_identity', + 'reported_at', + 'payload', + ]), + define('saf_rule', '/safety/saf_rule', 'writable', 'list', [ + 'rule_code!', + 'version_no', + 'threshold', + 'action', + 'gray_scope', + ]), + define( + 'saf_event', + '/safety/saf_event', + 'writable', + 'list', + [ + 'event_code!', + 'level!', + 'title!', + 'smart_cylinder_valve_identity!', + 'sla_at', + ], + [ + action('saf_event_disposal', '/safety/saf_event/:identity/disposals', [ + 'action!', + 'reason!', + ]), + ], + ), + define('saf_inspection', '/safety/saf_inspection', 'writable', 'list', [ + 'user_account_identity!', + 'staff_account_identity!', + 'result!', + 'evidence_uri', + ]), + define( + 'saf_event_disposal', + '/safety/saf_event/:identity/disposals', + 'append_only', + 'list', + ['action!', 'reason!'], + ), + define('ec_category', '/ec/ec_category', 'writable', 'tree', [ + 'parent_identity', + 'name!', + 'sort_no', + ]), + define('ec_product', '/ec/ec_product', 'writable', 'list', [ + 'ec_category_identity!', + 'product_code!', + 'name!', + 'price_amount!', + 'stock_quantity', + ]), + define( + 'ec_product_attribute', + '/ec/ec_product_attribute', + 'writable', + 'list', + ['ec_product_identity!', 'name!', 'value!', 'sort_no'], + ), + define('ec_product_image', '/ec/ec_product_image', 'writable', 'list', [ + 'ec_product_identity!', + 'image_uri!', + 'sort_no', + 'is_cover', + ]), + define('ec_cart', '/ec/ec_cart', 'writable', 'list', [ + 'user_account_identity!', + 'ec_product_identity!', + 'quantity!', + 'selected', + ]), + define('ec_order', '/ec/ec_order', 'writable', 'list', [ + 'user_account_identity!', + 'gas_basic_identity', + 'delivery_basic_identity', + 'order_no!', + 'total_amount!', + ]), + define('ec_order_item', '/ec/ec_order_item', 'writable', 'list', [ + 'ec_order_identity!', + 'ec_product_identity!', + 'product_snapshot!', + 'quantity!', + 'sale_amount!', + ]), + define('ec_review', '/ec/ec_review', 'writable', 'list', [ + 'ec_order_identity!', + 'ec_product_identity!', + 'user_account_identity!', + 'score!', + 'content!', + ]), + define('fin_payment', '/finance/fin_payment', 'writable', 'list', [ + 'ec_order_identity!', + 'channel!', + 'amount!', + 'paid_at', + ]), + define('fin_settlement', '/finance/fin_settlement', 'writable', 'list', [ + 'settlement_no!', + 'subject_type!', + 'subject_identity!', + 'period_start!', + 'period_end!', + ]), + define( + 'fin_reconciliation', + '/finance/fin_reconciliation', + 'writable', + 'list', + ['channel!', 'bill_date!', 'difference_amount!'], + ), + define('cnt_content', '/content/cnt_content', 'writable', 'list', [ + 'content_type!', + 'title!', + 'body!', + 'version_no', + 'publish_status', + ]), + define('ntf_template', '/notification/ntf_template', 'writable', 'list', [ + 'template_code!', + 'channel!', + 'content!', + ]), + define('cs_ticket', '/customer_service/cs_ticket', 'writable', 'list', [ + 'user_account_identity!', + 'ticket_no!', + 'category!', + 'priority!', + ]), + define('platfrom_account', '/platform/platfrom_account', 'writable', 'list', [ + 'username!', + 'password!', + 'display_name', + 'avatar', + 'platform_role_code', + 'phone', + ]), + define('platform_role', '/platform/platform_role', 'writable', 'list', [ + 'role_code!', + 'name!', + 'data_scope!', + ]), + define('platform_menu', '/platform/platform_menu', 'writable', 'tree', [ + 'parent_identity', + 'menu_code!', + 'name!', + 'icon', + 'path', + 'sort_no', + ]), + define('wallet', '/wallet/wallet', 'readonly', 'list', [ + 'owner_identity', + 'balance_amount', + 'frozen_amount', + 'status', + ]), + define('wallet_ledger', '/wallet/wallet_ledger', 'readonly', 'list', [ + 'wallet_identity', + 'amount', + 'balance_after', + ]), + define('wallet_recharge', '/wallet/wallet_recharge', 'readonly', 'list', [ + 'wallet_identity', + 'amount', + 'status', + ]), + define('wallet_withdrawal', '/wallet/wallet_withdrawal', 'readonly', 'list', [ + 'wallet_identity', + 'amount', + 'status', + ]), + define('report', '/report/report', 'readonly', 'list', [ + 'report_code', + 'report_type', + 'stat_period', + 'generated_at', + 'status', + ]), + define('report_item', '/report/report_item', 'readonly', 'list', [ + 'report_identity', + 'dimension', + 'metric_code', + 'metric_value', + ]), + define( + 'report_metric_snapshot', + '/report/report_metric_snapshot', + 'readonly', + 'list', + ['metric_code', 'scope_type', 'stat_at', 'metric_value'], + ), + define('aud_operation_log', '/audit/aud_operation_log', 'readonly', 'list', [ + 'operator_identity', + 'action', + 'object_identity', + 'created_at', + ]), + define('aud_export_log', '/audit/aud_export_log', 'readonly', 'list', [ + 'applicant_identity', + 'purpose', + 'field_scope', + 'approved_at', + 'file_uri', + ]), + define( + 'aud_approval', + '/audit/aud_approval', + 'readonly', + 'list', + [ + 'business_type', + 'business_identity', + 'applicant_identity', + 'opinion', + 'handler_identity', + 'status', + 'handled_at', + ], + [ + action('同意', '/audit/aud_approval/:identity/approve', ['opinion'], { + status: 'approved', + }), + action('驳回', '/audit/aud_approval/:identity/approve', ['opinion!'], { + status: 'rejected', + }), + ], + ), ]; -export const resourceByName = Object.fromEntries(resources.map((definition) => [definition.resource, definition])) as Record; +export const resourceByName = Object.fromEntries( + resources.map((definition) => [definition.resource, definition]), +) as Record; export function getResource(resourcePath: string): ResourceUiDefinition { const definition = resourceByName[resourcePath]; if (!definition) throw new Error('Unknown resource: ' + resourcePath); diff --git a/frontend/platform_admin/src/views/shared/CrudListPage.vue b/frontend/platform_admin/src/views/shared/CrudListPage.vue index 8b1d039..f3fdd17 100644 --- a/frontend/platform_admin/src/views/shared/CrudListPage.vue +++ b/frontend/platform_admin/src/views/shared/CrudListPage.vue @@ -1,37 +1,256 @@ + - + + diff --git a/frontend/platform_admin/src/views/shared/ReadOnlyListPage.vue b/frontend/platform_admin/src/views/shared/ReadOnlyListPage.vue index 015fdb7..1207ae1 100644 --- a/frontend/platform_admin/src/views/shared/ReadOnlyListPage.vue +++ b/frontend/platform_admin/src/views/shared/ReadOnlyListPage.vue @@ -1,25 +1,147 @@ + - + + diff --git a/frontend/platform_admin/src/views/shared/TreePage.vue b/frontend/platform_admin/src/views/shared/TreePage.vue index c8d05b2..b3ed7b2 100644 --- a/frontend/platform_admin/src/views/shared/TreePage.vue +++ b/frontend/platform_admin/src/views/shared/TreePage.vue @@ -1,17 +1,138 @@ - + +