This commit is contained in:
yvan 2025-09-05 22:43:48 +08:00
parent 4e6fe9a456
commit 89cf5f0d57
6 changed files with 354 additions and 29 deletions

View File

@ -3,6 +3,7 @@ package user
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
@ -22,25 +23,33 @@ var miniService = service.ServiceGroupApp.MiniService
// @Accept json
// @Produce json
// @Param data body request.MiniLoginRequest true "登录参数"
// @Success 200 {object} response.Response{data=response.MiniLoginResponse} "登录成功"
// @Success 200 {object} wechatResponse.Response{data=wechatResponse.MiniLoginResponse} "登录成功"
// @Router /wechat/user/mini/login [post]
func (w *MiniUserApi) Login(c *gin.Context) {
var req request.MiniLoginRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
c.JSON(200, wechatResponse.ErrorResponseWithMsg(wechatResponse.ERROR_PARAM_INVALID, err.Error()))
return
}
if req.Code == "" {
response.FailWithMessage("code不能为空", c)
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_PARAM_MISSING))
return
}
user, err := miniService.Code2Session(req.Code)
if err != nil {
global.GVA_LOG.Error("小程序登录失败!", zap.Error(err))
response.FailWithMessage("登录失败: "+err.Error(), c)
// 根据错误类型返回不同的错误码
if err.Error() == "获取用户 OpenID 失败" {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_WX_OPENID_EMPTY))
} else if err.Error() == "微信小程序 Code2Session 失败" {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_WX_SESSION_FAILED))
} else {
c.JSON(200, wechatResponse.ErrorResponseWithMsg(wechatResponse.ERROR_WX_CODE_INVALID, err.Error()))
}
return
}
@ -48,7 +57,7 @@ func (w *MiniUserApi) Login(c *gin.Context) {
token, _, err := utils.AppUserLoginToken(user)
if err != nil {
global.GVA_LOG.Error("生成token失败!", zap.Error(err))
response.FailWithMessage("登录失败: "+err.Error(), c)
c.JSON(200, wechatResponse.ErrorResponseWithMsg(wechatResponse.ERROR_INTERNAL, "生成token失败"))
return
}
@ -58,33 +67,65 @@ func (w *MiniUserApi) Login(c *gin.Context) {
Token: token,
}
response.OkWithDetailed(resp, "登录成功", c)
c.JSON(200, wechatResponse.SuccessResponseWithMsg(resp, "登录成功"))
}
// GetUserInfo 获取用户信息
// @Tags WechatMiniUser
// @Summary 获取用户信息
// @Description 获取小程序用户信息
// @Description 获取当前登录用户的详细信息
// @Accept json
// @Produce json
// @Param openid query string true "用户openid"
// @Success 200 {object} response.Response{data=model.WechatMiniUser} "获取成功"
// @Router /wechat/user/mini/userinfo [get]
// @Success 200 {object} wechatResponse.Response{data=wechatResponse.MiniUserInfoResponse} "获取成功"
// @Router /user/wechat/mini/userinfo [get]
func (w *MiniUserApi) GetUserInfo(c *gin.Context) {
openid := c.Query("openid")
// 从JWT token中获取用户信息
claims, exists := c.Get("user_claims")
if !exists {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_UNAUTHORIZED))
return
}
userClaims, ok := claims.(*systemReq.AppUserClaims)
if !ok {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_TOKEN_INVALID))
return
}
openid := userClaims.AppBaseClaims.OpenID
if openid == "" {
response.FailWithMessage("openid不能为空", c)
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_WX_OPENID_EMPTY))
return
}
user, err := miniService.GetUserInfo(openid)
if err != nil {
global.GVA_LOG.Error("获取用户信息失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
if err.Error() == "用户不存在" {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_USER_NOT_FOUND))
} else {
c.JSON(200, wechatResponse.ErrorResponseWithMsg(wechatResponse.ERROR_INTERNAL, err.Error()))
}
return
}
response.OkWithData(user, c)
// 构造响应数据
resp := wechatResponse.MiniUserInfoResponse{
ID: user.ID,
OpenID: user.OpenID,
UnionID: user.UnionID,
NickName: user.Nickname,
Avatar: user.AvatarURL,
Phone: user.Phone,
Gender: user.Gender,
City: user.City,
Province: user.Province,
Country: user.Country,
CreateTime: user.CreatedAt.Format("2006-01-02 15:04:05"),
UpdateTime: user.UpdatedAt.Format("2006-01-02 15:04:05"),
}
c.JSON(200, wechatResponse.SuccessResponseWithMsg(resp, "获取成功"))
}
// UpdateUserInfo 更新用户信息
@ -151,6 +192,59 @@ func (w *MiniUserApi) BindPhone(c *gin.Context) {
response.OkWithDetailed(user, "绑定成功", c)
}
// UpdatePhoneNumber 更新用户手机号
// @Tags WechatMiniUser
// @Summary 更新用户手机号
// @Description 通过微信手机号授权更新用户手机号
// @Accept json
// @Produce json
// @Param data body request.WxPhoneLoginRequest true "手机号授权参数"
// @Success 200 {object} response.Response{data=string} "更新成功"
// @Router /user/wechat/mini/phone-update [post]
func (w *MiniUserApi) UpdatePhoneNumber(c *gin.Context) {
var req request.WxPhoneLoginRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.EncryptedData == "" || req.IV == "" {
response.FailWithMessage("encryptedData和iv不能为空", c)
return
}
// 从 JWT token 中获取用户信息
claims, exists := c.Get("user_claims")
if !exists {
response.FailWithMessage("获取用户信息失败", c)
return
}
// 获取当前用户的信息
userClaims, ok := claims.(*systemReq.AppUserClaims)
if !ok {
response.FailWithMessage("用户信息格式错误", c)
return
}
// 直接从 claims 中获取 openid
openid := userClaims.AppBaseClaims.OpenID
if openid == "" {
response.FailWithMessage("用户 OpenID 不存在", c)
return
}
phoneNumber, err := miniService.UpdatePhoneByEncryption(openid, req.EncryptedData, req.IV)
if err != nil {
global.GVA_LOG.Error("更新手机号失败!", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c)
return
}
response.OkWithDetailed(phoneNumber, "手机号更新成功", c)
}
// CheckUnionID 检查UnionID是否存在为APP登录预留
// @Tags WechatMiniUser
// @Summary 检查UnionID

