// Package seed writes linked development data without replacing existing rows. package seed import ( "errors" "fmt" "reflect" "time" "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) const mockIdentityPrefix = "00000000-0000-7000-8000-" // MockData idempotently writes one connected development scenario across all // domain tables. Fixed identities and unique business numbers make reruns safe. func MockData(database *gorm.DB) error { passwordHash, err := bcrypt.GenerateFromPassword([]byte("Mock@123456"), bcrypt.DefaultCost) if err != nil { return fmt.Errorf("hash mock password: %w", err) } now := time.Date(2026, time.July, 1, 9, 0, 0, 0, time.Local) yesterday := now.Add(-24 * time.Hour) nextYear := now.AddDate(1, 0, 0) return database.Transaction(func(tx *gorm.DB) error { gas := models.GasBasic{ Entity: entity(1, common.StatusEnable), Code: "MOCK-GAS-001", Name: "和气示例气站", CreditCode: "91310000MOCKGAS001", Principal: "张站长", Address: "上海市浦东新区示例路 1 号", Longitude: "121.5440", Latitude: "31.2210", } if err := put(tx, &gas); err != nil { return err } gasAccount := models.GasAccount{ Entity: entity(2, common.StatusEnable), GasBasicID: gas.ID, Username: "mock_gas_admin", DisplayName: "示例气站管理员", PasswordHash: string(passwordHash), RoleCode: "admin", } if err := put(tx, &gasAccount); err != nil { return err } delivery := models.DeliveryBasic{ Entity: entity(3, common.StatusEnable), DeliveryCode: "MOCK-DELIVERY-001", GasBasicID: gas.ID, Name: "和气示例配送点", Principal: "李主管", Address: "上海市浦东新区示例路 18 号", } if err := put(tx, &delivery); err != nil { return err } deliveryAccount := models.DeliveryAccount{ Entity: entity(4, common.StatusEnable), DeliveryBasicID: delivery.ID, Username: "mock_delivery_admin", DisplayName: "示例配送点管理员", PasswordHash: string(passwordHash), RoleCode: "admin", } if err := put(tx, &deliveryAccount); err != nil { return err } staff := models.StaffAccount{ Entity: entity(5, common.StatusEnable), Username: "mock_driver", PasswordHash: string(passwordHash), Name: "王师傅", Phone: "13900000001", RoleCode: "delivery", GasBasicID: gas.ID, DeliveryBasicID: delivery.ID, WorkStatus: "on_duty", } if err := put(tx, &staff); err != nil { return err } credential := models.StaffCredential{ Entity: entity(6, common.StatusEnable), StaffAccountID: staff.ID, CredentialType: "delivery", CredentialNo: "MOCK-CERT-001", ExpiredAt: &nextYear, } if err := put(tx, &credential); err != nil { return err } user := models.UserAccount{ Entity: entity(7, common.StatusEnable), Username: "mock_customer", PasswordHash: string(passwordHash), Name: "陈女士", Phone: "13800000001", RealName: "陈示例", } if err := put(tx, &user); err != nil { return err } address := models.UserAddress{ Entity: entity(8, common.StatusEnable), UserAccountID: user.ID, Address: "上海市浦东新区客户路 88 号", Longitude: "121.5500", Latitude: "31.2250", IsDefault: true, } if err := put(tx, &address); err != nil { return err } serviceRelation := models.UserServiceRelation{ Entity: entity(9, common.StatusEnable), UserAccountID: user.ID, GasBasicID: gas.ID, DeliveryBasicID: delivery.ID, StaffAccountID: staff.ID, } if err := put(tx, &serviceRelation); err != nil { return err } productType := models.ProductType{ Entity: entity(10, common.StatusEnable), Code: "MOCK-LPG-15KG", Name: "15kg 液化气钢瓶", } if err := put(tx, &productType); err != nil { return err } warehouse := models.ProductWarehouse{ Entity: entity(11, common.StatusEnable), Code: "MOCK-WH-001", Name: "示例中心库房", Address: gas.Address, Manager: "赵库管", Phone: "13700000001", } if err := put(tx, &warehouse); err != nil { return err } enabledAt := yesterday productInfo := models.ProductInfo{ Entity: entity(12, common.StatusEnable), Code: "MOCK-CYLINDER-001", Name: "示例液化气钢瓶", ProductStatus: common.StatusInStock, ProductTypeID: productType.ID, Params: `{"weight":"15kg","medium":"LPG"}`, WarehouseID: warehouse.ID, ProducedAt: now.AddDate(-1, 0, 0), EnabledAt: &enabledAt, } if err := put(tx, &productInfo); err != nil { return err } productOwner := models.ProductOwner{ Entity: entity(13, common.StatusEnable), ProductInfoID: productInfo.ID, WarehouseID: warehouse.ID, Action: "stock_in", OccurredAt: yesterday, Reason: "模拟数据初始化", OperatorName: "系统", } if err := put(tx, &productOwner); err != nil { return err } completedAt := yesterday.Add(2 * time.Hour) productRepair := models.ProductRepair{ Entity: entity(14, common.StatusEnable), ProductInfoID: productInfo.ID, RepairNo: "MOCK-REPAIR-001", RepairType: "inspection", StartedAt: yesterday, CompletedAt: &completedAt, Result: "passed", TargetProductStatus: common.StatusInStock, Content: "外观、阀门与气密性检查", Operator: staff.Name, } if err := put(tx, &productRepair); err != nil { return err } contract := models.GasorderContract{ Entity: entity(15, common.StatusEnable), ContractStatus: common.StatusActive, ContractNo: "MOCK-CONTRACT-001", UserAccountID: user.ID, GasBasicID: gas.ID, DeliveryBasicID: delivery.ID, Title: "居民瓶装气配送示例合同", Terms: "按需配送,安全使用。", DefaultDeliveryFee: 500, SignedAt: yesterday, EffectiveAt: yesterday, ExpiredAt: &nextYear, } if err := put(tx, &contract); err != nil { return err } contractRevision := models.GasorderContractRevision{ Entity: entity(16, common.StatusEnable), GasorderContractID: contract.ID, Action: "activate", ContractStatus: common.StatusActive, EffectiveAt: contract.EffectiveAt, ExpiredAt: contract.ExpiredAt, OperatorIdentity: gasAccount.Identity, OperatorName: gasAccount.DisplayName, OccurredAt: yesterday, Reason: "模拟合同启用", } if err := put(tx, &contractRevision); err != nil { return err } contractProduct := models.GasorderContractProduct{ Entity: entity(17, common.StatusEnable), GasorderContractID: contract.ID, ProductInfoID: productInfo.ID, ProductCode: productInfo.Code, ProductTypeName: productType.Name, ProductParams: productInfo.Params, UnitPrice: 9800, BoundAt: yesterday, } if err := put(tx, &contractProduct); err != nil { return err } gasOrder := models.GasorderBasic{ Entity: entity(18, common.StatusEnable), OrderStatus: common.StatusCompleted, OrderNo: "MOCK-GASORDER-001", RequestNo: "MOCK-REQ-GASORDER-001", 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: 9800, DeliveryFee: 500, PayableAmount: 10300, OperatorIdentity: user.Identity, OperatorName: user.Name, Remark: "示例配送订单", } if err := put(tx, &gasOrder); err != nil { return err } gasOrderItem := models.GasorderItem{ Entity: entity(19, common.StatusEnable), GasorderBasicID: gasOrder.ID, GasorderContractProductID: contractProduct.ID, ProductInfoID: productInfo.ID, ProductCode: productInfo.Code, ProductTypeName: productType.Name, ProductParams: productInfo.Params, UnitPrice: contractProduct.UnitPrice, } if err := put(tx, &gasOrderItem); err != nil { return err } assignment := models.GasorderAssign{ Entity: entity(20, common.StatusEnable), GasorderBasicID: gasOrder.ID, GasBasicID: gas.ID, DeliveryBasicID: delivery.ID, StaffAccountID: staff.ID, AssignerIdentity: deliveryAccount.Identity, AssignerName: deliveryAccount.DisplayName, AssignedAt: now.Add(-2 * time.Hour), Reason: "系统示例派单", } if err := put(tx, &assignment); err != nil { return err } orderStatus := models.GasorderStatus{ Entity: entity(21, common.StatusEnable), GasorderBasicID: gasOrder.ID, FromStatus: common.StatusDelivering, ToStatus: common.StatusCompleted, OperatorIdentity: staff.Identity, OperatorName: staff.Name, OccurredAt: now, Reason: "用户已签收", } if err := put(tx, &orderStatus); err != nil { return err } trackCompletedAt := now.Add(-10 * time.Minute) track := models.GasorderTrack{ Entity: entity(22, common.StatusEnable), GasorderBasicID: gasOrder.ID, StaffAccountID: staff.ID, AttemptNo: 1, StartedAt: now.Add(-90 * time.Minute), CompletedAt: &trackCompletedAt, } if err := put(tx, &track); err != nil { return err } trackPoint := models.GasorderTrackPoint{ Entity: entity(23, common.StatusEnable), GasorderTrackID: track.ID, Longitude: address.Longitude, Latitude: address.Latitude, OccurredAt: trackCompletedAt, Source: "gps", Accuracy: "10m", } if err := put(tx, &trackPoint); err != nil { return err } confirmation := models.GasorderConfirm{ Entity: entity(24, common.StatusEnable), GasorderBasicID: gasOrder.ID, ConfirmType: "signature", RecipientName: user.Name, RecipientPhone: user.Phone, ProofURI: "/mock/proofs/gasorder-001.png", ConfirmedAt: now, Remark: "模拟签收", } if err := put(tx, &confirmation); err != nil { return err } category := models.EcCategory{ Entity: entity(25, common.StatusEnable), Name: "瓶装燃气", SortNo: 10, } if err := put(tx, &category); err != nil { return err } ecProduct := models.EcProduct{ Entity: entity(26, common.StatusEnable), EcCategoryID: category.ID, ProductCode: "MOCK-EC-LPG-001", Name: "15kg 液化气配送服务", PriceAmount: 10300, StockQuantity: 50, } if err := put(tx, &ecProduct); err != nil { return err } attribute := models.EcProductAttribute{ Entity: entity(27, common.StatusEnable), EcProductID: ecProduct.ID, Name: "规格", Value: "15kg/瓶", SortNo: 1, } if err := put(tx, &attribute); err != nil { return err } image := models.EcProductImage{ Entity: entity(28, common.StatusEnable), EcProductID: ecProduct.ID, ImageURI: "/mock/products/lpg-15kg.png", SortNo: 1, IsCover: true, } if err := put(tx, &image); err != nil { return err } cart := models.EcCart{ Entity: entity(29, common.StatusEnable), UserAccountID: user.ID, EcProductID: ecProduct.ID, Quantity: 1, Selected: true, } if err := put(tx, &cart); err != nil { return err } ecOrder := models.EcOrder{ Entity: entity(30, common.StatusEnable), OrderStatus: common.StatusPaid, OrderNo: "MOCK-ECORDER-001", UserAccountID: user.ID, GasStationID: gas.ID, DeliveryPointID: delivery.ID, TotalAmount: ecProduct.PriceAmount, } if err := put(tx, &ecOrder); err != nil { return err } ecOrderItem := models.EcOrderItem{ Entity: entity(31, common.StatusEnable), EcOrderID: ecOrder.ID, EcProductID: ecProduct.ID, ProductSnapshot: `{"code":"MOCK-EC-LPG-001","name":"15kg 液化气配送服务"}`, Quantity: 1, SaleAmount: ecProduct.PriceAmount, } if err := put(tx, &ecOrderItem); err != nil { return err } review := models.EcReview{ Entity: entity(32, common.StatusEnable), EcOrderID: ecOrder.ID, EcProductID: ecProduct.ID, UserAccountID: user.ID, Score: 5, Content: "配送及时,服务规范。", } if err := put(tx, &review); err != nil { return err } wallet := models.WalletBasic{ Entity: entity(33, common.StatusEnable), OwnerType: "user", OwnerID: user.ID, OwnerIdentity: user.Identity, AlipayID: "mock@example.com", AlipayName: user.Name, WxpayID: "mock_customer", WxpayName: user.Name, PayPasswordHash: string(passwordHash), Balance: 50000, WithdrawalBalance: 30000, } if err := put(tx, &wallet); err != nil { return err } bank := models.WalletBank{ Entity: entity(34, common.StatusEnable), WalletBasicID: wallet.ID, CardNoCiphertext: "mock-ciphertext-card", CardFingerprint: "mock-card-fingerprint-001", CardNoLast4: "8888", BankName: "示例银行", CardOwner: user.RealName, IDCardCiphertext: "mock-ciphertext-id", PhoneCiphertext: "mock-ciphertext-phone", BindID: "MOCK-BIND-001", BankType: "debit", Bank: "mock_bank", } if err := put(tx, &bank); err != nil { return err } walletPayment := models.WalletPayment{ Entity: entity(35, common.StatusEnable), PaymentStatus: common.StatusSuccess, WalletBasicID: wallet.ID, PaymentNo: "MOCK-PAYMENT-001", OrderNo: gasOrder.OrderNo, TradeNo: "MOCK-TRADE-001", PaymentType: "gasorder", PayChannel: "balance", PayType: "wallet", Amount: gasOrder.PayableAmount, Args: `{}`, CallbackMsg: `{"status":"success"}`, } if err := put(tx, &walletPayment); err != nil { return err } walletRecord := models.WalletRecord{ Entity: entity(36, common.StatusEnable), WalletBasicID: wallet.ID, RecordNo: "MOCK-RECORD-001", RequestNo: "MOCK-REQ-RECORD-001", Direction: "in", TradeType: "recharge", Amount: 50000, BalanceAfter: 50000, WithdrawalBalanceAfter: 30000, InTradeNo: walletPayment.PaymentNo, PayChannel: "manual", OperatorIdentity: gasAccount.Identity, OperatorName: gasAccount.DisplayName, Ymd: 20260701, Ym: 202607, Remark: "模拟钱包充值", } if err := put(tx, &walletRecord); err != nil { return err } refundCompletedAt := now refund := models.WalletRefund{ Entity: entity(37, common.StatusEnable), RefundStatus: common.StatusCompleted, WalletBasicID: wallet.ID, WalletPaymentID: walletPayment.ID, RefundNo: "MOCK-REFUND-001", OrderIdentity: gasOrder.Identity, Amount: 1000, Reason: "模拟部分退款", OrderInfo: `{"order_no":"MOCK-GASORDER-001"}`, Result: `{"status":"success"}`, TradeNo: "MOCK-REFUND-TRADE-001", CompletedAt: &refundCompletedAt, } if err := put(tx, &refund); err != nil { return err } applyCash := models.WalletApplyCash{ Entity: entity(38, common.StatusEnable), ApplyStatus: common.StatusApproved, WalletBasicID: wallet.ID, WalletBankID: bank.ID, CashNo: "MOCK-CASH-001", RequestNo: "MOCK-REQ-CASH-001", Amount: 5000, Channel: "bank", TradeNo: "MOCK-CASH-TRADE-001", Remark: "模拟提现", ReviewerIdentity: gasAccount.Identity, ReviewerName: gasAccount.DisplayName, ReviewedAt: &now, ReviewReason: "模拟审核通过", CompletedAt: &now, } if err := put(tx, &applyCash); err != nil { return err } gasOrderPayment := models.GasorderPayment{ Entity: entity(39, common.StatusEnable), GasorderBasicID: gasOrder.ID, WalletPaymentID: walletPayment.ID, AttemptNo: 1, Amount: gasOrder.PayableAmount, } if err := put(tx, &gasOrderPayment); err != nil { return err } finPayment := models.FinPayment{ Entity: entity(40, common.StatusEnable), PaymentStatus: common.StatusPaid, EcOrderID: ecOrder.ID, Channel: "wallet", Amount: ecOrder.TotalAmount, PaidAt: &now, } if err := put(tx, &finPayment); err != nil { return err } settlement := models.FinSettlement{ Entity: entity(41, common.StatusEnable), SettlementNo: "MOCK-SETTLEMENT-001", SubjectType: "gas", SubjectID: gas.ID, PeriodStart: now.AddDate(0, 0, -30), PeriodEnd: now, } if err := put(tx, &settlement); err != nil { return err } reconciliation := models.FinReconciliation{ Entity: entity(42, common.StatusEnable), ReconciliationStatus: common.StatusMatched, Channel: "wallet", BillDate: now, DifferenceAmount: 0, } if err := put(tx, &reconciliation); err != nil { return err } content := models.CmsContent{ Entity: entity(43, common.StatusEnable), ContentType: "notice", Title: "模拟数据使用说明", Body: "本内容由 platform-cli mock-data 生成。", VersionNo: 1, PublishStatus: "published", } if err := put(tx, &content); err != nil { return err } ticket := models.CsTicket{ Entity: entity(44, common.StatusEnable), TicketStatus: common.StatusOpen, TicketNo: "MOCK-TICKET-001", UserAccountID: user.ID, Category: "delivery", Priority: "normal", } if err := put(tx, &ticket); err != nil { return err } return ensureMockRoot(tx, string(passwordHash)) }) } // ensureMockRoot 复用已有 root 账号,仅在账号不存在时写入默认 root 用户。 func ensureMockRoot(database *gorm.DB, passwordHash string) error { role := models.PlatformRole{ Entity: entity(45, common.StatusEnable), RoleCode: "root", Name: "系统管理员", LocationScope: "precise", IsSystem: true, } if err := database.Where("role_code = ?", role.RoleCode).FirstOrCreate(&role).Error; err != nil { return fmt.Errorf("seed root platform role: %w", err) } var account models.PlatformAccount err := database.Where("username = ?", "root").First(&account).Error if err == nil { return nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("find root platform account: %w", err) } account = models.PlatformAccount{ Entity: entity(46, common.StatusEnable), Username: "root", DisplayName: "平台根管理员", PasswordHash: passwordHash, PlatformRoleCode: role.RoleCode, } if err := database.Create(&account).Error; err != nil { return fmt.Errorf("seed root platform account: %w", err) } return nil } func entity(sequence int, status int) models.Entity { return models.Entity{ Identity: fmt.Sprintf("%s%012d", mockIdentityPrefix, sequence), Status: status, } } func put[T any](database *gorm.DB, value *T) error { identity := identityOf(value) if identity == "" { return fmt.Errorf("seed %T: missing identity", value) } if err := database.Where("identity = ?", identity).FirstOrCreate(value).Error; err != nil { return fmt.Errorf("seed %T: %w", value, err) } return nil } func identityOf(value any) string { record := reflect.Indirect(reflect.ValueOf(value)) if !record.IsValid() { return "" } entityField := record.FieldByName("Entity") if !entityField.IsValid() { return "" } identityField := entityField.FieldByName("Identity") if !identityField.IsValid() || identityField.Kind() != reflect.String { return "" } return identityField.String() }