fix(wallet): enforce consistent withdrawal accounting
This commit is contained in:
@@ -8,8 +8,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
clientcommon "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/common"
|
||||
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -26,25 +25,25 @@ func Login(ctx *gin.Context) {
|
||||
Code string `json:"code"`
|
||||
RequestIdentity string `json:"request_identity"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil || !clientcommon.ValidPhone(request.Phone) {
|
||||
if ctx.ShouldBindJSON(&request) != nil || !common.ValidPhone(request.Phone) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var account models.StaffAccount
|
||||
if impl.DBService.Where("phone = ? AND status = ?", strings.TrimSpace(request.Phone), base.StatusEnable).First(&account).Error != nil ||
|
||||
if impl.DBService.Where("phone = ? AND status = ?", strings.TrimSpace(request.Phone), common.StatusEnable).First(&account).Error != nil ||
|
||||
!supportedRoles[account.RoleCode] {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
valid := request.Mode == "password" && bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.Password)) == nil
|
||||
if request.Mode == "verification_code" {
|
||||
valid = clientcommon.VerifyCode("service_app", account.Phone, "login", request.RequestIdentity, request.Code)
|
||||
valid = common.VerifyCode("service_app", account.Phone, "login", request.RequestIdentity, request.Code)
|
||||
}
|
||||
if !valid {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
accessToken, err := clientcommon.IssueToken(account.Identity, "service_app", account.RoleCode, map[string]string{"phone": account.Phone})
|
||||
accessToken, err := common.IssueToken(account.Identity, "service_app", account.RoleCode, map[string]string{"phone": account.Phone})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -54,7 +53,7 @@ func Login(ctx *gin.Context) {
|
||||
|
||||
// Profile 返回工作人员岗位和归属。
|
||||
func Profile(ctx *gin.Context) {
|
||||
account, ok := clientcommon.StaffAccount(ctx)
|
||||
account, ok := common.StaffAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -66,14 +65,14 @@ func Profile(ctx *gin.Context) {
|
||||
|
||||
// Preflight 返回当前单角色账号可由服务端确认的作业前置条件。
|
||||
func Preflight(ctx *gin.Context) {
|
||||
account, ok := clientcommon.StaffAccount(ctx)
|
||||
account, ok := common.StaffAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var credential models.StaffCredential
|
||||
credentialFound := impl.DBService.
|
||||
Where("staff_account_id = ? AND status = ?", account.ID, base.StatusEnable).
|
||||
Where("staff_account_id = ? AND status = ?", account.ID, common.StatusEnable).
|
||||
Order("expired_at desc").
|
||||
First(&credential).Error == nil
|
||||
credentialValid := credentialFound && (credential.ExpiredAt == nil || credential.ExpiredAt.After(time.Now()))
|
||||
@@ -117,7 +116,7 @@ func checkStatus(passed bool) string {
|
||||
|
||||
// ChangePassword 修改当前工作人员登录密码。
|
||||
func ChangePassword(ctx *gin.Context) {
|
||||
account, ok := clientcommon.StaffAccount(ctx)
|
||||
account, ok := common.StaffAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -125,12 +124,12 @@ func ChangePassword(ctx *gin.Context) {
|
||||
CurrentPassword string `json:"current_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil || !base.IsValidAccountPassword(request.NewPassword) ||
|
||||
if ctx.ShouldBindJSON(&request) != nil || !common.IsValidAccountPassword(request.NewPassword) ||
|
||||
bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.CurrentPassword)) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
hash, err := base.PasswordHash(request.NewPassword)
|
||||
hash, err := common.PasswordHash(request.NewPassword)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -150,18 +149,18 @@ func ResetPassword(ctx *gin.Context) {
|
||||
Code string `json:"code" binding:"required"`
|
||||
RequestIdentity string `json:"request_identity" binding:"required"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil || !base.IsValidAccountPassword(request.NewPassword) ||
|
||||
!clientcommon.VerifyCode("service_app", request.Phone, "reset_login_password", request.RequestIdentity, request.Code) {
|
||||
if ctx.ShouldBindJSON(&request) != nil || !common.IsValidAccountPassword(request.NewPassword) ||
|
||||
!common.VerifyCode("service_app", request.Phone, "reset_login_password", request.RequestIdentity, request.Code) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := base.PasswordHash(request.NewPassword)
|
||||
hash, err := common.PasswordHash(request.NewPassword)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
result := impl.DBService.Model(&models.StaffAccount{}).
|
||||
Where("phone = ? AND status = ?", strings.TrimSpace(request.Phone), base.StatusEnable).
|
||||
Where("phone = ? AND status = ?", strings.TrimSpace(request.Phone), common.StatusEnable).
|
||||
Update("password_hash", hash)
|
||||
if result.Error != nil {
|
||||
infra.Response.Error(ctx, result.Error)
|
||||
|
||||
@@ -10,8 +10,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
clientcommon "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/common"
|
||||
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
common "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"
|
||||
@@ -25,7 +24,7 @@ func ListDeliveryOrders(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
var orders []models.GasorderBasic
|
||||
if err := impl.DBService.Where("staff_account_id = ? AND status <> ?", account.ID, base.StatusArchived).
|
||||
if err := impl.DBService.Where("staff_account_id = ? AND status <> ?", account.ID, common.StatusArchived).
|
||||
Order("created_at desc").Find(&orders).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -48,12 +47,12 @@ func GetDeliveryOrder(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"order": deliveryOrderResponse(order), "items": base.ResourceResponse(items)})
|
||||
infra.Response.Success(ctx, gin.H{"order": deliveryOrderResponse(order), "items": common.ResourceResponse(items)})
|
||||
}
|
||||
|
||||
// StartDeliveryOrder 将已就绪订单置为配送中并创建本次轨迹。
|
||||
func StartDeliveryOrder(ctx *gin.Context) {
|
||||
transitionDeliveryOrder(ctx, base.StatusReady, base.StatusDelivering, true)
|
||||
transitionDeliveryOrder(ctx, common.StatusReady, common.StatusDelivering, true)
|
||||
}
|
||||
|
||||
// AppendDeliveryTracks 批量补传配送中轨迹点;request_no 保证重复补传不重复落库。
|
||||
@@ -70,7 +69,7 @@ func AppendDeliveryTracks(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
order, ok := requireDeliveryOrder(ctx, account, true)
|
||||
if !ok || order.OrderStatus != base.StatusDelivering {
|
||||
if !ok || order.OrderStatus != common.StatusDelivering {
|
||||
if ok {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
}
|
||||
@@ -90,7 +89,7 @@ func AppendDeliveryTracks(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
points = append(points, models.GasorderTrackPoint{
|
||||
Entity: base.NewEntity(base.StatusEnable), GasorderTrackID: track.ID, RequestNo: item.RequestNo,
|
||||
Entity: common.NewEntity(common.StatusEnable), GasorderTrackID: track.ID, RequestNo: item.RequestNo,
|
||||
Longitude: item.Longitude, Latitude: item.Latitude, OccurredAt: item.OccurredAt,
|
||||
ReceivedAt: receivedAt, Source: item.Source, Accuracy: item.Accuracy,
|
||||
Speed: item.Speed, Direction: item.Direction,
|
||||
@@ -119,7 +118,7 @@ func ArriveDeliveryOrder(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
distance, valid := coordinateDistanceMeters(order.Longitude, order.Latitude, request.Longitude, request.Latitude)
|
||||
if order.OrderStatus != base.StatusDelivering || !valid || distance > config.Spec.Global.DeliveryArrivalRadiusMeters {
|
||||
if order.OrderStatus != common.StatusDelivering || !valid || distance > config.Spec.Global.DeliveryArrivalRadiusMeters {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
@@ -132,7 +131,7 @@ func ArriveDeliveryOrder(ctx *gin.Context) {
|
||||
}
|
||||
now := time.Now()
|
||||
point := models.GasorderTrackPoint{
|
||||
Entity: base.NewEntity(base.StatusEnable), GasorderTrackID: track.ID, RequestNo: request.RequestNo,
|
||||
Entity: common.NewEntity(common.StatusEnable), GasorderTrackID: track.ID, RequestNo: request.RequestNo,
|
||||
Longitude: request.Longitude, Latitude: request.Latitude, OccurredAt: request.OccurredAt,
|
||||
ReceivedAt: now, Source: request.Source, Accuracy: request.Accuracy, Speed: request.Speed, Direction: request.Direction,
|
||||
}
|
||||
@@ -143,18 +142,18 @@ func ArriveDeliveryOrder(ctx *gin.Context) {
|
||||
return err
|
||||
}
|
||||
result := tx.Model(&models.GasorderBasic{}).
|
||||
Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, base.StatusDelivering).
|
||||
Update("order_status", base.StatusAwaitingConfirmation)
|
||||
Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, common.StatusDelivering).
|
||||
Update("order_status", common.StatusAwaitingConfirmation)
|
||||
if result.Error != nil || result.RowsAffected != 1 {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
return tx.Create(deliveryStatusRecord(order, account, base.StatusDelivering, base.StatusAwaitingConfirmation, "配送到达")).Error
|
||||
return tx.Create(deliveryStatusRecord(order, account, common.StatusDelivering, common.StatusAwaitingConfirmation, "配送到达")).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"order_status": base.StatusAwaitingConfirmation, "distance_meters": math.Round(distance)})
|
||||
infra.Response.Success(ctx, gin.H{"order_status": common.StatusAwaitingConfirmation, "distance_meters": math.Round(distance)})
|
||||
}
|
||||
|
||||
// ExceptionDeliveryOrder 将配送中或待签收订单置为异常。
|
||||
@@ -171,13 +170,13 @@ func ExceptionDeliveryOrder(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
order, ok := requireDeliveryOrder(ctx, account, true)
|
||||
if !ok || (order.OrderStatus != base.StatusDelivering && order.OrderStatus != base.StatusAwaitingConfirmation) {
|
||||
if !ok || (order.OrderStatus != common.StatusDelivering && order.OrderStatus != common.StatusAwaitingConfirmation) {
|
||||
if ok {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
}
|
||||
return
|
||||
}
|
||||
updateDeliveryStatus(ctx, order, account, base.StatusException, request.Reason, gin.H{"previous_order_status": order.OrderStatus})
|
||||
updateDeliveryStatus(ctx, order, account, common.StatusException, request.Reason, gin.H{"previous_order_status": order.OrderStatus})
|
||||
}
|
||||
|
||||
// RecoverDeliveryOrder 将本人异常订单恢复到异常前状态。
|
||||
@@ -194,15 +193,15 @@ func RecoverDeliveryOrder(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
order, ok := requireDeliveryOrder(ctx, account, true)
|
||||
if !ok || order.OrderStatus != base.StatusException ||
|
||||
(order.PreviousOrderStatus != base.StatusDelivering && order.PreviousOrderStatus != base.StatusAwaitingConfirmation) {
|
||||
if !ok || order.OrderStatus != common.StatusException ||
|
||||
(order.PreviousOrderStatus != common.StatusDelivering && order.PreviousOrderStatus != common.StatusAwaitingConfirmation) {
|
||||
if ok {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
}
|
||||
return
|
||||
}
|
||||
target := order.PreviousOrderStatus
|
||||
updateDeliveryStatus(ctx, order, account, target, request.Reason, gin.H{"previous_order_status": base.StatusDraft})
|
||||
updateDeliveryStatus(ctx, order, account, target, request.Reason, gin.H{"previous_order_status": common.StatusDraft})
|
||||
}
|
||||
|
||||
// SubmitDeliveryReceipt 保存签收凭证并完成订单,重复 request_no 返回既有结果。
|
||||
@@ -225,18 +224,18 @@ func SubmitDeliveryReceipt(ctx *gin.Context) {
|
||||
}
|
||||
var existing models.GasorderConfirm
|
||||
if impl.DBService.Where("request_no = ?", request.RequestNo).First(&existing).Error == nil {
|
||||
infra.Response.Success(ctx, gin.H{"confirmed": true, "identity": existing.Identity, "order_status": base.StatusCompleted})
|
||||
infra.Response.Success(ctx, gin.H{"confirmed": true, "identity": existing.Identity, "order_status": common.StatusCompleted})
|
||||
return
|
||||
}
|
||||
order, ok := requireDeliveryOrder(ctx, account, true)
|
||||
if !ok || order.OrderStatus != base.StatusAwaitingConfirmation {
|
||||
if !ok || order.OrderStatus != common.StatusAwaitingConfirmation {
|
||||
if ok {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
}
|
||||
return
|
||||
}
|
||||
confirm := models.GasorderConfirm{
|
||||
Entity: base.NewEntity(base.StatusEnable), GasorderBasicID: order.ID, RequestNo: request.RequestNo,
|
||||
Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID, RequestNo: request.RequestNo,
|
||||
ConfirmType: request.ConfirmType, RecipientName: request.RecipientName, RecipientPhone: request.RecipientPhone,
|
||||
ProofURI: request.ProofURI, ConfirmedAt: time.Now(), Remark: request.Remark,
|
||||
}
|
||||
@@ -245,21 +244,21 @@ func SubmitDeliveryReceipt(ctx *gin.Context) {
|
||||
return err
|
||||
}
|
||||
result := tx.Model(&models.GasorderBasic{}).
|
||||
Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, base.StatusAwaitingConfirmation).
|
||||
Update("order_status", base.StatusCompleted)
|
||||
Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, common.StatusAwaitingConfirmation).
|
||||
Update("order_status", common.StatusCompleted)
|
||||
if result.Error != nil || result.RowsAffected != 1 {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
if err := tx.Model(&models.GasorderItem{}).Where("gasorder_basic_id = ?", order.ID).Update("active", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(deliveryStatusRecord(order, account, base.StatusAwaitingConfirmation, base.StatusCompleted, "用户签收")).Error
|
||||
return tx.Create(deliveryStatusRecord(order, account, common.StatusAwaitingConfirmation, common.StatusCompleted, "用户签收")).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"confirmed": true, "identity": confirm.Identity, "order_status": base.StatusCompleted})
|
||||
infra.Response.Success(ctx, gin.H{"confirmed": true, "identity": confirm.Identity, "order_status": common.StatusCompleted})
|
||||
}
|
||||
|
||||
type deliveryTrackPointRequest struct {
|
||||
@@ -274,7 +273,7 @@ type deliveryTrackPointRequest struct {
|
||||
}
|
||||
|
||||
func requireDeliveryAccount(ctx *gin.Context) (models.StaffAccount, bool) {
|
||||
account, ok := clientcommon.StaffAccount(ctx)
|
||||
account, ok := common.StaffAccount(ctx)
|
||||
if !ok {
|
||||
return account, false
|
||||
}
|
||||
@@ -291,7 +290,7 @@ func requireDeliveryOrder(ctx *gin.Context, account models.StaffAccount, lock bo
|
||||
if lock {
|
||||
query = query.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
}
|
||||
if query.Where("identity = ? AND staff_account_id = ? AND status <> ?", ctx.Param("identity"), account.ID, base.StatusArchived).
|
||||
if query.Where("identity = ? AND staff_account_id = ? AND status <> ?", ctx.Param("identity"), account.ID, common.StatusArchived).
|
||||
First(&order).Error != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return order, false
|
||||
@@ -332,7 +331,7 @@ func transitionDeliveryOrder(ctx *gin.Context, from, to int, createTrack bool) {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&models.GasorderTrack{
|
||||
Entity: base.NewEntity(base.StatusEnable), GasorderBasicID: order.ID,
|
||||
Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID,
|
||||
StaffAccountID: account.ID, AttemptNo: attempt + 1, StartedAt: time.Now(),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
@@ -370,7 +369,7 @@ func updateDeliveryStatus(ctx *gin.Context, order models.GasorderBasic, account
|
||||
|
||||
func deliveryStatusRecord(order models.GasorderBasic, account models.StaffAccount, from, to int, reason string) models.GasorderStatus {
|
||||
return models.GasorderStatus{
|
||||
Entity: base.NewEntity(base.StatusEnable), GasorderBasicID: order.ID,
|
||||
Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID,
|
||||
FromStatus: from, ToStatus: to, OperatorIdentity: account.Identity,
|
||||
OperatorName: account.Name, OccurredAt: time.Now(), Reason: strings.TrimSpace(reason),
|
||||
}
|
||||
@@ -397,13 +396,13 @@ func deliveryOrderResponse(order models.GasorderBasic) gin.H {
|
||||
|
||||
func deliveryAllowedActions(status int) []string {
|
||||
switch status {
|
||||
case base.StatusReady:
|
||||
case common.StatusReady:
|
||||
return []string{"start"}
|
||||
case base.StatusDelivering:
|
||||
case common.StatusDelivering:
|
||||
return []string{"append_tracks", "arrive", "exception"}
|
||||
case base.StatusAwaitingConfirmation:
|
||||
case common.StatusAwaitingConfirmation:
|
||||
return []string{"submit_receipt", "exception"}
|
||||
case base.StatusException:
|
||||
case common.StatusException:
|
||||
return []string{"recover"}
|
||||
default:
|
||||
return []string{}
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
clientcommon "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/common"
|
||||
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
common "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"
|
||||
@@ -16,7 +15,7 @@ import (
|
||||
|
||||
// Attendance 上下班打卡;存在进行中任务时禁止下班。
|
||||
func Attendance(ctx *gin.Context) {
|
||||
account, ok := clientcommon.StaffAccount(ctx)
|
||||
account, ok := common.StaffAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -44,7 +43,7 @@ func Attendance(ctx *gin.Context) {
|
||||
}
|
||||
}
|
||||
record := models.StaffAttendance{
|
||||
Entity: base.NewEntity(base.StatusEnable), StaffAccountID: account.ID, RoleCode: account.RoleCode,
|
||||
Entity: common.NewEntity(common.StatusEnable), StaffAccountID: account.ID, RoleCode: account.RoleCode,
|
||||
Action: request.Action, OccurredAt: request.OccurredAt, Longitude: request.Longitude,
|
||||
Latitude: request.Latitude, DeviceIdentity: request.DeviceIdentity, RequestNo: request.RequestNo,
|
||||
}
|
||||
@@ -71,7 +70,7 @@ func Attendance(ctx *gin.Context) {
|
||||
|
||||
// ListTickets 仅返回分派给当前人员且与岗位匹配的工单。
|
||||
func ListTickets(ctx *gin.Context) {
|
||||
account, ok := clientcommon.StaffAccount(ctx)
|
||||
account, ok := common.StaffAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -84,27 +83,27 @@ func ListTickets(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
var list []models.CsTicket
|
||||
if err := impl.DBService.Where("staff_account_id = ? AND category IN ? AND status <> ?", account.ID, categories, base.StatusArchived).
|
||||
if err := impl.DBService.Where("staff_account_id = ? AND category IN ? AND status <> ?", account.ID, categories, common.StatusArchived).
|
||||
Order("created_at desc").Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
|
||||
// GetTicket 按公开 identity 返回当前工作人员被分派的单一工单。
|
||||
func GetTicket(ctx *gin.Context) {
|
||||
account, ok := clientcommon.StaffAccount(ctx)
|
||||
account, ok := common.StaffAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var ticket models.CsTicket
|
||||
if impl.DBService.Where("identity = ? AND staff_account_id = ? AND status <> ?", ctx.Param("identity"), account.ID, base.StatusArchived).
|
||||
if impl.DBService.Where("identity = ? AND staff_account_id = ? AND status <> ?", ctx.Param("identity"), account.ID, common.StatusArchived).
|
||||
First(&ticket).Error != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(ticket))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(ticket))
|
||||
}
|
||||
|
||||
// StartTicket 将本人已分派工单置为处理中。
|
||||
@@ -124,7 +123,7 @@ func RecoverTicket(ctx *gin.Context) {
|
||||
|
||||
// SubmitTicketResult 追加现场证据并提交用户确认;不合格或高风险结果必须进入异常。
|
||||
func SubmitTicketResult(ctx *gin.Context) {
|
||||
account, ok := clientcommon.StaffAccount(ctx)
|
||||
account, ok := common.StaffAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -185,7 +184,7 @@ func SubmitTicketResult(ctx *gin.Context) {
|
||||
now := time.Now()
|
||||
for _, item := range request.Evidences {
|
||||
record := models.CsTicketEvidence{
|
||||
Entity: base.NewEntity(base.StatusEnable), CsTicketID: ticket.ID, EvidenceType: item.EvidenceType,
|
||||
Entity: common.NewEntity(common.StatusEnable), CsTicketID: ticket.ID, EvidenceType: item.EvidenceType,
|
||||
MediaType: item.MediaType, FileURI: item.FileURI, CapturedAt: item.CapturedAt, ReceivedAt: now,
|
||||
Longitude: item.Longitude, Latitude: item.Latitude, Source: "app",
|
||||
IntegrityStatus: "unverified", OperatorIdentity: account.Identity, RequestNo: item.RequestNo,
|
||||
@@ -212,7 +211,7 @@ func SubmitTicketResult(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func updateTicketStatus(ctx *gin.Context, from, to int, extra map[string]any) {
|
||||
account, ok := clientcommon.StaffAccount(ctx)
|
||||
account, ok := common.StaffAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
clientcommon "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/common"
|
||||
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
common "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"
|
||||
@@ -16,21 +15,21 @@ import (
|
||||
|
||||
// ListAddresses 返回当前用户未归档地址。
|
||||
func ListAddresses(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var list []models.UserAddress
|
||||
if err := impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, base.StatusArchived).Order("is_default desc, created_at desc").Find(&list).Error; err != nil {
|
||||
if err := impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, common.StatusArchived).Order("is_default desc, created_at desc").Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
|
||||
// SaveAddress 新增地址,设为默认时原默认地址会在同一事务取消默认。
|
||||
func SaveAddress(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -45,7 +44,7 @@ func SaveAddress(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
address := models.UserAddress{
|
||||
Entity: base.NewEntity(base.StatusEnable), UserAccountID: account.ID, Address: request.Address,
|
||||
Entity: common.NewEntity(common.StatusEnable), UserAccountID: account.ID, Address: request.Address,
|
||||
Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault,
|
||||
}
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
@@ -69,7 +68,7 @@ var userTicketCategories = map[string]bool{
|
||||
|
||||
// CreateTicket 创建工单,服务人员只能由后台分派。
|
||||
func CreateTicket(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -85,18 +84,18 @@ func CreateTicket(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
var relation models.UserServiceRelation
|
||||
_ = impl.DBService.Where("user_account_id = ? AND status = ?", account.ID, base.StatusEnable).First(&relation).Error
|
||||
_ = impl.DBService.Where("user_account_id = ? AND status = ?", account.ID, common.StatusEnable).First(&relation).Error
|
||||
addressText := ""
|
||||
if request.AddressIdentity != "" {
|
||||
var address models.UserAddress
|
||||
if impl.DBService.Where("identity = ? AND user_account_id = ? AND status <> ?", request.AddressIdentity, account.ID, base.StatusArchived).First(&address).Error != nil {
|
||||
if impl.DBService.Where("identity = ? AND user_account_id = ? AND status <> ?", request.AddressIdentity, account.ID, common.StatusArchived).First(&address).Error != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
addressText = address.Address
|
||||
}
|
||||
ticket := models.CsTicket{
|
||||
Entity: base.NewEntity(base.StatusEnable), TicketStatus: 32, TicketNo: clientcommon.RecordNo("TK"),
|
||||
Entity: common.NewEntity(common.StatusEnable), TicketStatus: 32, TicketNo: common.RecordNo("TK"),
|
||||
RequestNo: request.RequestNo,
|
||||
UserAccountID: account.ID, GasBasicID: relation.GasBasicID, DeliveryBasicID: relation.DeliveryBasicID,
|
||||
Category: request.Category, Priority: "normal", Description: strings.TrimSpace(request.Description),
|
||||
@@ -111,21 +110,21 @@ func CreateTicket(ctx *gin.Context) {
|
||||
|
||||
// ListTickets 仅返回当前用户自己的工单。
|
||||
func ListTickets(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var list []models.CsTicket
|
||||
if err := impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, base.StatusArchived).Order("created_at desc").Find(&list).Error; err != nil {
|
||||
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
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
|
||||
// ConfirmTicket 用户确认工作人员提交的处理结果。
|
||||
func ConfirmTicket(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -146,7 +145,7 @@ func ConfirmTicket(ctx *gin.Context) {
|
||||
|
||||
// CancelTicket 取消尚未完成的本人工单。
|
||||
func CancelTicket(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
clientcommon "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/common"
|
||||
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -26,24 +25,24 @@ type loginRequest struct {
|
||||
// Login 支持密码和一次性验证码两种登录模式。
|
||||
func Login(ctx *gin.Context) {
|
||||
var request loginRequest
|
||||
if ctx.ShouldBindJSON(&request) != nil || !clientcommon.ValidPhone(request.Phone) {
|
||||
if ctx.ShouldBindJSON(&request) != nil || !common.ValidPhone(request.Phone) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var account models.UserAccount
|
||||
if impl.DBService.Where("phone = ? AND status = ?", strings.TrimSpace(request.Phone), base.StatusEnable).First(&account).Error != nil {
|
||||
if impl.DBService.Where("phone = ? AND status = ?", strings.TrimSpace(request.Phone), common.StatusEnable).First(&account).Error != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
valid := request.Mode == "password" && bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.Password)) == nil
|
||||
if request.Mode == "verification_code" {
|
||||
valid = clientcommon.VerifyCode("user_app", account.Phone, "login", request.RequestIdentity, request.Code)
|
||||
valid = common.VerifyCode("user_app", account.Phone, "login", request.RequestIdentity, request.Code)
|
||||
}
|
||||
if !valid {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
accessToken, err := clientcommon.IssueToken(account.Identity, "user_app", "user", map[string]string{"phone": account.Phone})
|
||||
accessToken, err := common.IssueToken(account.Identity, "user_app", "user", map[string]string{"phone": account.Phone})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -65,19 +64,19 @@ func Register(ctx *gin.Context) {
|
||||
Code string `json:"code" binding:"required"`
|
||||
RequestIdentity string `json:"request_identity" binding:"required"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil || !clientcommon.ValidPhone(request.Phone) ||
|
||||
!base.IsValidAccountPassword(request.Password) ||
|
||||
!clientcommon.VerifyCode("user_app", request.Phone, "register", request.RequestIdentity, request.Code) {
|
||||
if ctx.ShouldBindJSON(&request) != nil || !common.ValidPhone(request.Phone) ||
|
||||
!common.IsValidAccountPassword(request.Password) ||
|
||||
!common.VerifyCode("user_app", request.Phone, "register", request.RequestIdentity, request.Code) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := base.PasswordHash(request.Password)
|
||||
hash, err := common.PasswordHash(request.Password)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
account := models.UserAccount{
|
||||
Entity: base.NewEntity(base.StatusEnable), Username: strings.TrimSpace(request.Phone),
|
||||
Entity: common.NewEntity(common.StatusEnable), Username: strings.TrimSpace(request.Phone),
|
||||
Phone: strings.TrimSpace(request.Phone), PasswordHash: hash, Name: strings.TrimSpace(request.Name),
|
||||
}
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
@@ -85,7 +84,7 @@ func Register(ctx *gin.Context) {
|
||||
return err
|
||||
}
|
||||
address := models.UserAddress{
|
||||
Entity: base.NewEntity(base.StatusEnable), UserAccountID: account.ID, Address: request.Address,
|
||||
Entity: common.NewEntity(common.StatusEnable), UserAccountID: account.ID, Address: request.Address,
|
||||
Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: true,
|
||||
}
|
||||
if err := tx.Create(&address).Error; err != nil {
|
||||
@@ -98,19 +97,19 @@ func Register(ctx *gin.Context) {
|
||||
return nil
|
||||
}
|
||||
var gas models.GasBasic
|
||||
if err := tx.Where("identity = ? AND status = ?", request.GasIdentity, base.StatusEnable).First(&gas).Error; err != nil {
|
||||
if err := tx.Where("identity = ? AND status = ?", request.GasIdentity, common.StatusEnable).First(&gas).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var deliveryID uint64
|
||||
if request.DeliveryIdentity != "" {
|
||||
var delivery models.DeliveryBasic
|
||||
if err := tx.Where("identity = ? AND gas_basic_id = ? AND status = ?", request.DeliveryIdentity, gas.ID, base.StatusEnable).First(&delivery).Error; err != nil {
|
||||
if err := tx.Where("identity = ? AND gas_basic_id = ? AND status = ?", request.DeliveryIdentity, gas.ID, common.StatusEnable).First(&delivery).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
deliveryID = delivery.ID
|
||||
}
|
||||
return tx.Create(&models.UserServiceRelation{
|
||||
Entity: base.NewEntity(base.StatusEnable), UserAccountID: account.ID,
|
||||
Entity: common.NewEntity(common.StatusEnable), UserAccountID: account.ID,
|
||||
GasBasicID: gas.ID, DeliveryBasicID: deliveryID,
|
||||
}).Error
|
||||
})
|
||||
@@ -123,7 +122,7 @@ func Register(ctx *gin.Context) {
|
||||
|
||||
// Profile 返回当前用户的脱敏资料。
|
||||
func Profile(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -132,7 +131,7 @@ func Profile(ctx *gin.Context) {
|
||||
|
||||
// UpdateProfile 只允许修改非认证资料。
|
||||
func UpdateProfile(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -153,7 +152,7 @@ func UpdateProfile(ctx *gin.Context) {
|
||||
|
||||
// ChangePassword 使用当前密码修改登录密码。
|
||||
func ChangePassword(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -161,12 +160,12 @@ func ChangePassword(ctx *gin.Context) {
|
||||
CurrentPassword string `json:"current_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil || !base.IsValidAccountPassword(request.NewPassword) ||
|
||||
if ctx.ShouldBindJSON(&request) != nil || !common.IsValidAccountPassword(request.NewPassword) ||
|
||||
bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.CurrentPassword)) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
hash, err := base.PasswordHash(request.NewPassword)
|
||||
hash, err := common.PasswordHash(request.NewPassword)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -186,18 +185,18 @@ func ResetPassword(ctx *gin.Context) {
|
||||
Code string `json:"code" binding:"required"`
|
||||
RequestIdentity string `json:"request_identity" binding:"required"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil || !base.IsValidAccountPassword(request.NewPassword) ||
|
||||
!clientcommon.VerifyCode("user_app", request.Phone, "reset_login_password", request.RequestIdentity, request.Code) {
|
||||
if ctx.ShouldBindJSON(&request) != nil || !common.IsValidAccountPassword(request.NewPassword) ||
|
||||
!common.VerifyCode("user_app", request.Phone, "reset_login_password", request.RequestIdentity, request.Code) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := base.PasswordHash(request.NewPassword)
|
||||
hash, err := common.PasswordHash(request.NewPassword)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
result := impl.DBService.Model(&models.UserAccount{}).
|
||||
Where("phone = ? AND status = ?", strings.TrimSpace(request.Phone), base.StatusEnable).
|
||||
Where("phone = ? AND status = ?", strings.TrimSpace(request.Phone), common.StatusEnable).
|
||||
Update("password_hash", hash)
|
||||
if result.Error != nil {
|
||||
infra.Response.Error(ctx, result.Error)
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
clientcommon "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/common"
|
||||
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -16,31 +15,31 @@ import (
|
||||
// PublicGasStations 提供注册页所需的最小启用气站数据。
|
||||
func PublicGasStations(ctx *gin.Context) {
|
||||
var list []models.GasBasic
|
||||
if err := impl.DBService.Select("identity", "name", "address").Where("status = ?", base.StatusEnable).Order("name").Find(&list).Error; err != nil {
|
||||
if err := impl.DBService.Select("identity", "name", "address").Where("status = ?", common.StatusEnable).Order("name").Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
|
||||
// PublicDeliveryPoints 提供指定气站下的启用配送点。
|
||||
func PublicDeliveryPoints(ctx *gin.Context) {
|
||||
var gas models.GasBasic
|
||||
if impl.DBService.Where("identity = ? AND status = ?", ctx.Query("gas_identity"), base.StatusEnable).First(&gas).Error != nil {
|
||||
if impl.DBService.Where("identity = ? AND status = ?", ctx.Query("gas_identity"), common.StatusEnable).First(&gas).Error != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
var list []models.DeliveryBasic
|
||||
if err := impl.DBService.Select("identity", "name", "address").Where("gas_basic_id = ? AND status = ?", gas.ID, base.StatusEnable).Order("name").Find(&list).Error; err != nil {
|
||||
if err := impl.DBService.Select("identity", "name", "address").Where("gas_basic_id = ? AND status = ?", gas.ID, common.StatusEnable).Order("name").Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
|
||||
// PublicContents 返回已发布内容,支持内容类型筛选。
|
||||
func PublicContents(ctx *gin.Context) {
|
||||
query := impl.DBService.Where("status = ? AND publish_status = ?", base.StatusEnable, "published")
|
||||
query := impl.DBService.Where("status = ? AND publish_status = ?", common.StatusEnable, "published")
|
||||
if contentType := strings.TrimSpace(ctx.Query("content_type")); contentType != "" {
|
||||
query = query.Where("content_type = ?", contentType)
|
||||
}
|
||||
@@ -49,12 +48,12 @@ func PublicContents(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
|
||||
// ConfirmContentRead 记录用户对特定内容版本的确认,幂等号全局唯一。
|
||||
func ConfirmContentRead(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -74,7 +73,7 @@ func ConfirmContentRead(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
record := models.CmsContentRead{
|
||||
Entity: base.NewEntity(base.StatusEnable), UserAccountID: account.ID, CmsContentID: content.ID,
|
||||
Entity: common.NewEntity(common.StatusEnable), UserAccountID: account.ID, CmsContentID: content.ID,
|
||||
VersionNo: content.VersionNo, ShownAt: time.Now(), ConfirmedAt: timePointer(time.Now()),
|
||||
ClientVersion: request.ClientVersion, DeviceIdentity: request.DeviceIdentity, RequestNo: request.RequestNo,
|
||||
}
|
||||
|
||||
@@ -4,20 +4,19 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
clientcommon "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/common"
|
||||
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ServiceRelation 返回当前唯一有效服务归属的公开 identity。
|
||||
func ServiceRelation(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var relation models.UserServiceRelation
|
||||
if impl.DBService.Where("user_account_id = ? AND status = ?", account.ID, base.StatusEnable).First(&relation).Error != nil {
|
||||
if impl.DBService.Where("user_account_id = ? AND status = ?", account.ID, common.StatusEnable).First(&relation).Error != nil {
|
||||
infra.Response.Success(ctx, nil)
|
||||
return
|
||||
}
|
||||
@@ -39,41 +38,41 @@ func ServiceRelation(ctx *gin.Context) {
|
||||
|
||||
// ListGasContracts 返回用户自己的供气合同。
|
||||
func ListGasContracts(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var list []models.GasorderContract
|
||||
if err := impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, base.StatusArchived).Order("created_at desc").Find(&list).Error; err != nil {
|
||||
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
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
|
||||
// ListGasOrders 返回用户自己的供气订单。
|
||||
func ListGasOrders(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var list []models.GasorderBasic
|
||||
if err := impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, base.StatusArchived).Order("created_at desc").Find(&list).Error; err != nil {
|
||||
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
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
|
||||
// CancelGasOrder 仅允许取消已创建或已分派的本人订单。
|
||||
func CancelGasOrder(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
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{base.StatusCreated, base.StatusAssigned}).
|
||||
Updates(map[string]any{"order_status": base.StatusCancelled, "operator_identity": account.Identity})
|
||||
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
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
clientcommon "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/common"
|
||||
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
common "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"
|
||||
@@ -18,16 +17,16 @@ import (
|
||||
// PublicProducts 返回上架且有库存的商品。
|
||||
func PublicProducts(ctx *gin.Context) {
|
||||
var list []models.EcProduct
|
||||
if err := impl.DBService.Where("status = ? AND stock_quantity > 0", base.StatusEnable).Order("created_at desc").Find(&list).Error; err != nil {
|
||||
if err := impl.DBService.Where("status = ? AND stock_quantity > 0", common.StatusEnable).Order("created_at desc").Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
|
||||
// CreateShopOrder 按服务端价格创建订单并原子扣减库存。
|
||||
func CreateShopOrder(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -42,17 +41,17 @@ func CreateShopOrder(ctx *gin.Context) {
|
||||
Quantity int `json:"quantity" binding:"required,gt=0"`
|
||||
} `json:"items" binding:"required,min=1"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&request) != nil || !clientcommon.ValidPhone(request.ContactPhone) {
|
||||
if ctx.ShouldBindJSON(&request) != nil || !common.ValidPhone(request.ContactPhone) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var address models.UserAddress
|
||||
if impl.DBService.Where("identity = ? AND user_account_id = ? AND status <> ?", request.AddressIdentity, account.ID, base.StatusArchived).First(&address).Error != nil {
|
||||
if impl.DBService.Where("identity = ? AND user_account_id = ? AND status <> ?", request.AddressIdentity, account.ID, common.StatusArchived).First(&address).Error != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
order := models.EcOrder{
|
||||
Entity: base.NewEntity(base.StatusEnable), OrderStatus: 16, OrderNo: clientcommon.RecordNo("EC"),
|
||||
Entity: common.NewEntity(common.StatusEnable), OrderStatus: 16, OrderNo: common.RecordNo("EC"),
|
||||
RequestNo: request.RequestNo, UserAccountID: account.ID, UserAddressID: address.ID,
|
||||
Address: address.Address, Longitude: address.Longitude, Latitude: address.Latitude,
|
||||
ContactName: request.ContactName, ContactPhone: request.ContactPhone, Remark: request.Remark, LogisticsStatus: 10,
|
||||
@@ -63,7 +62,7 @@ func CreateShopOrder(ctx *gin.Context) {
|
||||
for _, requested := range request.Items {
|
||||
var product models.EcProduct
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("identity = ? AND status = ? AND stock_quantity >= ?", requested.ProductIdentity, base.StatusEnable, requested.Quantity).
|
||||
Where("identity = ? AND status = ? AND stock_quantity >= ?", requested.ProductIdentity, common.StatusEnable, requested.Quantity).
|
||||
First(&product).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -72,7 +71,7 @@ func CreateShopOrder(ctx *gin.Context) {
|
||||
}
|
||||
snapshot, _ := json.Marshal(gin.H{"identity": product.Identity, "name": product.Name, "product_code": product.ProductCode})
|
||||
items = append(items, models.EcOrderItem{
|
||||
Entity: base.NewEntity(base.StatusEnable), EcProductID: product.ID, ProductSnapshot: string(snapshot),
|
||||
Entity: common.NewEntity(common.StatusEnable), EcProductID: product.ID, ProductSnapshot: string(snapshot),
|
||||
Quantity: requested.Quantity, SaleAmount: product.PriceAmount,
|
||||
})
|
||||
amount += product.PriceAmount * int64(requested.Quantity)
|
||||
@@ -97,26 +96,26 @@ func CreateShopOrder(ctx *gin.Context) {
|
||||
}
|
||||
order = existing
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(order))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(order))
|
||||
}
|
||||
|
||||
// ListShopOrders 返回本人的商城订单。
|
||||
func ListShopOrders(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var list []models.EcOrder
|
||||
if err := impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, base.StatusArchived).Order("created_at desc").Find(&list).Error; err != nil {
|
||||
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
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, common.ResourceResponse(list))
|
||||
}
|
||||
|
||||
// CancelShopOrder 取消未支付订单并恢复库存。
|
||||
func CancelShopOrder(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -146,7 +145,7 @@ func CancelShopOrder(ctx *gin.Context) {
|
||||
|
||||
// PayShopOrder 使用用户钱包余额支付,金额和订单状态由服务端锁定校验。
|
||||
func PayShopOrder(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -169,12 +168,13 @@ func PayShopOrder(ctx *gin.Context) {
|
||||
Where("owner_type = ? AND owner_identity = ?", "user", account.Identity).First(&wallet).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !clientcommon.VerifyPaymentPassword(account.Identity, wallet, request.PaymentPassword) ||
|
||||
wallet.Balance < order.PayableAmount {
|
||||
if !common.VerifyPaymentPassword(account.Identity, wallet, request.PaymentPassword) {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
wallet.Balance -= order.PayableAmount
|
||||
if err := tx.Model(&wallet).Update("balance", wallet.Balance).Error; err != nil {
|
||||
if err := common.SpendWalletBalance(&wallet, order.PayableAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := common.SaveWalletBalances(tx, wallet); err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
@@ -183,7 +183,7 @@ func PayShopOrder(ctx *gin.Context) {
|
||||
}
|
||||
date := now.In(time.Local)
|
||||
return tx.Create(&models.WalletRecord{
|
||||
Entity: base.NewEntity(base.StatusEnable), WalletBasicID: wallet.ID, RecordNo: clientcommon.RecordNo("WR"),
|
||||
Entity: common.NewEntity(common.StatusEnable), WalletBasicID: wallet.ID, RecordNo: common.RecordNo("WR"),
|
||||
RequestNo: request.RequestNo, Direction: "expense", TradeType: "ec_order",
|
||||
Amount: order.PayableAmount, BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance,
|
||||
OutTradeNo: order.OrderNo, PayChannel: "wallet", OperatorIdentity: account.Identity,
|
||||
@@ -199,7 +199,7 @@ func PayShopOrder(ctx *gin.Context) {
|
||||
|
||||
// ConfirmShopReceipt 只推进独立物流状态,不伪造支付状态。
|
||||
func ConfirmShopReceipt(ctx *gin.Context) {
|
||||
account, ok := clientcommon.UserAccount(ctx)
|
||||
account, ok := common.UserAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package common 提供两个客户端共用的鉴权、验证码和账户范围能力。
|
||||
// Package common 提供各业务端共用的鉴权、资源、钱包和账户范围能力。
|
||||
package common
|
||||
|
||||
import (
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
)
|
||||
|
||||
func TestValidPhone(t *testing.T) {
|
||||
func TestClientValidPhone(t *testing.T) {
|
||||
tests := map[string]bool{
|
||||
"13800138000": true,
|
||||
"12800138000": false,
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -49,7 +48,7 @@ func ensureWallet(tx *gorm.DB, owner walletOwner) (models.WalletBasic, error) {
|
||||
return wallet, err
|
||||
}
|
||||
wallet = models.WalletBasic{
|
||||
Entity: base.NewEntity(base.StatusEnable), OwnerType: owner.Type, OwnerID: owner.ID, OwnerIdentity: owner.Identity,
|
||||
Entity: NewEntity(StatusEnable), OwnerType: owner.Type, OwnerID: owner.ID, OwnerIdentity: owner.Identity,
|
||||
}
|
||||
if err := tx.Create(&wallet).Error; err != nil {
|
||||
return wallet, err
|
||||
@@ -139,7 +138,7 @@ func CreateRecharge(client string) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
order := models.WalletRechargeOrder{
|
||||
Entity: base.NewEntity(base.StatusEnable), RechargeStatus: 10, WalletBasicID: wallet.ID,
|
||||
Entity: NewEntity(StatusEnable), RechargeStatus: 10, WalletBasicID: wallet.ID,
|
||||
RechargeNo: RecordNo("RC"), RequestNo: request.RequestNo, Amount: request.Amount,
|
||||
Channel: request.Channel, OwnerType: owner.Type, OwnerIdentity: owner.Identity,
|
||||
}
|
||||
@@ -151,7 +150,7 @@ func CreateRecharge(client string) gin.HandlerFunc {
|
||||
}
|
||||
order = existing
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(order))
|
||||
infra.Response.Success(ctx, ResourceResponse(order))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +191,7 @@ func ConfirmMockRecharge(client string) gin.HandlerFunc {
|
||||
}
|
||||
date := now.In(time.Local)
|
||||
return tx.Create(&models.WalletRecord{
|
||||
Entity: base.NewEntity(base.StatusEnable), WalletBasicID: wallet.ID, RecordNo: RecordNo("WR"),
|
||||
Entity: NewEntity(StatusEnable), WalletBasicID: wallet.ID, RecordNo: RecordNo("WR"),
|
||||
RequestNo: "recharge:" + response.Identity, Direction: "income", TradeType: "recharge",
|
||||
Amount: response.Amount, BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance,
|
||||
InTradeNo: response.RechargeNo, PayChannel: "mock", OperatorIdentity: owner.Identity,
|
||||
@@ -224,7 +223,7 @@ func ListWalletRecords(client string) gin.HandlerFunc {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, ResourceResponse(list))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +240,7 @@ func ListBanks(client string) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
var banks []models.WalletBank
|
||||
if err := impl.DBService.Where("wallet_basic_id = ? AND status <> ?", wallet.ID, base.StatusArchived).Find(&banks).Error; err != nil {
|
||||
if err := impl.DBService.Where("wallet_basic_id = ? AND status <> ?", wallet.ID, StatusArchived).Find(&banks).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
@@ -293,7 +292,7 @@ func BindBank(client string) gin.HandlerFunc {
|
||||
idCipher, _, _ := protectField(request.IDCard)
|
||||
phoneCipher, _, _ := protectField(request.Phone)
|
||||
bank := models.WalletBank{
|
||||
Entity: base.NewEntity(base.StatusEnable), WalletBasicID: wallet.ID, CardNoCiphertext: cardCipher,
|
||||
Entity: NewEntity(StatusEnable), WalletBasicID: wallet.ID, CardNoCiphertext: cardCipher,
|
||||
CardFingerprint: fingerprint, CardNoLast4: request.CardNo[len(request.CardNo)-4:],
|
||||
BankName: request.BankName, CardOwner: request.CardOwner, IDCardCiphertext: idCipher,
|
||||
PhoneCiphertext: phoneCipher, BankType: request.BankType, Bank: request.Bank,
|
||||
@@ -328,7 +327,7 @@ func UnbindBank(client string) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
var bank models.WalletBank
|
||||
if impl.DBService.Where("identity = ? AND wallet_basic_id = ? AND status <> ?", ctx.Param("identity"), wallet.ID, base.StatusArchived).First(&bank).Error != nil {
|
||||
if impl.DBService.Where("identity = ? AND wallet_basic_id = ? AND status <> ?", ctx.Param("identity"), wallet.ID, StatusArchived).First(&bank).Error != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
@@ -338,7 +337,7 @@ func UnbindBank(client string) gin.HandlerFunc {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Model(&bank).Update("status", base.StatusArchived).Error; err != nil {
|
||||
if err := impl.DBService.Model(&bank).Update("status", StatusArchived).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
@@ -371,25 +370,22 @@ func CreateWithdrawal(client string) gin.HandlerFunc {
|
||||
}
|
||||
var apply models.WalletApplyCash
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&wallet, wallet.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if wallet.WithdrawalBalance < request.Amount {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
var bank models.WalletBank
|
||||
if err := tx.Where("identity = ? AND wallet_basic_id = ? AND status = ?", request.BankIdentity, wallet.ID, base.StatusEnable).First(&bank).Error; err != nil {
|
||||
if err := tx.Where("identity = ? AND wallet_basic_id = ? AND status = ?", request.BankIdentity, wallet.ID, StatusEnable).First(&bank).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
apply = models.WalletApplyCash{
|
||||
Entity: base.NewEntity(base.StatusEnable), ApplyStatus: 10, WalletBasicID: wallet.ID,
|
||||
WalletBankID: bank.ID, CashNo: RecordNo("WD"), RequestNo: request.RequestNo,
|
||||
Amount: request.Amount, Channel: "bank", Remark: request.Remark,
|
||||
}
|
||||
if err := tx.Create(&apply).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&wallet).Update("withdrawal_balance", gorm.Expr("withdrawal_balance - ?", request.Amount)).Error
|
||||
var createErr error
|
||||
apply, _, createErr = CreateReservedWithdrawal(tx, WalletWithdrawalInput{
|
||||
WalletBasicID: wallet.ID,
|
||||
WalletBankID: bank.ID,
|
||||
RequestNo: request.RequestNo,
|
||||
CashNo: RecordNo("WD"),
|
||||
Amount: request.Amount,
|
||||
Channel: "bank",
|
||||
Remark: request.Remark,
|
||||
OperatorIdentity: owner.Identity,
|
||||
})
|
||||
return createErr
|
||||
})
|
||||
if err != nil {
|
||||
var existing models.WalletApplyCash
|
||||
@@ -399,7 +395,7 @@ func CreateWithdrawal(client string) gin.HandlerFunc {
|
||||
}
|
||||
apply = existing
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(apply))
|
||||
infra.Response.Success(ctx, ResourceResponse(apply))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,7 +416,7 @@ func ListWithdrawals(client string) gin.HandlerFunc {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, base.ResourceResponse(list))
|
||||
infra.Response.Success(ctx, ResourceResponse(list))
|
||||
}
|
||||
}
|
||||
|
||||
253
backend/api/internal/logic/common/wallet_balance.go
Normal file
253
backend/api/internal/logic/common/wallet_balance.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidWalletAmount = errors.New("invalid wallet amount")
|
||||
ErrWalletUnavailable = errors.New("wallet is unavailable")
|
||||
ErrWalletBalance = errors.New("insufficient wallet balance")
|
||||
ErrWalletOverflow = errors.New("wallet balance overflow")
|
||||
ErrIdempotencyConflict = errors.New("idempotency request conflicts with existing withdrawal")
|
||||
)
|
||||
|
||||
// WalletWithdrawalInput 是统一提现预扣所需的最小业务输入。
|
||||
type WalletWithdrawalInput struct {
|
||||
WalletBasicID uint64
|
||||
WalletBankID uint64
|
||||
RequestNo string
|
||||
CashNo string
|
||||
Amount int64
|
||||
Channel string
|
||||
Remark string
|
||||
OperatorIdentity string
|
||||
OperatorName string
|
||||
}
|
||||
|
||||
// LockWalletForUpdate 锁定钱包事实行,所有资金扣减必须在同一事务内调用。
|
||||
func LockWalletForUpdate(tx *gorm.DB, walletID uint64, requireEnabled bool) (models.WalletBasic, error) {
|
||||
var wallet models.WalletBasic
|
||||
query := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", walletID)
|
||||
if requireEnabled {
|
||||
query = query.Where("status = ?", StatusEnable)
|
||||
}
|
||||
if err := query.First(&wallet).Error; err != nil {
|
||||
return wallet, err
|
||||
}
|
||||
return wallet, nil
|
||||
}
|
||||
|
||||
// SpendWalletBalance 扣减普通消费,并保证可提现余额始终不超过总余额。
|
||||
func SpendWalletBalance(wallet *models.WalletBasic, amount int64) error {
|
||||
if amount <= 0 {
|
||||
return ErrInvalidWalletAmount
|
||||
}
|
||||
if wallet.Status != StatusEnable {
|
||||
return ErrWalletUnavailable
|
||||
}
|
||||
if wallet.Balance < 0 || wallet.WithdrawalBalance < 0 {
|
||||
return ErrWalletBalance
|
||||
}
|
||||
if wallet.Balance < amount {
|
||||
return ErrWalletBalance
|
||||
}
|
||||
wallet.Balance -= amount
|
||||
if wallet.WithdrawalBalance > wallet.Balance {
|
||||
wallet.WithdrawalBalance = wallet.Balance
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReserveWalletWithdrawal 在申请时同时预扣总余额和可提现余额。
|
||||
func ReserveWalletWithdrawal(wallet *models.WalletBasic, amount int64) error {
|
||||
if amount <= 0 {
|
||||
return ErrInvalidWalletAmount
|
||||
}
|
||||
if wallet.Status != StatusEnable {
|
||||
return ErrWalletUnavailable
|
||||
}
|
||||
if wallet.Balance < 0 || wallet.WithdrawalBalance < 0 || wallet.WithdrawalBalance > wallet.Balance {
|
||||
return ErrWalletBalance
|
||||
}
|
||||
if wallet.Balance < amount || wallet.WithdrawalBalance < amount {
|
||||
return ErrWalletBalance
|
||||
}
|
||||
wallet.Balance -= amount
|
||||
wallet.WithdrawalBalance -= amount
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReleaseWalletWithdrawal 在提现驳回时原样返还此前预扣的两类余额。
|
||||
func ReleaseWalletWithdrawal(wallet *models.WalletBasic, amount int64) error {
|
||||
if amount <= 0 {
|
||||
return ErrInvalidWalletAmount
|
||||
}
|
||||
if wallet.Balance < 0 || wallet.WithdrawalBalance < 0 || wallet.WithdrawalBalance > wallet.Balance {
|
||||
return ErrWalletBalance
|
||||
}
|
||||
if wallet.Balance > math.MaxInt64-amount || wallet.WithdrawalBalance > math.MaxInt64-amount {
|
||||
return ErrWalletOverflow
|
||||
}
|
||||
wallet.Balance += amount
|
||||
wallet.WithdrawalBalance += amount
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReleaseLegacyWithdrawalBalance 兼容旧客户端申请只预扣可提现余额的历史记录。
|
||||
func ReleaseLegacyWithdrawalBalance(wallet *models.WalletBasic, amount int64) error {
|
||||
if amount <= 0 {
|
||||
return ErrInvalidWalletAmount
|
||||
}
|
||||
if wallet.WithdrawalBalance > math.MaxInt64-amount {
|
||||
return ErrWalletOverflow
|
||||
}
|
||||
wallet.WithdrawalBalance += amount
|
||||
if wallet.WithdrawalBalance > wallet.Balance {
|
||||
wallet.WithdrawalBalance = wallet.Balance
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReleaseWithdrawalApplication 按新旧申请的实际预扣方式返还余额。
|
||||
func ReleaseWithdrawalApplication(wallet *models.WalletBasic, application models.WalletApplyCash) (bool, error) {
|
||||
if application.BalanceReserved {
|
||||
return true, ReleaseWalletWithdrawal(wallet, application.Amount)
|
||||
}
|
||||
if wallet.OwnerType == "user" || wallet.OwnerType == "staff" {
|
||||
return true, ReleaseLegacyWithdrawalBalance(wallet, application.Amount)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SettleLegacyWithdrawal 为升级前未完整预扣的申请补扣余额。
|
||||
func SettleLegacyWithdrawal(wallet *models.WalletBasic, application models.WalletApplyCash) (bool, error) {
|
||||
if application.BalanceReserved {
|
||||
return false, nil
|
||||
}
|
||||
if wallet.OwnerType == "user" || wallet.OwnerType == "staff" {
|
||||
return true, SpendWalletBalance(wallet, application.Amount)
|
||||
}
|
||||
return true, ReserveWalletWithdrawal(wallet, application.Amount)
|
||||
}
|
||||
|
||||
// SaveWalletBalances 将内存中已校验的余额快照写回当前事务。
|
||||
func SaveWalletBalances(tx *gorm.DB, wallet models.WalletBasic) error {
|
||||
result := tx.Model(&models.WalletBasic{}).Where("id = ?", wallet.ID).Updates(map[string]any{
|
||||
"balance": wallet.Balance,
|
||||
"withdrawal_balance": wallet.WithdrawalBalance,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return ErrWalletUnavailable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateReservedWithdrawal 幂等创建提现申请,并在同一事务中预扣两类余额和写入流水。
|
||||
func CreateReservedWithdrawal(tx *gorm.DB, input WalletWithdrawalInput) (models.WalletApplyCash, bool, error) {
|
||||
wallet, err := LockWalletForUpdate(tx, input.WalletBasicID, false)
|
||||
if err != nil {
|
||||
return models.WalletApplyCash{}, false, err
|
||||
}
|
||||
var existing models.WalletApplyCash
|
||||
err = tx.Where("request_no = ?", input.RequestNo).First(&existing).Error
|
||||
if err == nil {
|
||||
if existing.WalletBasicID != input.WalletBasicID ||
|
||||
existing.WalletBankID != input.WalletBankID ||
|
||||
existing.Amount != input.Amount ||
|
||||
existing.Channel != input.Channel {
|
||||
return existing, false, ErrIdempotencyConflict
|
||||
}
|
||||
return existing, false, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return models.WalletApplyCash{}, false, err
|
||||
}
|
||||
|
||||
if err := ReserveWalletWithdrawal(&wallet, input.Amount); err != nil {
|
||||
return models.WalletApplyCash{}, false, err
|
||||
}
|
||||
if input.CashNo == "" {
|
||||
input.CashNo = models.NewIdentity()
|
||||
}
|
||||
application := models.WalletApplyCash{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
ApplyStatus: StatusPending,
|
||||
WalletBasicID: input.WalletBasicID,
|
||||
WalletBankID: input.WalletBankID,
|
||||
CashNo: input.CashNo,
|
||||
RequestNo: input.RequestNo,
|
||||
Amount: input.Amount,
|
||||
Channel: input.Channel,
|
||||
Remark: input.Remark,
|
||||
BalanceReserved: true,
|
||||
}
|
||||
if err := tx.Create(&application).Error; err != nil {
|
||||
return models.WalletApplyCash{}, false, err
|
||||
}
|
||||
if err := SaveWalletBalances(tx, wallet); err != nil {
|
||||
return models.WalletApplyCash{}, false, err
|
||||
}
|
||||
record := NewWalletBalanceRecord(
|
||||
wallet,
|
||||
"withdrawal-reserve:"+application.Identity,
|
||||
"expense",
|
||||
"withdrawal_reserve",
|
||||
application.Amount,
|
||||
"",
|
||||
application.CashNo,
|
||||
application.Channel,
|
||||
input.OperatorIdentity,
|
||||
input.OperatorName,
|
||||
input.Remark,
|
||||
)
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return models.WalletApplyCash{}, false, err
|
||||
}
|
||||
return application, true, nil
|
||||
}
|
||||
|
||||
// NewWalletBalanceRecord 创建带完整余额快照的不可变资金流水。
|
||||
func NewWalletBalanceRecord(
|
||||
wallet models.WalletBasic,
|
||||
requestNo string,
|
||||
direction string,
|
||||
tradeType string,
|
||||
amount int64,
|
||||
inTradeNo string,
|
||||
outTradeNo string,
|
||||
channel string,
|
||||
operatorIdentity string,
|
||||
operatorName string,
|
||||
remark string,
|
||||
) models.WalletRecord {
|
||||
now := time.Now()
|
||||
return models.WalletRecord{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
WalletBasicID: wallet.ID,
|
||||
RecordNo: models.NewIdentity(),
|
||||
RequestNo: requestNo,
|
||||
Direction: direction,
|
||||
TradeType: tradeType,
|
||||
Amount: amount,
|
||||
BalanceAfter: wallet.Balance,
|
||||
WithdrawalBalanceAfter: wallet.WithdrawalBalance,
|
||||
InTradeNo: inTradeNo,
|
||||
OutTradeNo: outTradeNo,
|
||||
PayChannel: channel,
|
||||
OperatorIdentity: operatorIdentity,
|
||||
OperatorName: operatorName,
|
||||
Ymd: int32(now.Year()*10000 + int(now.Month())*100 + now.Day()),
|
||||
Ym: int32(now.Year()*100 + int(now.Month())),
|
||||
Remark: remark,
|
||||
}
|
||||
}
|
||||
212
backend/api/internal/logic/common/wallet_balance_test.go
Normal file
212
backend/api/internal/logic/common/wallet_balance_test.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
)
|
||||
|
||||
func TestSpendWalletBalanceClosesWithdrawableGap(t *testing.T) {
|
||||
wallet := models.WalletBasic{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
Balance: 10_000,
|
||||
WithdrawalBalance: 10_000,
|
||||
}
|
||||
|
||||
if err := SpendWalletBalance(&wallet, 8_000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if wallet.Balance != 2_000 || wallet.WithdrawalBalance != 2_000 {
|
||||
t.Fatalf("balances after expense = (%d, %d), want (2000, 2000)", wallet.Balance, wallet.WithdrawalBalance)
|
||||
}
|
||||
if err := ReserveWalletWithdrawal(&wallet, 10_000); err != ErrWalletBalance {
|
||||
t.Fatalf("withdrawal after expense error = %v, want %v", err, ErrWalletBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpendWalletBalanceConsumesNonWithdrawableBalanceFirst(t *testing.T) {
|
||||
wallet := models.WalletBasic{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
Balance: 15_000,
|
||||
WithdrawalBalance: 10_000,
|
||||
}
|
||||
|
||||
if err := SpendWalletBalance(&wallet, 4_000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if wallet.Balance != 11_000 || wallet.WithdrawalBalance != 10_000 {
|
||||
t.Fatalf("balances after first expense = (%d, %d), want (11000, 10000)", wallet.Balance, wallet.WithdrawalBalance)
|
||||
}
|
||||
if err := SpendWalletBalance(&wallet, 2_000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if wallet.Balance != 9_000 || wallet.WithdrawalBalance != 9_000 {
|
||||
t.Fatalf("balances after second expense = (%d, %d), want (9000, 9000)", wallet.Balance, wallet.WithdrawalBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithdrawalReserveAndRejectAreExactInverse(t *testing.T) {
|
||||
wallet := models.WalletBasic{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
Balance: 10_000,
|
||||
WithdrawalBalance: 10_000,
|
||||
}
|
||||
|
||||
if err := ReserveWalletWithdrawal(&wallet, 10_000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if wallet.Balance != 0 || wallet.WithdrawalBalance != 0 {
|
||||
t.Fatalf("reserved balances = (%d, %d), want (0, 0)", wallet.Balance, wallet.WithdrawalBalance)
|
||||
}
|
||||
if err := ReleaseWalletWithdrawal(&wallet, 10_000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if wallet.Balance != 10_000 || wallet.WithdrawalBalance != 10_000 {
|
||||
t.Fatalf("released balances = (%d, %d), want (10000, 10000)", wallet.Balance, wallet.WithdrawalBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyWithdrawalReleaseCannotExceedRemainingBalance(t *testing.T) {
|
||||
wallet := models.WalletBasic{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
Balance: 2_000,
|
||||
WithdrawalBalance: 2_000,
|
||||
}
|
||||
|
||||
if err := ReleaseLegacyWithdrawalBalance(&wallet, 8_000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if wallet.Balance != 2_000 || wallet.WithdrawalBalance != 2_000 {
|
||||
t.Fatalf("legacy released balances = (%d, %d), want (2000, 2000)", wallet.Balance, wallet.WithdrawalBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrozenWalletRejectsNewDebits(t *testing.T) {
|
||||
wallet := models.WalletBasic{
|
||||
Entity: NewEntity(StatusFrozen),
|
||||
Balance: 10_000,
|
||||
WithdrawalBalance: 10_000,
|
||||
}
|
||||
|
||||
if err := SpendWalletBalance(&wallet, 1); err != ErrWalletUnavailable {
|
||||
t.Fatalf("expense error = %v, want %v", err, ErrWalletUnavailable)
|
||||
}
|
||||
if err := ReserveWalletWithdrawal(&wallet, 1); err != ErrWalletUnavailable {
|
||||
t.Fatalf("withdrawal error = %v, want %v", err, ErrWalletUnavailable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithdrawalReleaseRejectsOverflow(t *testing.T) {
|
||||
wallet := models.WalletBasic{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
Balance: math.MaxInt64,
|
||||
WithdrawalBalance: math.MaxInt64,
|
||||
}
|
||||
|
||||
if err := ReleaseWalletWithdrawal(&wallet, 1); err != ErrWalletOverflow {
|
||||
t.Fatalf("release error = %v, want %v", err, ErrWalletOverflow)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWithdrawalRejectionRestoresBothBalances(t *testing.T) {
|
||||
wallet := models.WalletBasic{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
OwnerType: "delivery",
|
||||
Balance: 2_000,
|
||||
WithdrawalBalance: 2_000,
|
||||
}
|
||||
application := models.WalletApplyCash{Amount: 8_000, BalanceReserved: true}
|
||||
|
||||
released, err := ReleaseWithdrawalApplication(&wallet, application)
|
||||
if err != nil || !released {
|
||||
t.Fatalf("release = (%v, %v), want (true, nil)", released, err)
|
||||
}
|
||||
if wallet.Balance != 10_000 || wallet.WithdrawalBalance != 10_000 {
|
||||
t.Fatalf("released balances = (%d, %d), want (10000, 10000)", wallet.Balance, wallet.WithdrawalBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyOrganizationWithdrawalRejectionDoesNotMintBalance(t *testing.T) {
|
||||
wallet := models.WalletBasic{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
OwnerType: "delivery",
|
||||
Balance: 10_000,
|
||||
WithdrawalBalance: 10_000,
|
||||
}
|
||||
application := models.WalletApplyCash{Amount: 8_000}
|
||||
|
||||
released, err := ReleaseWithdrawalApplication(&wallet, application)
|
||||
if err != nil || released {
|
||||
t.Fatalf("release = (%v, %v), want (false, nil)", released, err)
|
||||
}
|
||||
if wallet.Balance != 10_000 || wallet.WithdrawalBalance != 10_000 {
|
||||
t.Fatalf("legacy organization balances changed to (%d, %d)", wallet.Balance, wallet.WithdrawalBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyWithdrawalCompletionDebitsMissingBalances(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ownerType string
|
||||
withdrawalBalance int64
|
||||
wantBalance int64
|
||||
wantWithdrawal int64
|
||||
}{
|
||||
{
|
||||
name: "client already reserved withdrawable balance",
|
||||
ownerType: "user",
|
||||
withdrawalBalance: 2_000,
|
||||
wantBalance: 2_000,
|
||||
wantWithdrawal: 2_000,
|
||||
},
|
||||
{
|
||||
name: "organization reserved neither balance",
|
||||
ownerType: "delivery",
|
||||
withdrawalBalance: 10_000,
|
||||
wantBalance: 2_000,
|
||||
wantWithdrawal: 2_000,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
wallet := models.WalletBasic{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
OwnerType: test.ownerType,
|
||||
Balance: 10_000,
|
||||
WithdrawalBalance: test.withdrawalBalance,
|
||||
}
|
||||
changed, err := SettleLegacyWithdrawal(&wallet, models.WalletApplyCash{Amount: 8_000})
|
||||
if err != nil || !changed {
|
||||
t.Fatalf("settle = (%v, %v), want (true, nil)", changed, err)
|
||||
}
|
||||
if wallet.Balance != test.wantBalance || wallet.WithdrawalBalance != test.wantWithdrawal {
|
||||
t.Fatalf(
|
||||
"settled balances = (%d, %d), want (%d, %d)",
|
||||
wallet.Balance,
|
||||
wallet.WithdrawalBalance,
|
||||
test.wantBalance,
|
||||
test.wantWithdrawal,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReservedWithdrawalCompletionDoesNotDebitAgain(t *testing.T) {
|
||||
wallet := models.WalletBasic{
|
||||
Entity: NewEntity(StatusEnable),
|
||||
OwnerType: "user",
|
||||
Balance: 2_000,
|
||||
WithdrawalBalance: 2_000,
|
||||
}
|
||||
application := models.WalletApplyCash{Amount: 8_000, BalanceReserved: true}
|
||||
|
||||
changed, err := SettleLegacyWithdrawal(&wallet, application)
|
||||
if err != nil || changed {
|
||||
t.Fatalf("settle = (%v, %v), want (false, nil)", changed, err)
|
||||
}
|
||||
if wallet.Balance != 2_000 || wallet.WithdrawalBalance != 2_000 {
|
||||
t.Fatalf("reserved completion changed balances to (%d, %d)", wallet.Balance, wallet.WithdrawalBalance)
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ func Recharge(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func CreateApplyCash(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
account, point, _, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -162,29 +162,31 @@ func CreateApplyCash(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var pending int64
|
||||
if err := db().Model(&models.WalletApplyCash{}).Where("wallet_basic_id = ? AND apply_status = ? AND status <> ?",
|
||||
wallet.ID, common.StatusPending, common.StatusArchived).Select("COALESCE(SUM(amount), 0)").Scan(&pending).Error; err != nil ||
|
||||
request.Amount > wallet.WithdrawalBalance-pending {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var bankID uint64
|
||||
if request.WalletBankIdentity != "" {
|
||||
var bank models.WalletBank
|
||||
if err := common.ActiveRecords(db()).Where("identity = ? AND wallet_basic_id = ?",
|
||||
request.WalletBankIdentity, wallet.ID).First(&bank).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
var apply models.WalletApplyCash
|
||||
err := db().Transaction(func(tx *gorm.DB) error {
|
||||
var bankID uint64
|
||||
if request.WalletBankIdentity != "" {
|
||||
var bank models.WalletBank
|
||||
if err := common.ActiveRecords(tx).Where("identity = ? AND wallet_basic_id = ?",
|
||||
request.WalletBankIdentity, wallet.ID).First(&bank).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
bankID = bank.ID
|
||||
}
|
||||
bankID = bank.ID
|
||||
}
|
||||
apply := models.WalletApplyCash{
|
||||
Entity: common.NewEntity(common.StatusEnable), ApplyStatus: common.StatusPending,
|
||||
WalletBasicID: wallet.ID, WalletBankID: bankID, CashNo: models.NewIdentity(), RequestNo: request.RequestNo,
|
||||
Amount: request.Amount, Channel: request.Channel, Remark: request.Remark,
|
||||
}
|
||||
if err := db().Create(&apply).Error; err != nil {
|
||||
var createErr error
|
||||
apply, _, createErr = common.CreateReservedWithdrawal(tx, common.WalletWithdrawalInput{
|
||||
WalletBasicID: wallet.ID,
|
||||
WalletBankID: bankID,
|
||||
RequestNo: request.RequestNo,
|
||||
Amount: request.Amount,
|
||||
Channel: request.Channel,
|
||||
Remark: request.Remark,
|
||||
OperatorIdentity: account.Identity,
|
||||
OperatorName: account.DisplayName,
|
||||
})
|
||||
return createErr
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
func currentWallet(ctx *gin.Context, gas models.GasBasic) (models.WalletBasic, bool) {
|
||||
@@ -58,7 +59,7 @@ func ListWalletApplyCash(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func CreateWalletApplyCash(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
account, station, ok := CurrentGasAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -78,30 +79,31 @@ func CreateWalletApplyCash(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var pendingAmount int64
|
||||
if err := impl.DBService.Model(&models.WalletApplyCash{}).
|
||||
Where("wallet_basic_id = ? AND status <> ? AND apply_status = ?", wallet.ID, common.StatusArchived, common.StatusPending).
|
||||
Select("COALESCE(SUM(amount), 0)").Scan(&pendingAmount).Error; err != nil ||
|
||||
request.Amount > wallet.WithdrawalBalance-pendingAmount {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var bankID uint64
|
||||
if request.WalletBankIdentity != "" {
|
||||
var bank models.WalletBank
|
||||
if err := common.ActiveRecords(impl.DBService).
|
||||
Where("identity = ? AND wallet_basic_id = ?", request.WalletBankIdentity, wallet.ID).First(&bank).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
var apply models.WalletApplyCash
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var bankID uint64
|
||||
if request.WalletBankIdentity != "" {
|
||||
var bank models.WalletBank
|
||||
if err := common.ActiveRecords(tx).
|
||||
Where("identity = ? AND wallet_basic_id = ?", request.WalletBankIdentity, wallet.ID).First(&bank).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
bankID = bank.ID
|
||||
}
|
||||
bankID = bank.ID
|
||||
}
|
||||
apply := models.WalletApplyCash{
|
||||
Entity: common.NewEntity(common.StatusEnable), ApplyStatus: common.StatusPending,
|
||||
WalletBasicID: wallet.ID, WalletBankID: bankID, CashNo: models.NewIdentity(),
|
||||
RequestNo: request.RequestNo, Amount: request.Amount, Channel: request.Channel, Remark: request.Remark,
|
||||
}
|
||||
if err := impl.DBService.Create(&apply).Error; err != nil {
|
||||
var createErr error
|
||||
apply, _, createErr = common.CreateReservedWithdrawal(tx, common.WalletWithdrawalInput{
|
||||
WalletBasicID: wallet.ID,
|
||||
WalletBankID: bankID,
|
||||
RequestNo: request.RequestNo,
|
||||
Amount: request.Amount,
|
||||
Channel: request.Channel,
|
||||
Remark: request.Remark,
|
||||
OperatorIdentity: account.Identity,
|
||||
OperatorName: account.DisplayName,
|
||||
})
|
||||
return createErr
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -311,14 +311,34 @@ func reviewWalletApplyCash(ctx *gin.Context, targetStatus int) {
|
||||
}
|
||||
now := time.Now()
|
||||
if targetStatus == common.StatusRejected {
|
||||
result := tx.Model(&models.WalletBasic{}).
|
||||
Where("id = ? AND withdrawal_balance <= ?", application.WalletBasicID, math.MaxInt64-application.Amount).
|
||||
Update("withdrawal_balance", gorm.Expr("withdrawal_balance + ?", application.Amount))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
wallet, err := common.LockWalletForUpdate(tx, application.WalletBasicID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return errors.New("withdrawal balance overflow")
|
||||
released, err := common.ReleaseWithdrawalApplication(&wallet, application)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if released {
|
||||
if err := common.SaveWalletBalances(tx, wallet); err != nil {
|
||||
return err
|
||||
}
|
||||
record := common.NewWalletBalanceRecord(
|
||||
wallet,
|
||||
"withdrawal-reject:"+application.Identity,
|
||||
"income",
|
||||
"withdrawal_release",
|
||||
application.Amount,
|
||||
application.CashNo,
|
||||
"",
|
||||
application.Channel,
|
||||
operatorIdentity,
|
||||
operatorName,
|
||||
request.Reason,
|
||||
)
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return tx.Model(&application).Updates(map[string]any{
|
||||
@@ -359,10 +379,43 @@ func CompleteWalletApplyCash(ctx *gin.Context) {
|
||||
if application.ApplyStatus != common.StatusApproved {
|
||||
return errors.New("cash application is not approved")
|
||||
}
|
||||
if !application.BalanceReserved {
|
||||
wallet, err := common.LockWalletForUpdate(tx, application.WalletBasicID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changed, err := common.SettleLegacyWithdrawal(&wallet, application)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !changed {
|
||||
return errors.New("legacy withdrawal settlement did not change wallet")
|
||||
}
|
||||
if err := common.SaveWalletBalances(tx, wallet); err != nil {
|
||||
return err
|
||||
}
|
||||
operatorIdentity, operatorName := common.PlatformOperator(ctx)
|
||||
record := common.NewWalletBalanceRecord(
|
||||
wallet,
|
||||
"withdrawal-complete:"+application.Identity,
|
||||
"expense",
|
||||
"withdrawal_complete",
|
||||
application.Amount,
|
||||
"",
|
||||
request.TradeNo,
|
||||
application.Channel,
|
||||
operatorIdentity,
|
||||
operatorName,
|
||||
"历史提现申请完成时补记总余额",
|
||||
)
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
return tx.Model(&application).Updates(map[string]any{
|
||||
"apply_status": common.StatusCompleted, "trade_no": strings.TrimSpace(request.TradeNo),
|
||||
"callback_msg": request.CallbackMsg, "completed_at": &now,
|
||||
"callback_msg": request.CallbackMsg, "completed_at": &now, "balance_reserved": true,
|
||||
}).Error
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -25,6 +25,7 @@ type WalletApplyCash struct {
|
||||
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamptz" json:"reviewed_at"` // 审核时间
|
||||
ReviewReason string `gorm:"column:review_reason;type:text;not null;default:''" json:"review_reason"` // 审核原因
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 完成时间
|
||||
BalanceReserved bool `gorm:"column:balance_reserved;not null;default:false;index" json:"balance_reserved"` // 是否已在申请时同时预扣总余额和可提现余额
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletApplyCash{}) }
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
sdkmiddleware "git.apinb.com/bsm-sdk/core/middleware"
|
||||
clientcommon "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/common"
|
||||
stafflogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/staff"
|
||||
userlogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/user"
|
||||
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ func RegisterClient(serviceKey string, engine *gin.Engine) {
|
||||
func registerUserClient(serviceKey string, engine *gin.Engine) {
|
||||
basePath := fmt.Sprintf("/%s/client/v1/user", serviceKey)
|
||||
anonymous := engine.Group(basePath)
|
||||
anonymous.POST("/auth/verification-code", clientcommon.SendVerificationCode("user_app"))
|
||||
anonymous.POST("/auth/verification-code", common.SendVerificationCode("user_app"))
|
||||
anonymous.POST("/auth/register", userlogic.Register)
|
||||
anonymous.POST("/auth/login", userlogic.Login)
|
||||
anonymous.POST("/auth/reset-password", userlogic.ResetPassword)
|
||||
@@ -29,7 +29,7 @@ func registerUserClient(serviceKey string, engine *gin.Engine) {
|
||||
anonymous.GET("/public/products", userlogic.PublicProducts)
|
||||
|
||||
protected := engine.Group(basePath)
|
||||
protected.Use(sdkmiddleware.JwtAuth(true), clientcommon.RequireClient("user_app"))
|
||||
protected.Use(sdkmiddleware.JwtAuth(true), common.RequireClient("user_app"))
|
||||
protected.GET("/auth/profile", userlogic.Profile)
|
||||
protected.PUT("/auth/profile", userlogic.UpdateProfile)
|
||||
protected.PUT("/auth/password", userlogic.ChangePassword)
|
||||
@@ -55,12 +55,12 @@ func registerUserClient(serviceKey string, engine *gin.Engine) {
|
||||
func registerStaffClient(serviceKey string, engine *gin.Engine) {
|
||||
basePath := fmt.Sprintf("/%s/client/v1/staff", serviceKey)
|
||||
anonymous := engine.Group(basePath)
|
||||
anonymous.POST("/auth/verification-code", clientcommon.SendVerificationCode("service_app"))
|
||||
anonymous.POST("/auth/verification-code", common.SendVerificationCode("service_app"))
|
||||
anonymous.POST("/auth/login", stafflogic.Login)
|
||||
anonymous.POST("/auth/reset-password", stafflogic.ResetPassword)
|
||||
|
||||
protected := engine.Group(basePath)
|
||||
protected.Use(sdkmiddleware.JwtAuth(true), clientcommon.RequireClient("service_app"))
|
||||
protected.Use(sdkmiddleware.JwtAuth(true), common.RequireClient("service_app"))
|
||||
protected.GET("/auth/profile", stafflogic.Profile)
|
||||
protected.GET("/preflight", stafflogic.Preflight)
|
||||
protected.PUT("/auth/password", stafflogic.ChangePassword)
|
||||
@@ -83,14 +83,14 @@ func registerStaffClient(serviceKey string, engine *gin.Engine) {
|
||||
}
|
||||
|
||||
func registerClientWalletRoutes(group *gin.RouterGroup, client string) {
|
||||
group.GET("/wallet", clientcommon.GetWallet(client))
|
||||
group.PUT("/wallet/payment-password", clientcommon.SetPaymentPassword(client))
|
||||
group.GET("/wallet/records", clientcommon.ListWalletRecords(client))
|
||||
group.POST("/wallet/recharges", clientcommon.CreateRecharge(client))
|
||||
group.POST("/wallet/recharges/:identity/mock-confirm", clientcommon.ConfirmMockRecharge(client))
|
||||
group.GET("/wallet/banks", clientcommon.ListBanks(client))
|
||||
group.POST("/wallet/banks", clientcommon.BindBank(client))
|
||||
group.DELETE("/wallet/banks/:identity", clientcommon.UnbindBank(client))
|
||||
group.GET("/wallet/withdrawals", clientcommon.ListWithdrawals(client))
|
||||
group.POST("/wallet/withdrawals", clientcommon.CreateWithdrawal(client))
|
||||
group.GET("/wallet", common.GetWallet(client))
|
||||
group.PUT("/wallet/payment-password", common.SetPaymentPassword(client))
|
||||
group.GET("/wallet/records", common.ListWalletRecords(client))
|
||||
group.POST("/wallet/recharges", common.CreateRecharge(client))
|
||||
group.POST("/wallet/recharges/:identity/mock-confirm", common.ConfirmMockRecharge(client))
|
||||
group.GET("/wallet/banks", common.ListBanks(client))
|
||||
group.POST("/wallet/banks", common.BindBank(client))
|
||||
group.DELETE("/wallet/banks/:identity", common.UnbindBank(client))
|
||||
group.GET("/wallet/withdrawals", common.ListWithdrawals(client))
|
||||
group.POST("/wallet/withdrawals", common.CreateWithdrawal(client))
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ func MockData(database *gorm.DB) error {
|
||||
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,
|
||||
ReviewedAt: &now, ReviewReason: "模拟审核通过", CompletedAt: &now, BalanceReserved: true,
|
||||
}
|
||||
if err := put(tx, &applyCash); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user