View File

@ -12,31 +12,45 @@ import (
func Api(ctx context.Context) {
// 注册微信集成相关API
utils.RegisterApis(
// 小程序用户相关API
// 小程序用户相关API(公开接口)
system.SysApi{
Path: "/wechat/mini/login",
Path: "/wechat/user/mini/login",
Description: "小程序登录",
ApiGroup: "微信小程序",
Method: "POST",
},
// 小程序用户相关API需要认证
system.SysApi{
Path: "/wechat/mini/userinfo",
Path: "/user/wechat/mini/phone-update",
Description: "更新用户手机号",
ApiGroup: "微信小程序",
Method: "POST",
},
// 小程序用户相关API需要认证
system.SysApi{
Path: "/user/wechat/mini/userinfo",
Description: "获取小程序用户信息",
ApiGroup: "微信小程序",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mini/userinfo",
Path: "/user/wechat/mini/userinfo",
Description: "更新小程序用户信息",
ApiGroup: "微信小程序",
Method: "PUT",
},
system.SysApi{
Path: "/wechat/mini/bind-phone",
Path: "/user/wechat/mini/bind-phone",
Description: "绑定手机号",
ApiGroup: "微信小程序",
Method: "POST",
},
system.SysApi{
Path: "/user/wechat/mini/check-unionid",
Description: "检查UnionID",
ApiGroup: "微信小程序",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mini/users",
Description: "获取小程序用户列表",

View File

@ -39,3 +39,10 @@ type SendImageMessageRequest struct {
OpenID string `json:"openid" binding:"required"` // 用户openid
MediaID string `json:"mediaId" binding:"required"` // 媒体ID
}
// WxPhoneLoginRequest 微信手机号授权登录请求
type WxPhoneLoginRequest struct {
Code string `json:"code" binding:"required"` // 微信登录凭证
EncryptedData string `json:"encryptedData" binding:"required"` // 加密的手机号数据
IV string `json:"iv" binding:"required"` // 初始向量
}

View File

@ -1,5 +1,131 @@
package response
// 微信小程序错误码定义
const (
// 成功
SUCCESS = 0
// 通用错误码 (1000-1999)
ERROR_COMMON = 1000 // 通用错误
ERROR_PARAM_INVALID = 1001 // 参数无效
ERROR_PARAM_MISSING = 1002 // 参数缺失
ERROR_INTERNAL = 1003 // 内部错误
ERROR_NETWORK = 1004 // 网络错误
ERROR_TIMEOUT = 1005 // 请求超时
// 认证相关错误码 (2000-2999)
ERROR_AUTH_FAILED = 2000 // 认证失败
ERROR_TOKEN_INVALID = 2001 // Token无效
ERROR_TOKEN_EXPIRED = 2002 // Token过期
ERROR_UNAUTHORIZED = 2003 // 未授权
ERROR_PERMISSION_DENIED = 2004 // 权限不足
// 微信相关错误码 (3000-3999)
ERROR_WX_CODE_INVALID = 3000 // 微信Code无效
ERROR_WX_SESSION_FAILED = 3001 // 获取微信Session失败
ERROR_WX_DECRYPT_FAILED = 3002 // 微信数据解密失败
ERROR_WX_OPENID_EMPTY = 3003 // 微信OpenID为空
ERROR_WX_SESSIONKEY_EMPTY = 3004 // 微信SessionKey为空
ERROR_WX_PHONE_DECRYPT_FAILED = 3005 // 微信手机号解密失败
ERROR_WX_PHONE_NOT_FOUND = 3006 // 解密数据中未找到手机号
// 用户相关错误码 (4000-4999)
ERROR_USER_NOT_FOUND = 4000 // 用户不存在
ERROR_USER_EXISTED = 4001 // 用户已存在
ERROR_USER_CREATE_FAILED = 4002 // 用户创建失败
ERROR_USER_UPDATE_FAILED = 4003 // 用户更新失败
ERROR_USER_DELETE_FAILED = 4004 // 用户删除失败
ERROR_USER_DISABLED = 4005 // 用户已禁用
)
// 错误信息映射
var ErrorMessages = map[int]string{
SUCCESS: "操作成功",
// 通用错误
ERROR_COMMON: "操作失败",
ERROR_PARAM_INVALID: "参数无效",
ERROR_PARAM_MISSING: "参数缺失",
ERROR_INTERNAL: "内部错误",
ERROR_NETWORK: "网络错误",
ERROR_TIMEOUT: "请求超时",
// 认证相关错误
ERROR_AUTH_FAILED: "认证失败",
ERROR_TOKEN_INVALID: "Token无效",
ERROR_TOKEN_EXPIRED: "Token已过期",
ERROR_UNAUTHORIZED: "未授权访问",
ERROR_PERMISSION_DENIED: "权限不足",
// 微信相关错误
ERROR_WX_CODE_INVALID: "微信Code无效",
ERROR_WX_SESSION_FAILED: "获取微信Session失败",
ERROR_WX_DECRYPT_FAILED: "微信数据解密失败",
ERROR_WX_OPENID_EMPTY: "微信OpenID为空",
ERROR_WX_SESSIONKEY_EMPTY: "微信SessionKey为空",
ERROR_WX_PHONE_DECRYPT_FAILED: "微信手机号解密失败",
ERROR_WX_PHONE_NOT_FOUND: "解密数据中未找到手机号",
// 用户相关错误
ERROR_USER_NOT_FOUND: "用户不存在",
ERROR_USER_EXISTED: "用户已存在",
ERROR_USER_CREATE_FAILED: "用户创建失败",
ERROR_USER_UPDATE_FAILED: "用户更新失败",
ERROR_USER_DELETE_FAILED: "用户删除失败",
ERROR_USER_DISABLED: "用户已禁用",
}
// GetErrorMessage 获取错误信息
func GetErrorMessage(code int) string {
if msg, exists := ErrorMessages[code]; exists {
return msg
}
return "未知错误"
}
// Response 标准响应结构
type Response struct {
Code int `json:"code"`
Data interface{} `json:"data"`
Msg string `json:"msg"`
}
// SuccessResponse 成功响应
func SuccessResponse(data interface{}) Response {
return Response{
Code: SUCCESS,
Data: data,
Msg: GetErrorMessage(SUCCESS),
}
}
// SuccessResponseWithMsg 带自定义消息的成功响应
func SuccessResponseWithMsg(data interface{}, msg string) Response {
return Response{
Code: SUCCESS,
Data: data,
Msg: msg,
}
}
// ErrorResponse 错误响应
func ErrorResponse(code int) Response {
return Response{
Code: code,
Data: nil,
Msg: GetErrorMessage(code),
}
}
// ErrorResponseWithMsg 带自定义消息的错误响应
func ErrorResponseWithMsg(code int, msg string) Response {
return Response{
Code: code,
Data: nil,
Msg: msg,
}
}
// MiniLoginResponse 小程序登录响应
type MiniLoginResponse struct {
OpenID string `json:"openid"` // 用户openid
@ -7,6 +133,27 @@ type MiniLoginResponse struct {
Token string `json:"token"` // JWT token
}
// MiniUserInfoResponse 小程序用户信息响应
type MiniUserInfoResponse struct {
ID uint `json:"id"` // 用户ID
OpenID string `json:"openid"` // 用户openid
UnionID *string `json:"unionid"` // 用户unionid
NickName *string `json:"nickName"` // 用户昵称
Avatar *string `json:"avatar"` // 用户头像
Phone *string `json:"phone"` // 用户手机号
Gender *int `json:"gender"` // 性别0-未知1-男2-女
City *string `json:"city"` // 城市
Province *string `json:"province"` // 省份
Country *string `json:"country"` // 国家
CreateTime string `json:"createTime"` // 创建时间
UpdateTime string `json:"updateTime"` // 更新时间
}
// PhoneUpdateResponse 手机号更新响应
type PhoneUpdateResponse struct {
Phone string `json:"phone"` // 更新后的手机号
}
// PageResult 分页结果
type PageResult struct {
List interface{} `json:"list"` // 数据列表

View File

@ -181,9 +181,10 @@ func (w *Router) InitUserRouter(Router *gin.RouterGroup) {
// 微信小程序用户端路由
userMiniGroup := wechatUserRouter.Group("mini")
{
userMiniGroup.GET("userinfo", miniUserApi.GetUserInfo) // 获取用户信息
userMiniGroup.PUT("userinfo", miniUserApi.UpdateUserInfo) // 更新用户信息
userMiniGroup.POST("bind-phone", miniUserApi.BindPhone) // 绑定手机号
userMiniGroup.GET("check-unionid", miniUserApi.CheckUnionID) // 检查UnionID
userMiniGroup.GET("userinfo", miniUserApi.GetUserInfo) // 获取用户信息
userMiniGroup.PUT("userinfo", miniUserApi.UpdateUserInfo) // 更新用户信息
userMiniGroup.POST("bind-phone", miniUserApi.BindPhone) // 绑定手机号
userMiniGroup.POST("phone-update", miniUserApi.UpdatePhoneNumber) // 更新手机号(通过加密数据)
userMiniGroup.GET("check-unionid", miniUserApi.CheckUnionID) // 检查UnionID
}
}

View File

@ -52,7 +52,7 @@ func (w *MiniService) Code2Session(code string) (*model.MiniUser, error) {
// 查找或创建用户
var user model.MiniUser
err = global.GVA_DB.Where("openid = ?", session.OpenID).First(&user).Error
err = global.GVA_DB.Where("open_id = ?", session.OpenID).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 创建新用户
@ -98,7 +98,20 @@ func (w *MiniService) Code2Session(code string) (*model.MiniUser, error) {
// GetUserInfo 获取用户信息
func (w *MiniService) GetUserInfo(openid string) (*model.MiniUser, error) {
var user model.MiniUser
err := global.GVA_DB.Where("openid = ?", openid).First(&user).Error
err := global.GVA_DB.Where("open_id = ?", openid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
}
return nil, err
}
return &user, nil
}
// GetUserByID 根据用户ID获取用户信息
func (w *MiniService) GetUserByID(userID uint) (*model.MiniUser, error) {
var user model.MiniUser
err := global.GVA_DB.Where("id = ?", userID).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
@ -152,7 +165,7 @@ func (w *MiniService) UpdateUserInfo(openid string, userInfo map[string]interfac
return errors.New("没有需要更新的信息")
}
err := global.GVA_DB.Model(&model.MiniUser{}).Where("openid = ?", openid).Updates(updates).Error
err := global.GVA_DB.Model(&model.MiniUser{}).Where("open_id = ?", openid).Updates(updates).Error
if err != nil {
global.GVA_LOG.Error("更新微信小程序用户信息失败: " + err.Error())
return err
@ -200,7 +213,7 @@ func (w *MiniService) GetUserByUnionID(unionid string) (*model.MiniUser, error)
// BindPhone 绑定手机号
func (w *MiniService) BindPhone(openid, phone string) (*model.MiniUser, error) {
// 更新小程序用户的手机号
err := global.GVA_DB.Model(&model.MiniUser{}).Where("openid = ?", openid).Update("phone", phone).Error
err := global.GVA_DB.Model(&model.MiniUser{}).Where("open_id = ?", openid).Update("phone", phone).Error
if err != nil {
global.GVA_LOG.Error("更新手机号失败: " + err.Error())
return nil, err
@ -208,7 +221,7 @@ func (w *MiniService) BindPhone(openid, phone string) (*model.MiniUser, error) {
// 获取更新后的用户信息
var user model.MiniUser
err = global.GVA_DB.Where("openid = ?", openid).First(&user).Error
err = global.GVA_DB.Where("open_id = ?", openid).First(&user).Error
if err != nil {
return nil, err
}
@ -237,7 +250,7 @@ func (w *MiniService) CheckUnionIDExists(unionid string) (*model.MiniUser, error
}
var user model.MiniUser
err := global.GVA_DB.Where("unionid = ?", unionid).First(&user).Error
err := global.GVA_DB.Where("union_id = ?", unionid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 不存在,返回 nil 但不报错
@ -246,3 +259,52 @@ func (w *MiniService) CheckUnionIDExists(unionid string) (*model.MiniUser, error
}
return &user, nil
}
// UpdatePhoneByEncryption 通过加密数据更新用户手机号
func (w *MiniService) UpdatePhoneByEncryption(openid, encryptedData, iv string) (string, error) {
// 1. 获取用户信息
var user model.MiniUser
err := global.GVA_DB.Where("open_id = ?", openid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", errors.New("用户不存在")
}
return "", err
}
if user.SessionKey == nil || *user.SessionKey == "" {
return "", errors.New("用户 SessionKey 不存在,请重新登录")
}
// 2. 获取微信小程序实例
mini, err := w.GetWechatMiniProgram()
if err != nil {
return "", err
}
// 3. 使用 session_key 解密手机号
encryptor := mini.GetEncryptor()
plainData, err := encryptor.Decrypt(*user.SessionKey, encryptedData, iv)
if err != nil {
global.GVA_LOG.Error("解密手机号失败: " + err.Error())
return "", errors.New("解密手机号失败")
}
// 4. 从解密数据中获取手机号
phoneNumber := ""
if plainData.PhoneNumber != "" {
phoneNumber = plainData.PhoneNumber
} else {
return "", errors.New("解密数据中未找到手机号")
}
// 5. 更新用户手机号
err = global.GVA_DB.Model(&user).Update("phone", phoneNumber).Error
if err != nil {
global.GVA_LOG.Error("更新手机号失败: " + err.Error())
return "", err
}
global.GVA_LOG.Info("用户手机号更新成功: " + phoneNumber + ", openid: " + openid)
return phoneNumber, nil
}