// 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" common "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" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "gorm.io/gorm/logger" ) 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"` Consents []loginConsent `json:"consents" binding:"max=100"` // 新客户端提交本次明确同意的内容版本;旧客户端兼容省略。 } // Login 支持密码和一次性验证码两种登录模式。 func Login(ctx *gin.Context) { var request loginRequest 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), 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 = common.VerifyCode("user_app", account.Phone, "login", request.RequestIdentity, request.Code) } if !valid { infra.Response.Error(ctx, errcode.ErrPassword) return } if err := recordLoginConsents(account.ID, account.Identity, request.Consents); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } accessToken, err := common.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 || !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 := common.PasswordHash(request.Password) if err != nil { infra.Response.Error(ctx, err) return } account := models.UserAccount{ 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 { if err := tx.Create(&account).Error; err != nil { return err } address := models.UserAddress{ 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 { 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, 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, common.StatusEnable).First(&delivery).Error; err != nil { return err } deliveryID = delivery.ID } return tx.Create(&models.UserServiceRelation{ Entity: common.NewEntity(common.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 := common.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}) } // Avatar 返回当前用户自己的受保护头像二进制内容。 func Avatar(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } upload.ServeAvatar(ctx, account.Avatar) } // UpdateProfile 只允许修改非认证资料。 func UpdateProfile(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } var request struct { Name string `json:"name" binding:"required,max=64"` Avatar *string `json:"avatar" binding:"omitempty,max=512"` } if ctx.ShouldBindJSON(&request) != nil || strings.TrimSpace(request.Name) == "" { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } updates := map[string]any{"name": strings.TrimSpace(request.Name)} if request.Avatar != nil { // 兼容保留旧头像或显式清空;替换必须引用当前用户刚上传的受控文件。 if *request.Avatar != "" && *request.Avatar != account.Avatar && !upload.OwnsAvatar("user_app", account.Identity, *request.Avatar) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } updates["avatar"] = *request.Avatar } if err := impl.DBService.Model(&account).Updates(updates).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 := common.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 || !common.IsValidAccountPassword(request.NewPassword) || bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.CurrentPassword)) != nil { infra.Response.Error(ctx, errcode.ErrPassword) return } hash, err := common.PasswordHash(request.NewPassword) if err != nil { infra.Response.Error(ctx, err) return } // 仅替换刚验证过的密码版本,避免并发改密覆盖已经生效的新密码。 // 密码散列不进入开发环境SQL日志。 result := impl.DBService.Session(&gorm.Session{Logger: logger.Default.LogMode(logger.Silent)}).Model(&account).Where("password_hash = ?", account.PasswordHash).Update("password_hash", hash) if result.Error != nil { infra.Response.Error(ctx, result.Error) return } if result.RowsAffected != 1 { infra.Response.Error(ctx, errcode.ErrPassword) 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 || !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 := 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), common.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}) }