// Package user 实现用户 App 的服务端业务接口。 package user import ( "strings" "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" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) type loginRequest struct { Phone string `json:"phone" binding:"required"` Mode string `json:"mode" binding:"required,oneof=password verification_code"` Password string `json:"password"` Code string `json:"code"` RequestIdentity string `json:"request_identity"` } // Login 支持密码和一次性验证码两种登录模式。 func Login(ctx *gin.Context) { var request loginRequest if ctx.ShouldBindJSON(&request) != nil || !clientcommon.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 { 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) } if !valid { infra.Response.Error(ctx, errcode.ErrPassword) return } accessToken, err := clientcommon.IssueToken(account.Identity, "user_app", "user", map[string]string{"phone": account.Phone}) if err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"access_token": accessToken, "token_type": "JWT", "identity": account.Identity}) } // Register 创建普通或邀请用户;邀请归属和默认地址在同一事务内完成。 func Register(ctx *gin.Context) { var request struct { Phone string `json:"phone" binding:"required"` Password string `json:"password" binding:"required"` Name string `json:"name" binding:"required,max=64"` Address string `json:"address" binding:"required,max=255"` Longitude string `json:"longitude"` Latitude string `json:"latitude"` GasIdentity string `json:"gas_identity"` DeliveryIdentity string `json:"delivery_identity"` 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) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } hash, err := base.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), Phone: strings.TrimSpace(request.Phone), PasswordHash: hash, Name: strings.TrimSpace(request.Name), } err = impl.DBService.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&account).Error; err != nil { return err } address := models.UserAddress{ Entity: base.NewEntity(base.StatusEnable), UserAccountID: account.ID, Address: request.Address, Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: true, } if err := tx.Create(&address).Error; err != nil { return err } if request.GasIdentity == "" { if request.DeliveryIdentity != "" { return gorm.ErrInvalidData } return nil } var gas models.GasBasic if err := tx.Where("identity = ? AND status = ?", request.GasIdentity, base.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 { return err } deliveryID = delivery.ID } return tx.Create(&models.UserServiceRelation{ Entity: base.NewEntity(base.StatusEnable), UserAccountID: account.ID, GasBasicID: gas.ID, DeliveryBasicID: deliveryID, }).Error }) if err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"identity": account.Identity}) } // Profile 返回当前用户的脱敏资料。 func Profile(ctx *gin.Context) { account, ok := clientcommon.UserAccount(ctx) if !ok { return } infra.Response.Success(ctx, gin.H{"identity": account.Identity, "name": account.Name, "phone": account.Phone, "avatar": account.Avatar, "real_name": account.RealName}) } // UpdateProfile 只允许修改非认证资料。 func UpdateProfile(ctx *gin.Context) { account, ok := clientcommon.UserAccount(ctx) if !ok { return } var request struct { Name string `json:"name" binding:"required,max=64"` Avatar string `json:"avatar" binding:"max=512"` } if ctx.ShouldBindJSON(&request) != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } if err := impl.DBService.Model(&account).Updates(map[string]any{"name": strings.TrimSpace(request.Name), "avatar": request.Avatar}).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"updated": true}) } // ChangePassword 使用当前密码修改登录密码。 func ChangePassword(ctx *gin.Context) { account, ok := clientcommon.UserAccount(ctx) if !ok { return } var request struct { CurrentPassword string `json:"current_password" binding:"required"` NewPassword string `json:"new_password" binding:"required"` } if ctx.ShouldBindJSON(&request) != nil || !base.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) if err != nil { infra.Response.Error(ctx, err) return } if err := impl.DBService.Model(&account).Update("password_hash", hash).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"changed": true}) } // ResetPassword 使用限定用途的手机号验证码重置登录密码。 func ResetPassword(ctx *gin.Context) { var request struct { Phone string `json:"phone" binding:"required"` NewPassword string `json:"new_password" binding:"required"` 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) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } hash, err := base.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). Update("password_hash", hash) if result.Error != nil { infra.Response.Error(ctx, result.Error) return } if result.RowsAffected != 1 { infra.Response.Error(ctx, errcode.ErrRecordNotFound) return } infra.Response.Success(ctx, gin.H{"changed": true}) }