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

@@ -1,143 +0,0 @@
// Package common 提供两个客户端共用的鉴权、验证码和账户范围能力。
package common
import (
"fmt"
"regexp"
"strings"
"time"
"git.apinb.com/bsm-sdk/core/crypto/token"
"git.apinb.com/bsm-sdk/core/env"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
sdkmiddleware "git.apinb.com/bsm-sdk/core/middleware"
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
)
var phonePattern = regexp.MustCompile(`^1[3-9]\d{9}$`)
var verificationPurposes = map[string]struct{}{
"login": {}, "register": {}, "reset_login_password": {}, "set_payment_password": {},
"reset_payment_password": {}, "bind_bank": {}, "unbind_bank": {},
}
type verificationValue struct {
Code string `json:"code"`
Phone string `json:"phone"`
Purpose string `json:"purpose"`
Client string `json:"client"`
}
// ValidPhone 判断手机号是否符合中国大陆手机号格式。
func ValidPhone(phone string) bool { return phonePattern.MatchString(strings.TrimSpace(phone)) }
// SendVerificationCode 创建一次性验证码。Mock 模式的验证码只保存在 Redis,不返回给客户端。
func SendVerificationCode(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
var request struct {
Phone string `json:"phone" binding:"required"`
Purpose string `json:"purpose" binding:"required"`
}
if ctx.ShouldBindJSON(&request) != nil || !ValidPhone(request.Phone) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if _, ok := verificationPurposes[request.Purpose]; !ok {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
phone := strings.TrimSpace(request.Phone)
throttleKey := impl.RedisService.BuildKey("client-verification-throttle", client, phone)
var sent bool
if impl.RedisService.Get(throttleKey, &sent) == nil && sent {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
requestIdentity := models.NewIdentity()
value := verificationValue{Code: config.Spec.Global.MockVerificationCode, Phone: phone, Purpose: request.Purpose, Client: client}
ttl := time.Duration(config.Spec.Global.VerificationTTLSeconds) * time.Second
if err := impl.RedisService.Set(verificationKey(requestIdentity), value, ttl); err != nil {
infra.Response.Error(ctx, err)
return
}
_ = impl.RedisService.Set(throttleKey, true, time.Duration(config.Spec.Global.VerificationSendIntervalSeconds)*time.Second)
infra.Response.Success(ctx, gin.H{"request_identity": requestIdentity, "expires_in": config.Spec.Global.VerificationTTLSeconds})
}
}
// VerifyCode 校验并消费验证码。
func VerifyCode(client, phone, purpose, requestIdentity, code string) bool {
if !config.Spec.Global.MockVerificationEnabled || requestIdentity == "" || code == "" {
return false
}
key := verificationKey(requestIdentity)
var value verificationValue
if impl.RedisService.Get(key, &value) != nil {
return false
}
if value.Client != client || value.Phone != strings.TrimSpace(phone) || value.Purpose != purpose || value.Code != code {
return false
}
return impl.RedisService.Delete(key) == nil
}
func verificationKey(identity string) string {
return impl.RedisService.BuildKey("client-verification", identity)
}
// IssueToken 签发严格区分 user_app 和 service_app 的 JWT。
func IssueToken(identity, client, role string, extend map[string]string) (string, error) {
return token.New(env.Runtime.JwtSecretKey).GenerateJwt(0, identity, client, role, nil, extend)
}
// RequireClient 验证客户端种类,阻止后台令牌跨端调用。
func RequireClient(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
claims, err := sdkmiddleware.ParseAuth(ctx)
if err != nil || claims.Client != client {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
ctx.Abort()
return
}
ctx.Next()
}
}
// UserAccount 返回当前启用的用户账户。
func UserAccount(ctx *gin.Context) (models.UserAccount, bool) {
claims, err := sdkmiddleware.ParseAuth(ctx)
if err != nil {
infra.Response.Error(ctx, err)
return models.UserAccount{}, false
}
var account models.UserAccount
if impl.DBService.Where("identity = ? AND status = ?", claims.Identity, 1).First(&account).Error != nil {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
return models.UserAccount{}, false
}
return account, true
}
// StaffAccount 返回当前启用的工作人员账户。
func StaffAccount(ctx *gin.Context) (models.StaffAccount, bool) {
claims, err := sdkmiddleware.ParseAuth(ctx)
if err != nil {
infra.Response.Error(ctx, err)
return models.StaffAccount{}, false
}
var account models.StaffAccount
if impl.DBService.Where("identity = ? AND status = ?", claims.Identity, 1).First(&account).Error != nil {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
return models.StaffAccount{}, false
}
return account, true
}
// RecordNo 生成便于检索的业务流水号。
func RecordNo(prefix string) string {
return fmt.Sprintf("%s%d", prefix, time.Now().UnixNano())
}

