From 20e3071789e1811bfec9362b6e71d3e46aeded4a Mon Sep 17 00:00:00 2001 From: yanweidong Date: Wed, 5 Aug 2026 13:58:16 +0800 Subject: [PATCH] feat(platform): improve admin data workflows --- backend/api/cmd/cli/main.go | 5 +- backend/api/internal/impl/new.go | 3 +- .../api/internal/logic/platform/gas/gas.go | 87 ++--- .../internal/logic/platform/gas/gas_test.go | 40 +++ .../api/internal/models/gas_basic_review.go | 23 -- backend/api/internal/routers/platform.go | 1 - backend/api/internal/routers/platform_test.go | 5 +- backend/api/internal/seed/mock.go | 307 +++++++++++++++++- backend/api/internal/seed/mock_test.go | 26 ++ backend/worker/internal/impl/new.go | 3 +- docs/05-平台总后台需求.md | 4 +- .../src/components/IdentityText.vue | 35 ++ .../src/components/menu/index.vue | 2 +- .../src/views/shared/CrudListPage.vue | 80 ++--- .../src/views/shared/ReadOnlyListPage.vue | 10 +- .../gas_admin/src/components/IdentityText.vue | 35 ++ .../gas_admin/src/components/menu/index.vue | 2 +- .../src/views/shared/CrudListPage.vue | 80 ++--- .../src/views/shared/ReadOnlyListPage.vue | 10 +- .../src/components/IdentityText.vue | 35 ++ .../src/components/menu/index.vue | 2 +- .../src/views/shared/CrudListPage.vue | 157 ++++----- .../src/views/shared/ReadOnlyListPage.vue | 10 +- 23 files changed, 666 insertions(+), 296 deletions(-) create mode 100644 backend/api/internal/logic/platform/gas/gas_test.go delete mode 100644 backend/api/internal/models/gas_basic_review.go create mode 100644 frontend/delivery_admin/src/components/IdentityText.vue create mode 100644 frontend/gas_admin/src/components/IdentityText.vue create mode 100644 frontend/platform_admin/src/components/IdentityText.vue diff --git a/backend/api/cmd/cli/main.go b/backend/api/cmd/cli/main.go index 99d7945..744db68 100644 --- a/backend/api/cmd/cli/main.go +++ b/backend/api/cmd/cli/main.go @@ -9,9 +9,11 @@ import ( "strings" "git.apinb.com/bsm-sdk/core/database" + dbsql "git.apinb.com/bsm-sdk/core/database/sql" "git.apinb.com/bsm-sdk/core/types" "git.apinb.com/bsm-sdk/core/vars" "git.apinb.com/heqiapp/platforms/backend/api/internal/config" + "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "git.apinb.com/heqiapp/platforms/backend/api/internal/initdb" deliverylogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/delivery" gaslogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/gas" @@ -162,11 +164,12 @@ func writeMockData() error { databaseService, err := database.NewDatabase( config.Spec.Databases.Driver, config.Spec.Databases.Source, - nil, + dbsql.SetOptions(nil), ) if err != nil { return fmt.Errorf("connect database: %w", err) } + impl.DBService = databaseService if err := initdb.New(); err != nil { return fmt.Errorf("initialize platform data: %w", err) } diff --git a/backend/api/internal/impl/new.go b/backend/api/internal/impl/new.go index 9c165c9..3129027 100644 --- a/backend/api/internal/impl/new.go +++ b/backend/api/internal/impl/new.go @@ -3,6 +3,7 @@ package impl import ( "git.apinb.com/bsm-sdk/core/cache/redis" + dbsql "git.apinb.com/bsm-sdk/core/database/sql" "git.apinb.com/bsm-sdk/core/logger" "git.apinb.com/bsm-sdk/core/with" "git.apinb.com/heqiapp/platforms/backend/api/internal/config" @@ -21,6 +22,6 @@ func NewImpl() { MemoryService = with.Memory(nil) RedisService = with.RedisCache(config.Spec.Cache) // HTTP 服务启动只建立连接。表结构迁移由 platform-cli migrate 显式执行, - DBService = with.Databases(config.Spec.Databases, nil) + DBService = with.Databases(config.Spec.Databases, dbsql.SetOptions(nil)) logger.New(nil) } diff --git a/backend/api/internal/logic/platform/gas/gas.go b/backend/api/internal/logic/platform/gas/gas.go index 3031935..04dc71d 100644 --- a/backend/api/internal/logic/platform/gas/gas.go +++ b/backend/api/internal/logic/platform/gas/gas.go @@ -1,25 +1,43 @@ package gas import ( - "errors" - "strings" - "time" - "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" - "gorm.io/gorm" - "gorm.io/gorm/clause" ) // ListGasBasic 查询可燃气体站分页列表。 func ListGasBasic(ctx *gin.Context) { common.ListPage[models.GasBasic](ctx) } // GetGasBasic 查询一个可燃气体站。 -func GetGasBasic(ctx *gin.Context) { common.GetByIdentity[models.GasBasic](ctx) } +func GetGasBasic(ctx *gin.Context) { + var station models.GasBasic + if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&station).Error; err != nil { + common.RespondRecordError(ctx, err) + return + } + response, err := gasBasicDetailResponse(station, common.HasPreciseLocationScope(ctx)) + if err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, response) +} + +// gasBasicDetailResponse 仅向具备精确定位范围的后台账号返回气站地址和坐标。 +func gasBasicDetailResponse(station models.GasBasic, retainPreciseLocation bool) (any, error) { + response, err := common.PublicResourceResponse(station) + if err != nil { + return nil, err + } + if !retainPreciseLocation { + common.ProtectPublicFields(response, false, false, false) + } + return response, nil +} // CreateGasBasic 创建可燃气体站档案。 func CreateGasBasic(ctx *gin.Context) { @@ -28,7 +46,7 @@ func CreateGasBasic(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } - request.Entity = common.NewEntity(common.StatusDraft) + request.Entity = common.NewEntity(common.StatusEnable) if err := impl.DBService.Create(&request).Error; err != nil { infra.Response.Error(ctx, err) return @@ -64,7 +82,7 @@ func UpdateGasBasicStatus(ctx *gin.Context) { return } result := impl.DBService.Model(&models.GasBasic{}). - Where("identity = ? AND status IN ?", ctx.Param("identity"), []int{common.StatusEnable, common.StatusDisable}). + Where("identity = ? AND status IN ?", ctx.Param("identity"), []int{common.StatusDraft, common.StatusEnable, common.StatusDisable}). Update("status", request.Status) if result.Error != nil { infra.Response.Error(ctx, result.Error) @@ -76,54 +94,3 @@ func UpdateGasBasicStatus(ctx *gin.Context) { } infra.Response.Success(ctx, gin.H{"updated": true}) } - -// ReviewGasBasic 审核待审核气站;不通过时必须填写理由。 -func ReviewGasBasic(ctx *gin.Context) { - var request struct { - Approved bool `json:"approved"` - Reason string `json:"reason" binding:"max=2000"` - } - if err := ctx.ShouldBindJSON(&request); err != nil || - (!request.Approved && strings.TrimSpace(request.Reason) == "") { - infra.Response.Error(ctx, errcode.ErrInvalidArgument) - return - } - - operatorIdentity, operatorName := common.PlatformOperator(ctx) - targetStatus := common.StatusDisable - reviewStatus := common.StatusRejected - if request.Approved { - targetStatus = common.StatusEnable - reviewStatus = common.StatusApproved - } - now := time.Now() - err := impl.DBService.Transaction(func(tx *gorm.DB) error { - var station models.GasBasic - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). - Where("identity = ? AND status <> ?", ctx.Param("identity"), common.StatusArchived). - First(&station).Error; err != nil { - return err - } - if station.Status != common.StatusDraft { - return errors.New("gas station is not pending review") - } - if err := tx.Model(&station).Update("status", targetStatus).Error; err != nil { - return err - } - review := models.GasBasicReview{ - Entity: common.NewEntity(common.StatusEnable), - GasBasicID: station.ID, - ReviewStatus: reviewStatus, - ReviewReason: strings.TrimSpace(request.Reason), - ReviewerIdentity: operatorIdentity, - ReviewerName: operatorName, - ReviewedAt: now, - } - return tx.Create(&review).Error - }) - if err != nil { - common.RespondRecordError(ctx, err) - return - } - infra.Response.Success(ctx, gin.H{"updated": true, "status": targetStatus}) -} diff --git a/backend/api/internal/logic/platform/gas/gas_test.go b/backend/api/internal/logic/platform/gas/gas_test.go new file mode 100644 index 0000000..da9fda6 --- /dev/null +++ b/backend/api/internal/logic/platform/gas/gas_test.go @@ -0,0 +1,40 @@ +package gas + +import ( + "testing" + + "git.apinb.com/heqiapp/platforms/backend/api/internal/models" +) + +func TestGasBasicDetailPreciseLocationProjection(t *testing.T) { + station := models.GasBasic{ + Entity: models.Entity{ID: 1, Identity: "00000000-0000-7000-8000-000000000001"}, + Code: "GAS-001", + Name: "示例气站", + Address: "示例地址", + Longitude: "121.5000", + Latitude: "31.2000", + } + + precise, err := gasBasicDetailResponse(station, true) + if err != nil { + t.Fatalf("project precise detail: %v", err) + } + preciseMap := precise.(map[string]any) + for key := range map[string]struct{}{"address": {}, "longitude": {}, "latitude": {}} { + if preciseMap[key] == nil || preciseMap[key] == "" { + t.Fatalf("precise detail missing %s: %#v", key, preciseMap) + } + } + + restricted, err := gasBasicDetailResponse(station, false) + if err != nil { + t.Fatalf("project restricted detail: %v", err) + } + restrictedMap := restricted.(map[string]any) + for _, key := range []string{"address", "longitude", "latitude"} { + if _, exists := restrictedMap[key]; exists { + t.Fatalf("restricted detail exposed %s: %#v", key, restrictedMap) + } + } +} diff --git a/backend/api/internal/models/gas_basic_review.go b/backend/api/internal/models/gas_basic_review.go deleted file mode 100644 index 04e05ff..0000000 --- a/backend/api/internal/models/gas_basic_review.go +++ /dev/null @@ -1,23 +0,0 @@ -package models - -import ( - "time" - - "git.apinb.com/bsm-sdk/core/database" -) - -// GasBasicReview 对应 gas_basic_review,保存气站审核记录。 -type GasBasicReview struct { - Entity // 公共实体字段 - GasBasicID uint64 `gorm:"column:gas_basic_id;not null;index" json:"gas_basic_id"` // 气站自增主键 - ReviewStatus int `gorm:"column:review_status;not null;index" json:"review_status"` // 审核结果:已通过或已驳回 - ReviewReason string `gorm:"column:review_reason;type:text;not null;default:''" json:"review_reason"` // 审核不通过理由 - ReviewerIdentity string `gorm:"column:reviewer_identity;type:varchar(36);not null;default:'';index" json:"reviewer_identity"` // 审核人业务标识 - ReviewerName string `gorm:"column:reviewer_name;type:varchar(64);not null;default:''" json:"reviewer_name"` // 审核人姓名快照 - ReviewedAt time.Time `gorm:"column:reviewed_at;type:timestamptz;not null" json:"reviewed_at"` // 审核时间 -} - -func init() { database.AppendMigrate(&GasBasicReview{}) } - -// TableName 返回与模型、文件名一致的单数数据表名。 -func (table *GasBasicReview) TableName() string { return "gas_basic_review" } diff --git a/backend/api/internal/routers/platform.go b/backend/api/internal/routers/platform.go index ba38f64..7199a87 100644 --- a/backend/api/internal/routers/platform.go +++ b/backend/api/internal/routers/platform.go @@ -56,7 +56,6 @@ func registerGasRoute(group *gin.RouterGroup) { basic.GET("/:identity", gas.GetGasBasic) basic.PUT("/:identity", gas.UpdateGasBasic) basic.PATCH("/:identity/status", gas.UpdateGasBasicStatus) - basic.POST("/:identity/review", gas.ReviewGasBasic) basic.DELETE("/:identity", func(ctx *gin.Context) { common.ArchiveRecord(ctx, &models.GasBasic{}) }) registerWritableResource(group, "/gas_account", gas.ListGasAccount, gas.CreateGasAccount, gas.GetGasAccount, gas.UpdateGasAccount, &models.GasAccount{}) } diff --git a/backend/api/internal/routers/platform_test.go b/backend/api/internal/routers/platform_test.go index 0ff9b35..396c908 100644 --- a/backend/api/internal/routers/platform_test.go +++ b/backend/api/internal/routers/platform_test.go @@ -63,16 +63,15 @@ func TestPlatformGasRouteUsesGasBasic(t *testing.T) { t.Fatal("gas_basic list route is not registered") } -func TestPlatformGasRouteExposesReview(t *testing.T) { +func TestPlatformGasRouteDoesNotExposeReview(t *testing.T) { engine := gin.New() RegisterPlatform("heqi", engine) for _, route := range engine.Routes() { if route.Method == http.MethodPost && route.Path == "/heqi/platform/v1/gas_basic/:identity/review" { - return + t.Fatal("gas_basic review route must not be registered") } } - t.Fatal("gas_basic review route is not registered") } func TestPlatformOrganizationAndAccountRoutesExposeResourceCRUD(t *testing.T) { diff --git a/backend/api/internal/seed/mock.go b/backend/api/internal/seed/mock.go index d01ce5f..a20264f 100644 --- a/backend/api/internal/seed/mock.go +++ b/backend/api/internal/seed/mock.go @@ -436,10 +436,311 @@ func MockData(database *gorm.DB) error { return err } - return ensureMockRoot(tx, string(passwordHash)) + attendance := models.StaffAttendance{ + Entity: entity(48, common.StatusEnable), StaffAccountID: staff.ID, + RoleCode: staff.RoleCode, Action: "clock_in", OccurredAt: yesterday, + Longitude: gas.Longitude, Latitude: gas.Latitude, + DeviceIdentity: "mock-staff-device-001", RequestNo: "MOCK-ATTENDANCE-001", + } + if err := put(tx, &attendance); err != nil { + return err + } + + producer := models.ProducerAccount{ + Entity: entity(49, common.StatusEnable), ProducerCode: "MOCK-PRODUCER-001", + Name: "和气示例生产企业", CreditCode: "91310000MOCKPROD01", + Principal: "周厂长", Phone: "13600000001", Address: "上海市示例工业路 6 号", + Username: "mock_producer_admin", DisplayName: "示例生产管理员", + PasswordHash: string(passwordHash), RoleCode: "admin", Remark: "仅用于开发联调", + } + if err := put(tx, &producer); err != nil { + return err + } + + recharge := models.WalletRechargeOrder{ + Entity: entity(50, common.StatusEnable), RechargeStatus: common.StatusCompleted, + WalletBasicID: wallet.ID, RechargeNo: "MOCK-RECHARGE-001", + RequestNo: "MOCK-REQ-RECHARGE-001", Amount: 50000, Channel: "mock", + OwnerType: wallet.OwnerType, OwnerIdentity: wallet.OwnerIdentity, CompletedAt: &now, + } + if err := put(tx, &recharge); err != nil { + return err + } + + refundItem := models.PaymentRefundItem{ + Identity: entity(51, common.StatusEnable).Identity, PaymentRefundID: refund.ID, + OrderItemIdentity: gasOrderItem.Identity, Quantity: 1, Amount: refund.Amount, + } + if err := put(tx, &refundItem); err != nil { + return err + } + + contentRead := models.CmsContentRead{ + Entity: entity(52, common.StatusEnable), UserAccountID: user.ID, CmsContentID: content.ID, + VersionNo: content.VersionNo, ShownAt: yesterday, ConfirmedAt: &now, + ClientVersion: "mock-1.0.0", DeviceIdentity: "mock-user-device-001", + RequestNo: "MOCK-CONTENT-READ-001", + } + if err := put(tx, &contentRead); err != nil { + return err + } + + evidence := models.CsTicketEvidence{ + Entity: entity(53, common.StatusEnable), CsTicketID: ticket.ID, + EvidenceType: "created", MediaType: "image", FileURI: "/mock/tickets/ticket-001.png", + CapturedAt: yesterday, ReceivedAt: now, Longitude: address.Longitude, Latitude: address.Latitude, + Source: "mock", IntegrityStatus: "verified", OperatorIdentity: staff.Identity, + RequestNo: "MOCK-TICKET-EVIDENCE-001", + } + if err := put(tx, &evidence); err != nil { + return err + } + + command := models.IotCommand{ + Entity: entity(54, common.StatusEnable), DeviceIdentity: productInfo.Identity, + DeviceID: "0000000000000001", IdempotencyKey: "MOCK-IOT-COMMAND-001", + Action: "query_status", RequestPayload: `{"source":"mock-data"}`, + CommandStatus: "pending", ExpiresAt: now.Add(10 * time.Minute), + } + if err := put(tx, &command); err != nil { + return err + } + + deviceMessage := models.IotDeviceMessage{ + Identity: entity(55, common.StatusEnable).Identity, DeviceID: command.DeviceID, + Topic: "mock/device/0000000000000001/up", MessageType: "telemetry", + PayloadHex: "0000", DecodedFrame: `{"source":"mock-data"}`, + DeviceOccurredAt: &yesterday, ReceivedAt: now, + } + if err := put(tx, &deviceMessage); err != nil { + return err + } + + outbox := models.IotOutbox{ + Identity: entity(56, common.StatusEnable).Identity, CommandIdentity: command.Identity, + EventType: "iot.command.requested", Payload: `{"source":"mock-data"}`, + OutboxStatus: "pending", AvailableAt: now, + } + if err := put(tx, &outbox); err != nil { + return err + } + + if err := ensureMockRoot(tx, string(passwordHash)); err != nil { + return err + } + var rootRole models.PlatformRole + if err := tx.Where("role_code = ?", "root").First(&rootRole).Error; err != nil { + return fmt.Errorf("find root role for mock menu: %w", err) + } + roleMenu := models.PlatformRoleMenu{PlatformRoleID: rootRole.ID, MenuIdentity: "dashboard_overview"} + if err := tx.Where("platform_role_id = ? AND menu_identity = ?", rootRole.ID, roleMenu.MenuIdentity).FirstOrCreate(&roleMenu).Error; err != nil { + return fmt.Errorf("seed root role menu: %w", err) + } + + return seedAdditionalCoreScenarios(tx, string(passwordHash), now) }) } +// seedAdditionalCoreScenarios 补齐十组可独立检索的核心主数据。 +// 每组记录都使用本组父表的数据库 ID 建立关联,避免仅有展示字段而无真实关系。 +func seedAdditionalCoreScenarios(database *gorm.DB, passwordHash string, now time.Time) error { + for scenario := 2; scenario <= 10; scenario++ { + sequence := 1000 + scenario*100 + suffix := fmt.Sprintf("%03d", scenario) + phoneSuffix := fmt.Sprintf("%08d", scenario) + + gas := models.GasBasic{ + Entity: entity(sequence+1, common.StatusEnable), Code: "MOCK-GAS-" + suffix, + Name: fmt.Sprintf("和气示例气站 %d", scenario), CreditCode: "91310000MOCKGAS" + suffix, + Principal: fmt.Sprintf("示例站长%d", scenario), Address: fmt.Sprintf("上海市示例路 %d 号", scenario), + Longitude: fmt.Sprintf("121.5%03d", scenario), Latitude: fmt.Sprintf("31.2%03d", scenario), + } + if err := put(database, &gas); err != nil { + return err + } + + delivery := models.DeliveryBasic{ + Entity: entity(sequence+2, common.StatusEnable), DeliveryCode: "MOCK-DELIVERY-" + suffix, + GasBasicID: gas.ID, Name: fmt.Sprintf("和气示例配送点 %d", scenario), + Principal: fmt.Sprintf("示例主管%d", scenario), Address: fmt.Sprintf("上海市配送路 %d 号", scenario), + } + if err := put(database, &delivery); err != nil { + return err + } + + staff := models.StaffAccount{ + Entity: entity(sequence+3, common.StatusEnable), Username: "mock_driver_" + suffix, + PasswordHash: passwordHash, Name: fmt.Sprintf("示例配送员%d", scenario), Phone: "139" + phoneSuffix, + RoleCode: "delivery", GasBasicID: gas.ID, DeliveryBasicID: delivery.ID, WorkStatus: "on_duty", + } + if err := put(database, &staff); err != nil { + return err + } + + user := models.UserAccount{ + Entity: entity(sequence+4, common.StatusEnable), Username: "mock_customer_" + suffix, + PasswordHash: passwordHash, Name: fmt.Sprintf("示例客户%d", scenario), Phone: "138" + phoneSuffix, + RealName: fmt.Sprintf("模拟用户%d", scenario), + } + if err := put(database, &user); err != nil { + return err + } + + address := models.UserAddress{ + Entity: entity(sequence+5, common.StatusEnable), UserAccountID: user.ID, + Address: fmt.Sprintf("上海市客户路 %d 号", scenario), Longitude: gas.Longitude, + Latitude: gas.Latitude, IsDefault: true, + } + if err := put(database, &address); err != nil { + return err + } + + relation := models.UserServiceRelation{ + Entity: entity(sequence+6, common.StatusEnable), UserAccountID: user.ID, + GasBasicID: gas.ID, DeliveryBasicID: delivery.ID, StaffAccountID: staff.ID, + } + if err := put(database, &relation); err != nil { + return err + } + + productType := models.ProductType{ + Entity: entity(sequence+7, common.StatusEnable), Code: "MOCK-TYPE-" + suffix, + Name: fmt.Sprintf("示例智能瓶阀类型 %d", scenario), + } + if err := put(database, &productType); err != nil { + return err + } + + warehouse := models.ProductWarehouse{ + Entity: entity(sequence+8, common.StatusEnable), Code: "MOCK-WH-" + suffix, + Name: fmt.Sprintf("示例库房 %d", scenario), Address: gas.Address, + Manager: fmt.Sprintf("示例库管%d", scenario), Phone: "137" + phoneSuffix, + } + if err := put(database, &warehouse); err != nil { + return err + } + + product := models.ProductInfo{ + Entity: entity(sequence+9, common.StatusEnable), Code: "MOCK-CYLINDER-" + suffix, + Name: fmt.Sprintf("示例智能瓶阀 %d", scenario), ProductStatus: common.StatusInStock, + ProductTypeID: productType.ID, Params: `{"source":"mock-data"}`, + WarehouseID: warehouse.ID, ProducedAt: now.AddDate(-1, 0, scenario), + } + if err := put(database, &product); err != nil { + return err + } + + contract := models.GasorderContract{ + Entity: entity(sequence+10, common.StatusEnable), ContractStatus: common.StatusActive, + ContractNo: "MOCK-CONTRACT-" + suffix, UserAccountID: user.ID, GasBasicID: gas.ID, + DeliveryBasicID: delivery.ID, Title: fmt.Sprintf("示例供气合同 %d", scenario), + Terms: "仅用于开发联调。", DefaultDeliveryFee: 500, + SignedAt: now, EffectiveAt: now, + } + if err := put(database, &contract); err != nil { + return err + } + + contractProduct := models.GasorderContractProduct{ + Entity: entity(sequence+11, common.StatusEnable), GasorderContractID: contract.ID, + ProductInfoID: product.ID, ProductCode: product.Code, ProductTypeName: productType.Name, + ProductParams: product.Params, UnitPrice: int64(9000 + scenario*100), BoundAt: now, + } + if err := put(database, &contractProduct); err != nil { + return err + } + + gasOrder := models.GasorderBasic{ + Entity: entity(sequence+12, common.StatusEnable), OrderStatus: common.StatusPending, + OrderNo: "MOCK-GASORDER-" + suffix, RequestNo: "MOCK-REQ-GASORDER-" + suffix, + GasorderContractID: contract.ID, UserAccountID: user.ID, CreatorType: "user", CreatorID: user.ID, + CreatorIdentity: user.Identity, GasBasicID: gas.ID, DeliveryBasicID: delivery.ID, + StaffAccountID: staff.ID, Address: address.Address, Longitude: address.Longitude, Latitude: address.Latitude, + ContactName: user.Name, ContactPhone: user.Phone, ProductAmount: contractProduct.UnitPrice, + DeliveryFee: 500, PayableAmount: contractProduct.UnitPrice + 500, + OperatorIdentity: user.Identity, OperatorName: user.Name, Remark: "模拟待处理订单", + } + if err := put(database, &gasOrder); err != nil { + return err + } + + gasOrderItem := models.GasorderItem{ + Entity: entity(sequence+13, common.StatusEnable), GasorderBasicID: gasOrder.ID, + GasorderContractProductID: contractProduct.ID, ProductInfoID: product.ID, + ProductCode: product.Code, ProductTypeName: productType.Name, + ProductParams: product.Params, UnitPrice: contractProduct.UnitPrice, + } + if err := put(database, &gasOrderItem); err != nil { + return err + } + + category := models.EcCategory{ + Entity: entity(sequence+14, common.StatusEnable), Name: fmt.Sprintf("示例商品分类 %d", scenario), SortNo: scenario, + } + if err := put(database, &category); err != nil { + return err + } + + ecProduct := models.EcProduct{ + Entity: entity(sequence+15, common.StatusEnable), EcCategoryID: category.ID, + ProductCode: "MOCK-EC-PRODUCT-" + suffix, Name: fmt.Sprintf("示例配送商品 %d", scenario), + PriceAmount: gasOrder.PayableAmount, StockQuantity: 20 + scenario, + } + if err := put(database, &ecProduct); err != nil { + return err + } + + ecOrder := models.EcOrder{ + Entity: entity(sequence+16, common.StatusEnable), OrderStatus: common.StatusPending, + OrderNo: "MOCK-ECORDER-" + suffix, UserAccountID: user.ID, + GasStationID: gas.ID, DeliveryPointID: delivery.ID, TotalAmount: ecProduct.PriceAmount, + } + if err := put(database, &ecOrder); err != nil { + return err + } + + wallet := models.WalletBasic{ + Entity: entity(sequence+17, common.StatusEnable), OwnerType: "user", OwnerID: user.ID, + OwnerIdentity: user.Identity, AlipayID: "mock-" + suffix + "@example.invalid", AlipayName: user.Name, + WxpayID: "mock_customer_" + suffix, WxpayName: user.Name, PayPasswordHash: passwordHash, + Balance: int64(scenario * 10000), WithdrawalBalance: int64(scenario * 5000), + } + if err := put(database, &wallet); err != nil { + return err + } + + content := models.CmsContent{ + Entity: entity(sequence+18, common.StatusEnable), ContentType: "notice", + Title: fmt.Sprintf("模拟公告 %d", scenario), Body: "本内容由 mock-data 生成。", + VersionNo: 1, PublishStatus: "published", + } + if err := put(database, &content); err != nil { + return err + } + + ticket := models.CsTicket{ + Entity: entity(sequence+19, common.StatusEnable), TicketStatus: common.StatusOpen, + TicketNo: "MOCK-TICKET-" + suffix, UserAccountID: user.ID, Category: "delivery", Priority: "normal", + } + if err := put(database, &ticket); err != nil { + return err + } + + producer := models.ProducerAccount{ + Entity: entity(sequence+20, common.StatusEnable), ProducerCode: "MOCK-PRODUCER-" + suffix, + Name: fmt.Sprintf("示例生产企业 %d", scenario), CreditCode: "91310000MOCKPROD" + suffix, + Principal: fmt.Sprintf("示例厂长%d", scenario), Phone: "136" + phoneSuffix, + Address: fmt.Sprintf("上海市工业路 %d 号", scenario), Username: "mock_producer_" + suffix, + DisplayName: fmt.Sprintf("示例生产管理员%d", scenario), PasswordHash: passwordHash, + RoleCode: "admin", Remark: "仅用于开发联调", + } + if err := put(database, &producer); err != nil { + return err + } + } + return nil +} + // ensureMockRoot 复用已有 root 账号,仅在账号不存在时写入默认 root 用户。 func ensureMockRoot(database *gorm.DB, passwordHash string) error { role := models.PlatformRole{ @@ -493,6 +794,10 @@ func identityOf(value any) string { if !record.IsValid() { return "" } + directIdentity := record.FieldByName("Identity") + if directIdentity.IsValid() && directIdentity.Kind() == reflect.String { + return directIdentity.String() + } entityField := record.FieldByName("Entity") if !entityField.IsValid() { return "" diff --git a/backend/api/internal/seed/mock_test.go b/backend/api/internal/seed/mock_test.go index 61ace66..255992f 100644 --- a/backend/api/internal/seed/mock_test.go +++ b/backend/api/internal/seed/mock_test.go @@ -41,3 +41,29 @@ func TestIdentityOfEmbeddedEntity(t *testing.T) { t.Fatalf("identityOf(nil) = %q, want empty", got) } } + +func TestIdentityOfDirectIdentityField(t *testing.T) { + record := struct { + Identity string + }{Identity: "mock-direct-identity"} + if got := identityOf(&record); got != record.Identity { + t.Fatalf("identityOf() = %q, want %q", got, record.Identity) + } +} + +func TestAdditionalScenarioIdentitiesDoNotOverlapBaseScenario(t *testing.T) { + seen := make(map[string]struct{}, 226) + for sequence := 1; sequence <= 56; sequence++ { + seen[entity(sequence, common.StatusEnable).Identity] = struct{}{} + } + for scenario := 2; scenario <= 10; scenario++ { + base := 1000 + scenario*100 + for offset := 1; offset <= 20; offset++ { + identity := entity(base+offset, common.StatusEnable).Identity + if _, exists := seen[identity]; exists { + t.Fatalf("duplicate identity %q", identity) + } + seen[identity] = struct{}{} + } + } +} diff --git a/backend/worker/internal/impl/new.go b/backend/worker/internal/impl/new.go index 2826c7e..b84c396 100644 --- a/backend/worker/internal/impl/new.go +++ b/backend/worker/internal/impl/new.go @@ -3,6 +3,7 @@ package impl import ( "git.apinb.com/bsm-sdk/core/cache/redis" + dbsql "git.apinb.com/bsm-sdk/core/database/sql" "git.apinb.com/bsm-sdk/core/logger" "git.apinb.com/bsm-sdk/core/with" "git.apinb.com/heqiapp/platforms/backend/worker/internal/config" @@ -20,6 +21,6 @@ var ( func NewImpl() { MemoryService = with.Memory(nil) RedisService = with.RedisCache(config.Spec.Cache) - DBService = with.Databases(config.Spec.Databases, nil) + DBService = with.Databases(config.Spec.Databases, dbsql.SetOptions(nil)) logger.New(nil) } diff --git a/docs/05-平台总后台需求.md b/docs/05-平台总后台需求.md index 3dd2eef..b18aee1 100644 --- a/docs/05-平台总后台需求.md +++ b/docs/05-平台总后台需求.md @@ -113,12 +113,12 @@ | 资源 | 路径 | 模式 | 已实现能力 | | --- | --- | --- | --- | -| 气站 | `/gas_basic` | 可写 | 新增草稿、详情、编辑、审核、启停、归档;管理气站账户和钱包 | +| 气站 | `/gas_basic` | 可写 | 平台后台新增、详情、编辑、启停、归档;管理气站账户和钱包 | | 气站账户 | `/gas_account` | 可写 | 账号新增、编辑、启停和归档;必须关联气站 | | 配送点 | `/delivery_basic` | 可写 | 新增、编辑、启停和归档;可关联气站;管理配送点账户和钱包 | | 配送点账户 | `/delivery_account` | 可写 | 账号新增、编辑、启停和归档;必须关联配送点 | -气站创建后为草稿状态。气站审核通过后进入启用,审核不通过进入停用且必须填写理由;只有已启用或已停用气站可继续切换启停状态。审核过程写入独立审核记录。 +气站只能由平台总后台新增,创建后直接进入启用状态,无需审核。已启用或已停用气站可继续切换启停状态。 ### 6.2 工作人员管理 diff --git a/frontend/delivery_admin/src/components/IdentityText.vue b/frontend/delivery_admin/src/components/IdentityText.vue new file mode 100644 index 0000000..20cd9fa --- /dev/null +++ b/frontend/delivery_admin/src/components/IdentityText.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/frontend/delivery_admin/src/components/menu/index.vue b/frontend/delivery_admin/src/components/menu/index.vue index 096c46d..c4f6a77 100644 --- a/frontend/delivery_admin/src/components/menu/index.vue +++ b/frontend/delivery_admin/src/components/menu/index.vue @@ -133,7 +133,7 @@ export default defineComponent({ v-model:open-keys={openKeys.value} show-collapse-button={appStore.device !== 'mobile'} auto-open={false} - selected-keys={selectedKey.value} + selectedKeys={selectedKey.value} auto-open-selected={true} level-indent={34} style="height: 100%;width:100%;" diff --git a/frontend/delivery_admin/src/views/shared/CrudListPage.vue b/frontend/delivery_admin/src/views/shared/CrudListPage.vue index 373391b..129b08c 100644 --- a/frontend/delivery_admin/src/views/shared/CrudListPage.vue +++ b/frontend/delivery_admin/src/views/shared/CrudListPage.vue @@ -31,10 +31,13 @@