2026-07-30 18:04:34 +08:00
// 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"
2026-07-31 09:54:17 +08:00
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
2026-07-30 18:04:34 +08:00
"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
2026-07-31 09:54:17 +08:00
if ctx . ShouldBindJSON ( & request ) != nil || ! common . ValidPhone ( request . Phone ) {
2026-07-30 18:04:34 +08:00
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
var account models . UserAccount
2026-07-31 09:54:17 +08:00
if impl . DBService . Where ( "phone = ? AND status = ?" , strings . TrimSpace ( request . Phone ) , common . StatusEnable ) . First ( & account ) . Error != nil {
2026-07-30 18:04:34 +08:00
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" {
2026-07-31 09:54:17 +08:00
valid = common . VerifyCode ( "user_app" , account . Phone , "login" , request . RequestIdentity , request . Code )
2026-07-30 18:04:34 +08:00
}
if ! valid {
infra . Response . Error ( ctx , errcode . ErrPassword )
return
}
2026-07-31 09:54:17 +08:00
accessToken , err := common . IssueToken ( account . Identity , "user_app" , "user" , map [ string ] string { "phone" : account . Phone } )
2026-07-30 18:04:34 +08:00
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" `
}
2026-07-31 09:54:17 +08:00
if ctx . ShouldBindJSON ( & request ) != nil || ! common . ValidPhone ( request . Phone ) ||
! common . IsValidAccountPassword ( request . Password ) ||
! common . VerifyCode ( "user_app" , request . Phone , "register" , request . RequestIdentity , request . Code ) {
2026-07-30 18:04:34 +08:00
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-07-31 09:54:17 +08:00
hash , err := common . PasswordHash ( request . Password )
2026-07-30 18:04:34 +08:00
if err != nil {
infra . Response . Error ( ctx , err )
return
}
account := models . UserAccount {
2026-07-31 09:54:17 +08:00
Entity : common . NewEntity ( common . StatusEnable ) , Username : strings . TrimSpace ( request . Phone ) ,
2026-07-30 18:04:34 +08:00
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 {
2026-07-31 09:54:17 +08:00
Entity : common . NewEntity ( common . StatusEnable ) , UserAccountID : account . ID , Address : request . Address ,
2026-07-30 18:04:34 +08:00
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
2026-07-31 09:54:17 +08:00
if err := tx . Where ( "identity = ? AND status = ?" , request . GasIdentity , common . StatusEnable ) . First ( & gas ) . Error ; err != nil {
2026-07-30 18:04:34 +08:00
return err
}
var deliveryID uint64
if request . DeliveryIdentity != "" {
var delivery models . DeliveryBasic
2026-07-31 09:54:17 +08:00
if err := tx . Where ( "identity = ? AND gas_basic_id = ? AND status = ?" , request . DeliveryIdentity , gas . ID , common . StatusEnable ) . First ( & delivery ) . Error ; err != nil {
2026-07-30 18:04:34 +08:00
return err
}
deliveryID = delivery . ID
}
return tx . Create ( & models . UserServiceRelation {
2026-07-31 09:54:17 +08:00
Entity : common . NewEntity ( common . StatusEnable ) , UserAccountID : account . ID ,
2026-07-30 18:04:34 +08:00
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 ) {
2026-07-31 09:54:17 +08:00
account , ok := common . UserAccount ( ctx )
2026-07-30 18:04:34 +08:00
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 ) {
2026-07-31 09:54:17 +08:00
account , ok := common . UserAccount ( ctx )
2026-07-30 18:04:34 +08:00
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 ) {
2026-07-31 09:54:17 +08:00
account , ok := common . UserAccount ( ctx )
2026-07-30 18:04:34 +08:00
if ! ok {
return
}
var request struct {
CurrentPassword string ` json:"current_password" binding:"required" `
NewPassword string ` json:"new_password" binding:"required" `
}
2026-07-31 09:54:17 +08:00
if ctx . ShouldBindJSON ( & request ) != nil || ! common . IsValidAccountPassword ( request . NewPassword ) ||
2026-07-30 18:04:34 +08:00
bcrypt . CompareHashAndPassword ( [ ] byte ( account . PasswordHash ) , [ ] byte ( request . CurrentPassword ) ) != nil {
infra . Response . Error ( ctx , errcode . ErrPassword )
return
}
2026-07-31 09:54:17 +08:00
hash , err := common . PasswordHash ( request . NewPassword )
2026-07-30 18:04:34 +08:00
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" `
}
2026-07-31 09:54:17 +08:00
if ctx . ShouldBindJSON ( & request ) != nil || ! common . IsValidAccountPassword ( request . NewPassword ) ||
! common . VerifyCode ( "user_app" , request . Phone , "reset_login_password" , request . RequestIdentity , request . Code ) {
2026-07-30 18:04:34 +08:00
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-07-31 09:54:17 +08:00
hash , err := common . PasswordHash ( request . NewPassword )
2026-07-30 18:04:34 +08:00
if err != nil {
infra . Response . Error ( ctx , err )
return
}
result := impl . DBService . Model ( & models . UserAccount { } ) .
2026-07-31 09:54:17 +08:00
Where ( "phone = ? AND status = ?" , strings . TrimSpace ( request . Phone ) , common . StatusEnable ) .
2026-07-30 18:04:34 +08:00
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 } )
}