Files
platforms/backend/api/internal/logic/delivery/user.go
2026-08-31 20:49:51 +08:00

388 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 功能描述:实现配送点范围内的用户、服务关系和收货地址管理。
// 版本v1.2.0。
package delivery
import (
"errors"
"strings"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/upload"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgconn"
"gorm.io/gorm"
)
const deliveryUserUsernameConstraint = "idx_user_account_username"
var errDeliveryUserUsernameExists = errors.New("用户名已存在,请更换用户名")
// deliveryUserCreateError 仅转换配送点新建用户时的用户名唯一约束冲突。
// 参数err 为用户和服务关系事务返回的错误。
// 返回值:用户名冲突返回中文提示,其他错误保持原样。
func deliveryUserCreateError(err error) error {
var postgresError *pgconn.PgError
if errors.As(err, &postgresError) && postgresError.Code == "23505" && postgresError.ConstraintName == deliveryUserUsernameConstraint {
return errDeliveryUserUsernameExists
}
return err
}
func userQuery(pointID uint64) *gorm.DB {
return common.ActiveRecords(db().Model(&models.UserAccount{})).
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_account.id AND user_service_relation.status <> ?",
common.StatusArchived).
Where("user_service_relation.delivery_basic_id = ?", pointID)
}
func scopedUser(ctx *gin.Context, identity string, pointID uint64) (models.UserAccount, models.UserServiceRelation, bool) {
var user models.UserAccount
if err := userQuery(pointID).Where("user_account.identity = ?", identity).First(&user).Error; err != nil {
common.RespondRecordError(ctx, err)
return user, models.UserServiceRelation{}, false
}
var relation models.UserServiceRelation
if err := common.ActiveRecords(db()).Where("user_account_id = ? AND delivery_basic_id = ?", user.ID, pointID).First(&relation).Error; err != nil {
common.RespondRecordError(ctx, err)
return user, relation, false
}
return user, relation, true
}
func ListUser(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if ok {
listScoped(ctx, &models.UserAccount{}, userQuery(point.ID), "user_account.created_at desc")
}
}
func GetUser(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
user, relation, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
if !ok {
return
}
var addresses []models.UserAddress
if err := common.ActiveRecords(db()).Where("user_account_id = ?", user.ID).Order("is_default desc, created_at desc").Find(&addresses).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
response, _ := common.PublicResourceResponse(gin.H{"user": user, "relation": relation, "addresses": addresses})
infra.Response.Success(ctx, response)
}
// GetUserAvatar 返回当前配送点存在有效服务关系的用户受保护头像。
func GetUserAvatar(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
user, _, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
if !ok {
return
}
upload.ServeAvatar(ctx, user.Avatar)
}
type userRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name" binding:"required,max=64"`
Phone string `json:"phone" binding:"max=32"`
Avatar *string `json:"avatar" binding:"omitempty,max=512"`
RealName string `json:"real_name" binding:"max=64"`
}
func CreateUser(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
var request userRequest
if err := ctx.ShouldBindJSON(&request); err != nil || request.Username == "" || !common.IsValidAccountPassword(request.Password) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
hash, err := common.PasswordHash(request.Password)
if err != nil {
infra.Response.Error(ctx, err)
return
}
avatar := ""
if request.Avatar != nil {
avatar = *request.Avatar
}
user := models.UserAccount{
Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash,
Name: request.Name, Phone: request.Phone, Avatar: avatar, RealName: request.RealName,
}
relation := models.UserServiceRelation{
Entity: common.NewEntity(common.StatusEnable), GasBasicID: point.GasBasicID, DeliveryBasicID: point.ID,
}
if err := db().Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err
}
relation.UserAccountID = user.ID
return tx.Create(&relation).Error
}); err != nil {
infra.Response.Error(ctx, deliveryUserCreateError(err))
return
}
common.RespondCreatedResource(ctx, user)
}
func UpdateUser(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
user, _, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
if !ok {
return
}
var request userRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
values := map[string]any{"name": request.Name, "phone": request.Phone, "real_name": request.RealName}
// 未选择新头像时不提交 avatar避免编辑基础资料误清空现有头像。
if request.Avatar != nil {
values["avatar"] = *request.Avatar
}
if err := db().Model(&user).Updates(values).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"updated": true})
}
func ResetUserPassword(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
user, _, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
if !ok {
return
}
var request struct {
Password string `json:"password" binding:"required"`
}
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsValidAccountPassword(request.Password) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
hash, _ := common.PasswordHash(request.Password)
if err := db().Model(&user).Update("password_hash", hash).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"changed": true})
}
func UpdateUserStatus(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
if _, _, ok := scopedUser(ctx, ctx.Param("identity"), point.ID); ok {
common.UpdateRecordStatus(ctx, &models.UserAccount{})
}
}
func ArchiveUser(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
user, relation, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
if !ok {
return
}
var blocking int64
if err := db().Model(&models.GasorderBasic{}).Where("user_account_id = ? AND order_status NOT IN ?",
user.ID, []int{common.StatusCompleted, common.StatusCancelled}).Count(&blocking).Error; err != nil || blocking > 0 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err := db().Model(&models.CsTicket{}).Where("user_account_id = ? AND status <> ? AND ticket_status = ?",
user.ID, common.StatusArchived, common.StatusOpen).Count(&blocking).Error; err != nil || blocking > 0 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var wallet models.WalletBasic
err := db().Where("owner_type = ? AND owner_id = ? AND status <> ?", "user", user.ID, common.StatusArchived).First(&wallet).Error
if err == nil && (wallet.Balance > 0 || wallet.WithdrawalBalance > 0) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err == nil {
if cashErr := db().Model(&models.WalletApplyCash{}).Where("wallet_basic_id = ? AND apply_status = ? AND status <> ?",
wallet.ID, common.StatusPending, common.StatusArchived).Count(&blocking).Error; cashErr != nil || blocking > 0 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
infra.Response.Error(ctx, err)
return
}
if err := db().Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&relation).Update("status", common.StatusArchived).Error; err != nil {
return err
}
if err := tx.Model(&models.UserAddress{}).Where("user_account_id = ?", user.ID).Update("status", common.StatusArchived).Error; err != nil {
return err
}
return tx.Model(&user).Update("status", common.StatusArchived).Error
}); err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"archived": true})
}
func addressQuery(pointID uint64) *gorm.DB {
return addressQueryWithDatabase(db(), pointID)
}
// addressQueryWithDatabase 构造配送点有效服务用户的地址范围,便于独立验证查询约束。
func addressQueryWithDatabase(database *gorm.DB, pointID uint64) *gorm.DB {
return common.ActiveRecords(database.Model(&models.UserAddress{})).
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_address.user_account_id AND user_service_relation.status <> ?",
common.StatusArchived).Where("user_service_relation.delivery_basic_id = ?", pointID)
}
// addressQueryForContract 将当前配送点地址进一步限定为合同签约用户。
func addressQueryForContract(query *gorm.DB, userAccountID uint64) *gorm.DB {
return query.Where("user_address.user_account_id = ?", userAccountID)
}
func ListAddress(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
query := addressQuery(point.ID)
if contractIdentity := strings.TrimSpace(ctx.Query("gasorder_contract_identity")); contractIdentity != "" {
contract, valid := scopedContract(ctx, contractIdentity, point.ID)
if !valid {
return
}
query = addressQueryForContract(query, contract.UserAccountID)
}
listScoped(ctx, &models.UserAddress{}, query, "user_address.is_default desc, user_address.created_at desc")
}
func GetAddress(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
var address models.UserAddress
respondRecord(ctx, addressQuery(point.ID).Where("user_address.identity = ?", ctx.Param("identity")), &address)
}
type addressRequest struct {
UserIdentity string `json:"user_account_identity" binding:"required"`
Address string `json:"address" binding:"required,max=255"`
Longitude string `json:"longitude" binding:"max=32"`
Latitude string `json:"latitude" binding:"max=32"`
IsDefault bool `json:"is_default"`
}
func CreateAddress(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
var request addressRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
user, _, ok := scopedUser(ctx, request.UserIdentity, point.ID)
if !ok {
return
}
address := models.UserAddress{
Entity: common.NewEntity(common.StatusEnable), UserAccountID: user.ID, Address: request.Address,
Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault,
}
if err := db().Transaction(func(tx *gorm.DB) error {
if request.IsDefault {
if err := tx.Model(&models.UserAddress{}).Where("user_account_id = ?", user.ID).Update("is_default", false).Error; err != nil {
return err
}
}
return tx.Create(&address).Error
}); err != nil {
infra.Response.Error(ctx, err)
return
}
common.RespondCreatedResource(ctx, address)
}
func UpdateAddress(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
var existing models.UserAddress
if err := addressQuery(point.ID).Where("user_address.identity = ?", ctx.Param("identity")).First(&existing).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
var request addressRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
user, _, ok := scopedUser(ctx, request.UserIdentity, point.ID)
if !ok || user.ID != existing.UserAccountID {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err := db().Transaction(func(tx *gorm.DB) error {
if request.IsDefault {
if err := tx.Model(&models.UserAddress{}).Where("user_account_id = ? AND id <> ?", user.ID, existing.ID).
Update("is_default", false).Error; err != nil {
return err
}
}
return tx.Model(&existing).Updates(map[string]any{
"address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude, "is_default": request.IsDefault,
}).Error
}); err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"updated": true})
}
func UpdateAddressStatus(ctx *gin.Context) { mutateAddress(ctx, false) }
func ArchiveAddress(ctx *gin.Context) { mutateAddress(ctx, true) }
func mutateAddress(ctx *gin.Context, archive bool) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
var count int64
if err := addressQuery(point.ID).Where("user_address.identity = ?", ctx.Param("identity")).Count(&count).Error; err != nil || count != 1 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if archive {
common.ArchiveRecord(ctx, &models.UserAddress{})
} else {
common.UpdateRecordStatus(ctx, &models.UserAddress{})
}
}