View File

@@ -1,43 +0,0 @@
package common
import (
"testing"
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
)
func TestValidPhone(t *testing.T) {
tests := map[string]bool{
"13800138000": true,
"12800138000": false,
"1380013800": false,
"138001380000": false,
"": false,
}
for phone, want := range tests {
if got := ValidPhone(phone); got != want {
t.Errorf("ValidPhone(%q) = %v, want %v", phone, got, want)
}
}
}
func TestProtectFieldUsesRandomCiphertextAndStableFingerprint(t *testing.T) {
original := config.Spec.Global.FieldEncryptionKey
config.Spec.Global.FieldEncryptionKey = "0123456789abcdef0123456789abcdef"
t.Cleanup(func() { config.Spec.Global.FieldEncryptionKey = original })
firstCipher, firstFingerprint, err := protectField("6222021234567890")
if err != nil {
t.Fatal(err)
}
secondCipher, secondFingerprint, err := protectField("6222021234567890")
if err != nil {
t.Fatal(err)
}
if firstCipher == secondCipher {
t.Fatal("AES-GCM ciphertext must use a fresh nonce")
}
if firstFingerprint != secondFingerprint {
t.Fatal("the same card number must have a stable HMAC fingerprint")
}
}

View File

@@ -1,476 +0,0 @@
package common
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"io"
"strings"
"time"
"git.apinb.com/bsm-sdk/core/errcode"
"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"
"golang.org/x/crypto/hkdf"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type walletOwner struct {
Type, Identity, Phone string
ID uint64
}
func currentOwner(ctx *gin.Context, client string) (walletOwner, bool) {
if client == "user_app" {
account, ok := UserAccount(ctx)
return walletOwner{Type: "user", Identity: account.Identity, Phone: account.Phone, ID: account.ID}, ok
}
account, ok := StaffAccount(ctx)
return walletOwner{Type: "staff", Identity: account.Identity, Phone: account.Phone, ID: account.ID}, ok
}
func ensureWallet(tx *gorm.DB, owner walletOwner) (models.WalletBasic, error) {
var wallet models.WalletBasic
err := tx.Where("owner_type = ? AND owner_identity = ?", owner.Type, owner.Identity).First(&wallet).Error
if err == nil {
return wallet, nil
}
if err != gorm.ErrRecordNotFound {
return wallet, err
}
wallet = models.WalletBasic{
Entity: base.NewEntity(base.StatusEnable), OwnerType: owner.Type, OwnerID: owner.ID, OwnerIdentity: owner.Identity,
}
if err := tx.Create(&wallet).Error; err != nil {
return wallet, err
}
return wallet, nil
}
// GetWallet 延迟创建并返回当前主体钱包。
func GetWallet(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
owner, ok := currentOwner(ctx, client)
if !ok {
return
}
wallet, err := ensureWallet(impl.DBService, owner)
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{
"identity": wallet.Identity, "balance": wallet.Balance,
"withdrawal_balance": wallet.WithdrawalBalance, "payment_password_set": wallet.PayPasswordHash != "",
})
}
}
// SetPaymentPassword 设置或重置六位数字支付密码。
func SetPaymentPassword(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
owner, ok := currentOwner(ctx, client)
if !ok {
return
}
var request struct {
NewPassword string `json:"new_password" binding:"required,len=6,numeric"`
CurrentPassword string `json:"current_password"`
Code string `json:"code"`
RequestIdentity string `json:"request_identity"`
}
if ctx.ShouldBindJSON(&request) != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
wallet, err := ensureWallet(impl.DBService, owner)
if err != nil {
infra.Response.Error(ctx, err)
return
}
valid := wallet.PayPasswordHash == "" && VerifyCode(client, owner.Phone, "set_payment_password", request.RequestIdentity, request.Code)
if wallet.PayPasswordHash != "" {
valid = bcrypt.CompareHashAndPassword([]byte(wallet.PayPasswordHash), []byte(request.CurrentPassword)) == nil ||
VerifyCode(client, owner.Phone, "reset_payment_password", request.RequestIdentity, request.Code)
}
if !valid {
infra.Response.Error(ctx, errcode.ErrPassword)
return
}
hash, _ := bcrypt.GenerateFromPassword([]byte(request.NewPassword), bcrypt.DefaultCost)
if err := impl.DBService.Model(&wallet).Update("pay_password_hash", string(hash)).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"changed": true})
}
}
// CreateRecharge 创建待支付充值订单,不直接增加余额。
func CreateRecharge(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
owner, ok := currentOwner(ctx, client)
if !ok {
return
}
var request struct {
Amount int64 `json:"amount" binding:"required,gt=0"`
Channel string `json:"channel" binding:"required,oneof=mock wechat alipay"`
RequestNo string `json:"request_no" binding:"required"`
}
if ctx.ShouldBindJSON(&request) != nil || request.Amount > config.Spec.Global.ManualRechargeMaxAmount ||
(request.Channel == "mock" && !config.Spec.Global.MockPaymentEnabled) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
wallet, err := ensureWallet(impl.DBService, owner)
if err != nil {
infra.Response.Error(ctx, err)
return
}
order := models.WalletRechargeOrder{
Entity: base.NewEntity(base.StatusEnable), RechargeStatus: 10, WalletBasicID: wallet.ID,
RechargeNo: RecordNo("RC"), RequestNo: request.RequestNo, Amount: request.Amount,
Channel: request.Channel, OwnerType: owner.Type, OwnerIdentity: owner.Identity,
}
if err := impl.DBService.Create(&order).Error; err != nil {
var existing models.WalletRechargeOrder
if impl.DBService.Where("request_no = ? AND owner_identity = ?", request.RequestNo, owner.Identity).First(&existing).Error != nil {
infra.Response.Error(ctx, err)
return
}
order = existing
}
infra.Response.Success(ctx, base.ResourceResponse(order))
}
}
// ConfirmMockRecharge 模拟支付回调,生产关闭;事务内只入账一次。
func ConfirmMockRecharge(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
if !config.Spec.Global.MockPaymentEnabled {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
return
}
owner, ok := currentOwner(ctx, client)
if !ok {
return
}
var response models.WalletRechargeOrder
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ? AND owner_identity = ?", ctx.Param("identity"), owner.Identity).First(&response).Error; err != nil {
return err
}
if response.RechargeStatus == 23 {
return nil
}
if response.RechargeStatus != 10 || response.Channel != "mock" {
return gorm.ErrInvalidData
}
var wallet models.WalletBasic
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&wallet, response.WalletBasicID).Error; err != nil {
return err
}
wallet.Balance += response.Amount
if err := tx.Model(&wallet).Update("balance", wallet.Balance).Error; err != nil {
return err
}
now := time.Now()
if err := tx.Model(&response).Updates(map[string]any{"recharge_status": 23, "completed_at": &now}).Error; err != nil {
return err
}
date := now.In(time.Local)
return tx.Create(&models.WalletRecord{
Entity: base.NewEntity(base.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,
Ymd: int32(date.Year()*10000 + int(date.Month())*100 + date.Day()), Ym: int32(date.Year()*100 + int(date.Month())),
}).Error
})
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"confirmed": true})
}
}
// ListWalletRecords 返回当前钱包不可变流水。
func ListWalletRecords(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
owner, ok := currentOwner(ctx, client)
if !ok {
return
}
wallet, err := ensureWallet(impl.DBService, owner)
if err != nil {
infra.Response.Error(ctx, err)
return
}
var list []models.WalletRecord
if err := impl.DBService.Where("wallet_basic_id = ?", wallet.ID).Order("created_at desc").Find(&list).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, base.ResourceResponse(list))
}
}
// ListBanks 仅返回银行卡掩码和非敏感字段。
func ListBanks(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
owner, ok := currentOwner(ctx, client)
if !ok {
return
}
wallet, err := ensureWallet(impl.DBService, owner)
if err != nil {
infra.Response.Error(ctx, err)
return
}
var banks []models.WalletBank
if err := impl.DBService.Where("wallet_basic_id = ? AND status <> ?", wallet.ID, base.StatusArchived).Find(&banks).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
list := make([]gin.H, 0, len(banks))
for _, bank := range banks {
list = append(list, gin.H{"identity": bank.Identity, "card_no_masked": "**** **** **** " + bank.CardNoLast4, "bank_name": bank.BankName, "card_owner": bank.CardOwner, "bank_type": bank.BankType})
}
infra.Response.Success(ctx, list)
}
}
// BindBank 加密保存银行卡;支付渠道绑定标识在首期保持为空。
func BindBank(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
owner, ok := currentOwner(ctx, client)
if !ok {
return
}
var request struct {
CardNo string `json:"card_no" binding:"required,min=12,max=32"`
BankName string `json:"bank_name" binding:"required,max=128"`
CardOwner string `json:"card_owner" binding:"required,max=128"`
IDCard string `json:"id_card" binding:"required"`
Phone string `json:"phone" binding:"required"`
BankType string `json:"bank_type"`
Bank string `json:"bank"`
PaymentPassword string `json:"payment_password"`
Code string `json:"code"`
RequestIdentity string `json:"request_identity"`
}
if ctx.ShouldBindJSON(&request) != nil || !ValidPhone(request.Phone) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
wallet, err := ensureWallet(impl.DBService, owner)
if err != nil {
infra.Response.Error(ctx, err)
return
}
if !walletProof(wallet, client, owner.Phone, "bind_bank", request.PaymentPassword, request.RequestIdentity, request.Code) {
infra.Response.Error(ctx, errcode.ErrPassword)
return
}
cardCipher, fingerprint, err := protectField(request.CardNo)
if err != nil {
infra.Response.Error(ctx, err)
return
}
idCipher, _, _ := protectField(request.IDCard)
phoneCipher, _, _ := protectField(request.Phone)
bank := models.WalletBank{
Entity: base.NewEntity(base.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,
}
if err := impl.DBService.Create(&bank).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"identity": bank.Identity, "card_no_masked": "**** **** **** " + bank.CardNoLast4})
}
}
// UnbindBank 归档银行卡;存在待处理提现时拒绝。
func UnbindBank(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
owner, ok := currentOwner(ctx, client)
if !ok {
return
}
var request struct {
PaymentPassword string `json:"payment_password"`
Code string `json:"code"`
RequestIdentity string `json:"request_identity"`
}
if ctx.ShouldBindJSON(&request) != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
wallet, err := ensureWallet(impl.DBService, owner)
if err != nil || !walletProof(wallet, client, owner.Phone, "unbind_bank", request.PaymentPassword, request.RequestIdentity, request.Code) {
infra.Response.Error(ctx, errcode.ErrPassword)
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 {
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
return
}
var count int64
impl.DBService.Model(&models.WalletApplyCash{}).Where("wallet_bank_id = ? AND apply_status IN ?", bank.ID, []int{10, 18}).Count(&count)
if count > 0 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err := impl.DBService.Model(&bank).Update("status", base.StatusArchived).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"archived": true})
}
}
// CreateWithdrawal 创建提现申请并预扣可提现余额。
func CreateWithdrawal(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
owner, ok := currentOwner(ctx, client)
if !ok {
return
}
var request struct {
BankIdentity string `json:"bank_identity" binding:"required"`
Amount int64 `json:"amount" binding:"required,gt=0"`
RequestNo string `json:"request_no" binding:"required"`
PaymentPassword string `json:"payment_password" binding:"required"`
Remark string `json:"remark"`
}
if ctx.ShouldBindJSON(&request) != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
wallet, err := ensureWallet(impl.DBService, owner)
if err != nil || !VerifyPaymentPassword(owner.Identity, wallet, request.PaymentPassword) {
infra.Response.Error(ctx, errcode.ErrPassword)
return
}
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 {
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
})
if err != nil {
var existing models.WalletApplyCash
if impl.DBService.Where("request_no = ? AND wallet_basic_id = ?", request.RequestNo, wallet.ID).First(&existing).Error != nil {
infra.Response.Error(ctx, err)
return
}
apply = existing
}
infra.Response.Success(ctx, base.ResourceResponse(apply))
}
}
// ListWithdrawals 返回当前钱包提现申请。
func ListWithdrawals(client string) gin.HandlerFunc {
return func(ctx *gin.Context) {
owner, ok := currentOwner(ctx, client)
if !ok {
return
}
wallet, err := ensureWallet(impl.DBService, owner)
if err != nil {
infra.Response.Error(ctx, err)
return
}
var list []models.WalletApplyCash
if err := impl.DBService.Where("wallet_basic_id = ?", wallet.ID).Order("created_at desc").Find(&list).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, base.ResourceResponse(list))
}
}
func walletProof(wallet models.WalletBasic, client, phone, purpose, password, requestIdentity, code string) bool {
return wallet.PayPasswordHash != "" && bcrypt.CompareHashAndPassword([]byte(wallet.PayPasswordHash), []byte(password)) == nil ||
VerifyCode(client, phone, purpose, requestIdentity, code)
}
// VerifyPaymentPassword 校验支付密码,并在 Redis 中累计失败次数、短时锁定。
func VerifyPaymentPassword(ownerIdentity string, wallet models.WalletBasic, password string) bool {
lockKey := impl.RedisService.BuildKey("payment-password-lock", ownerIdentity)
var locked bool
if impl.RedisService.Get(lockKey, &locked) == nil && locked {
return false
}
if wallet.PayPasswordHash != "" && bcrypt.CompareHashAndPassword([]byte(wallet.PayPasswordHash), []byte(password)) == nil {
_ = impl.RedisService.Delete(impl.RedisService.BuildKey("payment-password-failures", ownerIdentity))
return true
}
failureKey := impl.RedisService.BuildKey("payment-password-failures", ownerIdentity)
var failures int
_ = impl.RedisService.Get(failureKey, &failures)
failures++
_ = impl.RedisService.Set(failureKey, failures, 15*time.Minute)
if failures >= 5 {
_ = impl.RedisService.Set(lockKey, true, 15*time.Minute)
}
return false
}
func protectField(value string) (string, string, error) {
reader := hkdf.New(sha256.New, []byte(config.Spec.Global.FieldEncryptionKey), nil, []byte("heqi-wallet-field-v1"))
key := make([]byte, 64)
if _, err := io.ReadFull(reader, key); err != nil {
return "", "", err
}
block, err := aes.NewCipher(key[:32])
if err != nil {
return "", "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return "", "", err
}
sealed := gcm.Seal(nil, nonce, []byte(strings.TrimSpace(value)), nil)
mac := hmac.New(sha256.New, key[32:])
_, _ = mac.Write([]byte(strings.TrimSpace(value)))
return base64.RawStdEncoding.EncodeToString(append(nonce, sealed...)), hex.EncodeToString(mac.Sum(nil)), nil
}

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
}

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

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

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"
)
@@ -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,
}

View File

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

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