package user import ( "errors" "time" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/payment" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" "gorm.io/gorm/clause" ) // ServiceRelation 返回当前唯一有效服务归属的公开 identity。 func ServiceRelation(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } var relation models.UserServiceRelation if impl.DBService.Where("user_account_id = ? AND status = ?", account.ID, common.StatusEnable).First(&relation).Error != nil { infra.Response.Success(ctx, nil) return } response := gin.H{} if relation.GasBasicID != 0 { var gas models.GasBasic if impl.DBService.First(&gas, relation.GasBasicID).Error == nil { response["gas_identity"], response["gas_name"] = gas.Identity, gas.Name } } if relation.DeliveryBasicID != 0 { var delivery models.DeliveryBasic if impl.DBService.First(&delivery, relation.DeliveryBasicID).Error == nil { response["delivery_identity"], response["delivery_name"] = delivery.Identity, delivery.Name } } infra.Response.Success(ctx, response) } // PayGasOrder 为本人未履约供气订单创建余额或第三方支付单。 func PayGasOrder(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } var request struct { PaymentPassword string `json:"payment_password"` RequestNo string `json:"request_no" binding:"required"` Channel string `json:"channel" binding:"required,oneof=wallet alipay wechat"` PayType string `json:"pay_type" binding:"required"` OpenID string `json:"openid"` } if ctx.ShouldBindJSON(&request) != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } if request.Channel == "wallet" { if err := payGasOrderWithWallet(account, ctx.Param("identity"), request.RequestNo, request.PaymentPassword); err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"paid": true}) return } var order models.GasorderBasic if err := impl.DBService.Where("identity = ? AND user_account_id = ? AND order_status IN ?", ctx.Param("identity"), account.ID, []int{common.StatusCreated, common.StatusAssigned}).First(&order).Error; err != nil { infra.Response.Error(ctx, err) return } payOrder, err := payment.Create(ctx, payment.CreateInput{RequestNo: request.RequestNo, BusinessType: "gasorder", BusinessIdentity: order.Identity, UserIdentity: account.Identity, Channel: request.Channel, PayType: request.PayType, Subject: "和气供气订单 " + order.OrderNo, OpenID: request.OpenID, Amount: order.PayableAmount}) if err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, payment.PublicResponse(payOrder)) } // payGasOrderWithWallet 在同一事务内记录支付事实、扣减钱包并推进供气订单。 func payGasOrderWithWallet(account models.UserAccount, identity, requestNo, password string) error { return impl.DBService.Transaction(func(tx *gorm.DB) error { var existing models.PaymentOrder err := tx.Where("business_type = ? AND request_no = ?", "gasorder", requestNo).First(&existing).Error if err == nil { if existing.BusinessIdentity != identity || existing.UserIdentity != account.Identity || existing.Channel != "wallet" || existing.PaymentStatus != payment.StatusPaid { return errors.New("idempotency conflict") } return nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return err } var order models.GasorderBasic if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("identity = ? AND user_account_id = ? AND order_status IN ?", identity, account.ID, []int{common.StatusCreated, common.StatusAssigned}).First(&order).Error; err != nil { return err } var wallet models.WalletBasic if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("owner_type = ? AND owner_identity = ?", "user", account.Identity).First(&wallet).Error; err != nil { return err } if !common.VerifyPaymentPassword(account.Identity, wallet, password) { return gorm.ErrInvalidData } if err := common.SpendWalletBalance(&wallet, order.PayableAmount); err != nil { return err } if err := common.SaveWalletBalances(tx, wallet); err != nil { return err } now := time.Now() if err := tx.Model(&order).Update("order_status", common.StatusPaid).Error; err != nil { return err } if err := payment.CompleteGasDeposits(tx, order.Identity, now); err != nil { return err } if err := tx.Create(&models.PaymentOrder{ Entity: common.NewEntity(common.StatusEnable), PaymentStatus: payment.StatusPaid, PaymentNo: common.RecordNo("PAY"), RequestNo: requestNo, BusinessType: "gasorder", BusinessIdentity: order.Identity, UserIdentity: account.Identity, MerchantIdentity: "platform", Channel: "wallet", PayType: "balance", Amount: order.PayableAmount, Subject: "和气供气订单 " + order.OrderNo, ExpiresAt: now, PaidAt: &now, }).Error; err != nil { return err } date := now.In(time.Local) return tx.Create(&models.WalletRecord{ Entity: common.NewEntity(common.StatusEnable), WalletBasicID: wallet.ID, RecordNo: common.RecordNo("WR"), RequestNo: requestNo, Direction: "expense", TradeType: "gasorder", Amount: order.PayableAmount, BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance, OutTradeNo: order.OrderNo, PayChannel: "wallet", OperatorIdentity: account.Identity, Ymd: int32(date.Year()*10000 + int(date.Month())*100 + date.Day()), Ym: int32(date.Year()*100 + int(date.Month())), }).Error }) } // ListGasContracts 返回用户自己的供气合同。 func ListGasContracts(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } var list []models.GasorderContract if err := impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, common.StatusArchived).Order("created_at desc").Find(&list).Error; err != nil { infra.Response.Error(ctx, err) return } response := make([]gin.H, 0, len(list)) contractIDs, stationIDs := []uint64{}, []uint64{} for _, contract := range list { contractIDs = append(contractIDs, contract.ID) stationIDs = append(stationIDs, contract.GasBasicID) } // 合同绑定属于合同,不得把合同主键误作订单主键读取气瓶。 var bindings []models.GasorderContractProduct var stations []models.GasBasic if len(list) > 0 { if err := impl.DBService.Where("gasorder_contract_id IN ? AND status <> ?", contractIDs, common.StatusArchived).Find(&bindings).Error; err != nil { infra.Response.Error(ctx, err) return } if err := impl.DBService.Where("id IN ? AND status = ?", stationIDs, common.StatusEnable).Find(&stations).Error; err != nil { infra.Response.Error(ctx, err) return } } stationNames := map[uint64]string{} for _, station := range stations { stationNames[station.ID] = station.Name } for _, order := range list { items := []gin.H{} for _, binding := range bindings { if binding.GasorderContractID == order.ID { items = append(items, gin.H{"identity": binding.Identity, "product_code": binding.ProductCode, "product_type_name": binding.ProductTypeName, "unit_price": binding.UnitPrice, "bound_at": binding.BoundAt, "unbound_at": binding.UnboundAt}) } } value := common.ResourceResponse(order).(map[string]any) // 旧字段保留空值以兼容客户端,私有附件不能以原始存储地址下发。 value["file_uri"] = "" value["has_attachment"] = order.FileURI != "" value["station_name"] = stationNames[order.GasBasicID] value["items"] = items response = append(response, value) } infra.Response.Success(ctx, response) } // ListGasOrders 返回用户自己的供气订单。 func ListGasOrders(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } query, page, ok := paginateClientList(ctx, impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, common.StatusArchived)) if !ok { return } var list []models.GasorderBasic if err := query.Order("created_at desc, identity").Find(&list).Error; err != nil { infra.Response.Error(ctx, err) return } stationIDs := make([]uint64, 0, len(list)) seenStations := map[uint64]struct{}{} for _, order := range list { if _, exists := seenStations[order.GasBasicID]; !exists { seenStations[order.GasBasicID] = struct{}{} stationIDs = append(stationIDs, order.GasBasicID) } } stationNames := map[uint64]string{} if len(stationIDs) > 0 { var stations []models.GasBasic if err := impl.DBService.Where("id IN ? AND status = ?", stationIDs, common.StatusEnable).Find(&stations).Error; err != nil { infra.Response.Error(ctx, err) return } for _, station := range stations { stationNames[station.ID] = station.Name } } response := make([]gin.H, 0, len(list)) for _, order := range list { var items []models.GasorderItem if err := impl.DBService.Where("gasorder_basic_id = ?", order.ID).Find(&items).Error; err != nil { infra.Response.Error(ctx, err) return } value := common.ResourceResponse(order).(map[string]any) value["items"] = common.ResourceResponse(items) value["station_name"] = stationNames[order.GasBasicID] value["status_code"], value["status_name"] = order.OrderStatus, gasOrderStatusName(order.OrderStatus) value["allowed_actions"] = gasOrderActions(order.OrderStatus) response = append(response, value) } respondClientPage(ctx, page, response) } // CancelGasOrder 仅允许取消已创建或已分派的本人订单。 func CancelGasOrder(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } err := impl.DBService.Transaction(func(tx *gorm.DB) error { var order models.GasorderBasic if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND user_account_id = ? AND order_status IN ?", ctx.Param("identity"), account.ID, []int{common.StatusCreated, common.StatusAssigned}).First(&order).Error; err != nil { return err } if err := tx.Model(&order).Updates(map[string]any{"order_status": common.StatusCancelled, "operator_identity": account.Identity}).Error; err != nil { return err } if err := tx.Model(&models.GasorderItem{}).Where("gasorder_basic_id = ?", order.ID).Update("active", false).Error; err != nil { return err } return tx.Model(&models.GasorderDeposit{}).Where("gasorder_basic_id = ? AND deposit_status = ?", order.ID, common.StatusPending).Update("deposit_status", common.StatusCancelled).Error }) if err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } infra.Response.Success(ctx, gin.H{"cancelled": true}) }