174 lines
6.6 KiB
Go
174 lines
6.6 KiB
Go
|
|
// 功能描述:用户地址新增、编辑、归档及默认切换,账户行锁保护并发写入。
|
|||
|
|
// 版本:1.0.0。
|
|||
|
|
package user
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"errors"
|
|||
|
|
"math"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"git.apinb.com/bsm-sdk/core/errcode"
|
|||
|
|
"git.apinb.com/bsm-sdk/core/infra"
|
|||
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
|||
|
|
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
|||
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
"gorm.io/gorm/clause"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// addressRequest 保留旧新增字段,联系人省略时从本账户补齐。
|
|||
|
|
type addressRequest struct {
|
|||
|
|
Address string `json:"address" binding:"required,max=255"`
|
|||
|
|
ContactName *string `json:"contact_name" binding:"omitempty,max=64"`
|
|||
|
|
ContactPhone *string `json:"contact_phone" binding:"omitempty,max=32"`
|
|||
|
|
Longitude string `json:"longitude" binding:"max=32"`
|
|||
|
|
Latitude string `json:"latitude" binding:"max=32"`
|
|||
|
|
IsDefault bool `json:"is_default"`
|
|||
|
|
RequestNo string `json:"request_no" binding:"max=64"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// validAddressCoordinates 接受手动地址的空坐标;有坐标时必须成对且为合法经纬度。
|
|||
|
|
func validAddressCoordinates(longitude, latitude string) bool {
|
|||
|
|
if longitude == "" && latitude == "" {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
lon, e1 := strconv.ParseFloat(longitude, 64)
|
|||
|
|
lat, e2 := strconv.ParseFloat(latitude, 64)
|
|||
|
|
return e1 == nil && e2 == nil && !math.IsNaN(lon) && !math.IsNaN(lat) && lon >= -180 && lon <= 180 && lat >= -90 && lat <= 90
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// lockAddressOwner 按账户串行处理默认地址和幂等请求,避免首次新增的并发竞态。
|
|||
|
|
func lockAddressOwner(tx *gorm.DB, id uint64) error {
|
|||
|
|
return tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").First(&models.UserAccount{}, id).Error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// clearAddressDefault 在同一事务取消旧默认,保留现有唯一索引约束。
|
|||
|
|
func clearAddressDefault(tx *gorm.DB, userID uint64) error {
|
|||
|
|
return tx.Model(&models.UserAddress{}).Where("user_account_id = ? AND is_default = ?", userID, true).Update("is_default", false).Error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SaveAddress 新增地址,带 request_no 的重试返回原标识,不重复创建。
|
|||
|
|
func SaveAddress(ctx *gin.Context) { writeAddress(ctx, false) }
|
|||
|
|
|
|||
|
|
// UpdateAddress 仅更新本人地址,旧订单使用已有快照,不随地址修改。
|
|||
|
|
func UpdateAddress(ctx *gin.Context) { writeAddress(ctx, true) }
|
|||
|
|
|
|||
|
|
// writeAddress 校验请求后在事务中持久化,失败时保持原地址和默认设置。
|
|||
|
|
func writeAddress(ctx *gin.Context, updating bool) {
|
|||
|
|
account, ok := common.UserAccount(ctx)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var request addressRequest
|
|||
|
|
if ctx.ShouldBindJSON(&request) != nil {
|
|||
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
request.Address = strings.TrimSpace(request.Address)
|
|||
|
|
request.Longitude = strings.TrimSpace(request.Longitude)
|
|||
|
|
request.Latitude = strings.TrimSpace(request.Latitude)
|
|||
|
|
if request.Address == "" || !validAddressCoordinates(request.Longitude, request.Latitude) {
|
|||
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var address models.UserAddress
|
|||
|
|
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
|||
|
|
if err := lockAddressOwner(tx, account.ID); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if updating {
|
|||
|
|
if err := tx.Where("identity = ? AND user_account_id = ? AND status <> ?", ctx.Param("identity"), account.ID, common.StatusArchived).First(&address).Error; err != nil {
|
|||
|
|
return errcode.ErrRecordNotFound
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
address = models.UserAddress{Entity: common.NewEntity(common.StatusEnable), UserAccountID: account.ID, RequestNo: request.RequestNo}
|
|||
|
|
}
|
|||
|
|
name, phone := address.ContactName, address.ContactPhone
|
|||
|
|
if name == "" {
|
|||
|
|
name = account.Name
|
|||
|
|
}
|
|||
|
|
if phone == "" {
|
|||
|
|
phone = account.Phone
|
|||
|
|
}
|
|||
|
|
if request.ContactName != nil {
|
|||
|
|
name = strings.TrimSpace(*request.ContactName)
|
|||
|
|
}
|
|||
|
|
if request.ContactPhone != nil {
|
|||
|
|
phone = strings.TrimSpace(*request.ContactPhone)
|
|||
|
|
}
|
|||
|
|
if (request.ContactName != nil && name == "") || !common.ValidPhone(phone) {
|
|||
|
|
return errcode.ErrInvalidArgument
|
|||
|
|
}
|
|||
|
|
if !updating && request.RequestNo != "" {
|
|||
|
|
var previous models.UserAddress
|
|||
|
|
err := tx.Where("user_account_id = ? AND request_no = ?", account.ID, request.RequestNo).First(&previous).Error
|
|||
|
|
if err == nil {
|
|||
|
|
if previous.Status == common.StatusArchived || previous.Address != request.Address || previous.ContactName != name || previous.ContactPhone != phone || previous.Longitude != request.Longitude || previous.Latitude != request.Latitude {
|
|||
|
|
return errcode.ErrInvalidArgument
|
|||
|
|
}
|
|||
|
|
address = previous
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if request.IsDefault {
|
|||
|
|
if err := clearAddressDefault(tx, account.ID); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
address.Address, address.ContactName, address.ContactPhone = request.Address, name, phone
|
|||
|
|
address.Longitude, address.Latitude, address.IsDefault = request.Longitude, request.Latitude, request.IsDefault
|
|||
|
|
if updating {
|
|||
|
|
return tx.Model(&address).Select("address", "contact_name", "contact_phone", "longitude", "latitude", "is_default").Updates(&address).Error
|
|||
|
|
}
|
|||
|
|
return tx.Create(&address).Error
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
infra.Response.Success(ctx, gin.H{"identity": address.Identity})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetDefaultAddress 设置本人有效地址为默认,不接受请求体中的用户或地址归属。
|
|||
|
|
func SetDefaultAddress(ctx *gin.Context) { changeAddressState(ctx, false) }
|
|||
|
|
|
|||
|
|
// DeleteAddress 归档本人地址并取消默认;重复删除成功,历史订单引用保留。
|
|||
|
|
func DeleteAddress(ctx *gin.Context) { changeAddressState(ctx, true) }
|
|||
|
|
|
|||
|
|
// changeAddressState 所有权校验先于默认切换,跨账户请求不能影响本人的默认设置。
|
|||
|
|
func changeAddressState(ctx *gin.Context, deleting bool) {
|
|||
|
|
account, ok := common.UserAccount(ctx)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
|||
|
|
if err := lockAddressOwner(tx, account.ID); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
var address models.UserAddress
|
|||
|
|
if err := tx.Where("identity = ? AND user_account_id = ?", ctx.Param("identity"), account.ID).First(&address).Error; err != nil {
|
|||
|
|
return errcode.ErrRecordNotFound
|
|||
|
|
}
|
|||
|
|
if deleting {
|
|||
|
|
return tx.Model(&address).Updates(map[string]any{"status": common.StatusArchived, "is_default": false}).Error
|
|||
|
|
}
|
|||
|
|
if address.Status == common.StatusArchived {
|
|||
|
|
return errcode.ErrRecordNotFound
|
|||
|
|
}
|
|||
|
|
if err := clearAddressDefault(tx, account.ID); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return tx.Model(&address).Update("is_default", true).Error
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
infra.Response.Success(ctx, gin.H{"identity": ctx.Param("identity")})
|
|||
|
|
}
|