fix(wallet): enforce consistent withdrawal accounting

This commit is contained in:
david
2026-07-31 09:54:17 +08:00
parent 36a5ced1c0
commit 42a2322c8e
23 changed files with 1405 additions and 250 deletions

View File

@@ -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)

View File

@@ -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{}

View File

@@ -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
}