已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
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"
|
||||
@@ -8,6 +11,8 @@ import (
|
||||
"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。
|
||||
@@ -37,22 +42,31 @@ func ServiceRelation(ctx *gin.Context) {
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
// PayGasOrder 为本人未履约供气订单创建统一第三方支付单。
|
||||
// PayGasOrder 为本人未履约供气订单创建余额或第三方支付单。
|
||||
func PayGasOrder(ctx *gin.Context) {
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
RequestNo string `json:"request_no" binding:"required"`
|
||||
Channel string `json:"channel" binding:"required,oneof=alipay wechat"`
|
||||
PayType string `json:"pay_type" binding:"required"`
|
||||
OpenID string `json:"openid"`
|
||||
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)
|
||||
@@ -67,6 +81,68 @@ func PayGasOrder(ctx *gin.Context) {
|
||||
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)
|
||||
@@ -79,14 +155,41 @@ func ListGasContracts(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
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["items"] = common.ResourceResponse(items)
|
||||
// 旧字段保留空值以兼容客户端,私有附件不能以原始存储地址下发。
|
||||
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)
|
||||
@@ -98,12 +201,49 @@ func ListGasOrders(ctx *gin.Context) {
|
||||
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 := impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, common.StatusArchived).Order("created_at desc").Find(&list).Error; err != nil {
|
||||
if err := query.Order("created_at desc, identity").Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
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 仅允许取消已创建或已分派的本人订单。
|
||||
@@ -112,14 +252,20 @@ func CancelGasOrder(ctx *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
result := impl.DBService.Model(&models.GasorderBasic{}).
|
||||
Where("identity = ? AND user_account_id = ? AND order_status IN ?", ctx.Param("identity"), account.ID, []int{common.StatusCreated, common.StatusAssigned}).
|
||||
Updates(map[string]any{"order_status": common.StatusCancelled, "operator_identity": account.Identity})
|
||||
if result.Error != nil {
|
||||
infra.Response.Error(ctx, result.Error)
|
||||
return
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user