From 4e6fe9a4561bc1c35631df38b40df6628afdaae1 Mon Sep 17 00:00:00 2001 From: yvan <8574526@qq.com> Date: Tue, 19 Aug 2025 21:57:19 +0800 Subject: [PATCH 1/6] wechat --- .../api/user/mini_user_api.go | 182 ++++++++++++++++++ .../wechat-integration/initialize/router.go | 3 + .../wechat-integration/router/router.go | 40 +++- 3 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 server/plugin/wechat-integration/api/user/mini_user_api.go diff --git a/server/plugin/wechat-integration/api/user/mini_user_api.go b/server/plugin/wechat-integration/api/user/mini_user_api.go new file mode 100644 index 00000000..f4c6cf7f --- /dev/null +++ b/server/plugin/wechat-integration/api/user/mini_user_api.go @@ -0,0 +1,182 @@ +package user + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "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" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type MiniUserApi struct{} + +var miniService = service.ServiceGroupApp.MiniService + +// Login 小程序登录 +// @Tags WechatMiniUser +// @Summary 小程序登录 +// @Description 微信小程序登录接口 +// @Accept json +// @Produce json +// @Param data body request.MiniLoginRequest true "登录参数" +// @Success 200 {object} response.Response{data=response.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) + return + } + + if req.Code == "" { + response.FailWithMessage("code不能为空", c) + return + } + + user, err := miniService.Code2Session(req.Code) + if err != nil { + global.GVA_LOG.Error("小程序登录失败!", zap.Error(err)) + response.FailWithMessage("登录失败: "+err.Error(), c) + return + } + + // 生成JWT token + token, _, err := utils.AppUserLoginToken(user) + if err != nil { + global.GVA_LOG.Error("生成token失败!", zap.Error(err)) + response.FailWithMessage("登录失败: "+err.Error(), c) + return + } + + resp := wechatResponse.MiniLoginResponse{ + OpenID: user.OpenID, + UnionID: user.UnionID, + Token: token, + } + + response.OkWithDetailed(resp, "登录成功", c) +} + +// GetUserInfo 获取用户信息 +// @Tags WechatMiniUser +// @Summary 获取用户信息 +// @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] +func (w *MiniUserApi) GetUserInfo(c *gin.Context) { + openid := c.Query("openid") + if openid == "" { + response.FailWithMessage("openid不能为空", c) + return + } + + user, err := miniService.GetUserInfo(openid) + if err != nil { + global.GVA_LOG.Error("获取用户信息失败!", zap.Error(err)) + response.FailWithMessage("获取失败: "+err.Error(), c) + return + } + + response.OkWithData(user, c) +} + +// UpdateUserInfo 更新用户信息 +// @Tags WechatMiniUser +// @Summary 更新用户信息 +// @Description 更新小程序用户信息 +// @Accept json +// @Produce json +// @Param data body request.UpdateUserInfoRequest true "用户信息" +// @Success 200 {object} response.Response "更新成功" +// @Router /wechat/user/mini/userinfo [put] +func (w *MiniUserApi) UpdateUserInfo(c *gin.Context) { + var req request.UpdateUserInfoRequest + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + if req.OpenID == "" { + response.FailWithMessage("openid不能为空", c) + return + } + + err = miniService.UpdateUserInfo(req.OpenID, req.UserInfo) + if err != nil { + global.GVA_LOG.Error("更新用户信息失败!", zap.Error(err)) + response.FailWithMessage("更新失败: "+err.Error(), c) + return + } + + response.OkWithMessage("更新成功", c) +} + +// BindPhone 绑定手机号 +// @Tags WechatMiniUser +// @Summary 绑定手机号 +// @Description 绑定手机号并关联系统用户 +// @Accept json +// @Produce json +// @Param data body request.BindPhoneRequest true "绑定参数" +// @Success 200 {object} response.Response{data=model.WechatMiniUser} "绑定成功" +// @Router /wechat/user/mini/bind-phone [post] +func (w *MiniUserApi) BindPhone(c *gin.Context) { + var req request.BindPhoneRequest + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + if req.OpenID == "" || req.Phone == "" { + response.FailWithMessage("openid和手机号不能为空", c) + return + } + + user, err := miniService.BindPhone(req.OpenID, req.Phone) + if err != nil { + global.GVA_LOG.Error("绑定手机号失败!", zap.Error(err)) + response.FailWithMessage("绑定失败: "+err.Error(), c) + return + } + + response.OkWithDetailed(user, "绑定成功", c) +} + +// CheckUnionID 检查UnionID是否存在(为APP登录预留) +// @Tags WechatMiniUser +// @Summary 检查UnionID +// @Description 检查UnionID是否已存在用户 +// @Accept json +// @Produce json +// @Param unionid query string true "UnionID" +// @Success 200 {object} response.Response{data=model.WechatMiniUser} "检查成功" +// @Router /wechat/user/mini/check-unionid [get] +func (w *MiniUserApi) CheckUnionID(c *gin.Context) { + unionid := c.Query("unionid") + if unionid == "" { + response.FailWithMessage("unionid不能为空", c) + return + } + + user, err := miniService.CheckUnionIDExists(unionid) + if err != nil { + global.GVA_LOG.Error("检查UnionID失败!", zap.Error(err)) + response.FailWithMessage("检查失败: "+err.Error(), c) + return + } + + if user == nil { + response.OkWithDetailed(nil, "UnionID不存在", c) + } else { + response.OkWithDetailed(user, "UnionID已存在", c) + } +} diff --git a/server/plugin/wechat-integration/initialize/router.go b/server/plugin/wechat-integration/initialize/router.go index 02f7294e..16d924b8 100644 --- a/server/plugin/wechat-integration/initialize/router.go +++ b/server/plugin/wechat-integration/initialize/router.go @@ -25,4 +25,7 @@ func Router(engine *gin.Engine) { // 私有路由(需要认证) wechatRouter.InitRouter(privateGroup) + + // 用户端路由(用户认证) + wechatRouter.InitUserRouter(userGroup) } diff --git a/server/plugin/wechat-integration/router/router.go b/server/plugin/wechat-integration/router/router.go index 591b816b..81bb54ab 100644 --- a/server/plugin/wechat-integration/router/router.go +++ b/server/plugin/wechat-integration/router/router.go @@ -2,6 +2,7 @@ package router import ( "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/api" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/api/user" "github.com/gin-gonic/gin" ) @@ -26,15 +27,10 @@ func (w *Router) InitRouter(Router *gin.RouterGroup) { mpDraftApi := api.MpDraftApi{} { - // 微信小程序路由 + // 微信小程序管理端路由 miniGroup := wechatRouter.Group("mini") { - miniGroup.POST("login", miniApi.Login) // 小程序登录 - miniGroup.GET("userinfo", miniApi.GetUserInfo) // 获取用户信息 - miniGroup.PUT("userinfo", miniApi.UpdateUserInfo) // 更新用户信息 - miniGroup.POST("bind-phone", miniApi.BindPhone) // 绑定手机号 - miniGroup.GET("users", miniApi.GetUserList) // 获取用户列表(管理) - miniGroup.GET("check-unionid", miniApi.CheckUnionID) // 检查UnionID + miniGroup.GET("users", miniApi.GetUserList) // 获取用户列表(管理) // 小程序统计 miniGroup.GET("statistics", miniStatisticsApi.GetMiniStatistics) // 获取基础统计数据 @@ -155,9 +151,39 @@ func (w *Router) InitWechatPublicRouter(Router *gin.RouterGroup) { wechatPublicRouter := Router.Group("wechat") webhookApi := api.WebhookApi{} + // 用户端API实例 + miniUserApi := user.MiniUserApi{} { + // 用户端API路由(不需要认证) + userGroup := wechatPublicRouter.Group("user") + { + // 微信小程序用户端路由 + userMiniGroup := userGroup.Group("mini") + { + userMiniGroup.POST("login", miniUserApi.Login) // 小程序登录(不需要认证) + } + } + // 微信公众号Webhook(公开接口,微信服务器调用) wechatPublicRouter.Any("official/webhook", webhookApi.OfficialAccountWebhook) } } + +// InitUserRouter 初始化用户端路由(需要用户认证) +func (w *Router) InitUserRouter(Router *gin.RouterGroup) { + wechatUserRouter := Router.Group("user/wechat") + + // 用户端API实例 + miniUserApi := user.MiniUserApi{} + + // 用户端API路由(需要用户认证) + // 微信小程序用户端路由 + 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 + } +} From 89cf5f0d57fe26db7240b9e18bab425144d41aff Mon Sep 17 00:00:00 2001 From: yvan <8574526@qq.com> Date: Fri, 5 Sep 2025 22:43:48 +0800 Subject: [PATCH 2/6] wechat --- .../api/user/mini_user_api.go | 122 +++++++++++++-- .../wechat-integration/initialize/api.go | 24 ++- .../model/request/mini_request.go | 7 + .../model/response/response.go | 147 ++++++++++++++++++ .../wechat-integration/router/router.go | 9 +- .../service/mini_service.go | 74 ++++++++- 6 files changed, 354 insertions(+), 29 deletions(-) diff --git a/server/plugin/wechat-integration/api/user/mini_user_api.go b/server/plugin/wechat-integration/api/user/mini_user_api.go index f4c6cf7f..9d940e67 100644 --- a/server/plugin/wechat-integration/api/user/mini_user_api.go +++ b/server/plugin/wechat-integration/api/user/mini_user_api.go @@ -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 diff --git a/server/plugin/wechat-integration/initialize/api.go b/server/plugin/wechat-integration/initialize/api.go index a010c37f..5824d9de 100644 --- a/server/plugin/wechat-integration/initialize/api.go +++ b/server/plugin/wechat-integration/initialize/api.go @@ -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: "获取小程序用户列表", diff --git a/server/plugin/wechat-integration/model/request/mini_request.go b/server/plugin/wechat-integration/model/request/mini_request.go index 5a10ab21..f88f04c5 100644 --- a/server/plugin/wechat-integration/model/request/mini_request.go +++ b/server/plugin/wechat-integration/model/request/mini_request.go @@ -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"` // 初始向量 +} diff --git a/server/plugin/wechat-integration/model/response/response.go b/server/plugin/wechat-integration/model/response/response.go index eea8308c..6b336d5b 100644 --- a/server/plugin/wechat-integration/model/response/response.go +++ b/server/plugin/wechat-integration/model/response/response.go @@ -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"` // 数据列表 diff --git a/server/plugin/wechat-integration/router/router.go b/server/plugin/wechat-integration/router/router.go index 81bb54ab..e338c4fc 100644 --- a/server/plugin/wechat-integration/router/router.go +++ b/server/plugin/wechat-integration/router/router.go @@ -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 } } diff --git a/server/plugin/wechat-integration/service/mini_service.go b/server/plugin/wechat-integration/service/mini_service.go index 975c67af..727a8108 100644 --- a/server/plugin/wechat-integration/service/mini_service.go +++ b/server/plugin/wechat-integration/service/mini_service.go @@ -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 +} From 16fb22479f20fb29faa756a8dc0c085580bb39da Mon Sep 17 00:00:00 2001 From: yvan <8574526@qq.com> Date: Fri, 5 Sep 2025 22:43:51 +0800 Subject: [PATCH 3/6] wechat --- .../wechat-integration/utils/response.go | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 server/plugin/wechat-integration/utils/response.go diff --git a/server/plugin/wechat-integration/utils/response.go b/server/plugin/wechat-integration/utils/response.go new file mode 100644 index 00000000..79e75273 --- /dev/null +++ b/server/plugin/wechat-integration/utils/response.go @@ -0,0 +1,102 @@ +package utils + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response" + "github.com/gin-gonic/gin" +) + +// ResponseHelper 响应助手 +type ResponseHelper struct{} + +// Success 成功响应 +func (r *ResponseHelper) Success(c *gin.Context, data interface{}) { + c.JSON(200, response.SuccessResponse(data)) +} + +// SuccessWithMsg 带消息的成功响应 +func (r *ResponseHelper) SuccessWithMsg(c *gin.Context, data interface{}, msg string) { + c.JSON(200, response.SuccessResponseWithMsg(data, msg)) +} + +// Error 错误响应 +func (r *ResponseHelper) Error(c *gin.Context, code int) { + c.JSON(200, response.ErrorResponse(code)) +} + +// ErrorWithMsg 带消息的错误响应 +func (r *ResponseHelper) ErrorWithMsg(c *gin.Context, code int, msg string) { + c.JSON(200, response.ErrorResponseWithMsg(code, msg)) +} + +// HandleError 处理错误并返回相应的错误码 +func (r *ResponseHelper) HandleError(c *gin.Context, err error) { + if err == nil { + return + } + + errMsg := err.Error() + + // 根据错误信息返回相应的错误码 + switch errMsg { + case "用户不存在": + r.Error(c, response.ERROR_USER_NOT_FOUND) + case "获取用户 OpenID 失败": + r.Error(c, response.ERROR_WX_OPENID_EMPTY) + case "微信小程序 Code2Session 失败": + r.Error(c, response.ERROR_WX_SESSION_FAILED) + case "解密手机号失败": + r.Error(c, response.ERROR_WX_PHONE_DECRYPT_FAILED) + case "解密数据中未找到手机号": + r.Error(c, response.ERROR_WX_PHONE_NOT_FOUND) + case "用户 SessionKey 不存在,请重新登录": + r.Error(c, response.ERROR_WX_SESSIONKEY_EMPTY) + default: + // 检查是否是数据库相关错误 + if isDBError(errMsg) { + r.ErrorWithMsg(c, response.ERROR_INTERNAL, "数据库操作失败") + } else { + r.ErrorWithMsg(c, response.ERROR_COMMON, errMsg) + } + } +} + +// isDBError 判断是否是数据库错误 +func isDBError(errMsg string) bool { + dbErrorKeywords := []string{ + "Unknown column", + "Table doesn't exist", + "Duplicate entry", + "Data too long", + "cannot be null", + "foreign key constraint", + } + + for _, keyword := range dbErrorKeywords { + if contains(errMsg, keyword) { + return true + } + } + return false +} + +// contains 检查字符串是否包含子字符串 +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || + (len(s) > len(substr) && + (s[:len(substr)] == substr || + s[len(s)-len(substr):] == substr || + indexOf(s, substr) >= 0))) +} + +// indexOf 查找子字符串的位置 +func indexOf(s, substr string) int { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return i + } + } + return -1 +} + +// 全局响应助手实例 +var ResponseUtil = &ResponseHelper{} From e26a9e773550ff1f2a509d9e70ab01ef50f72698 Mon Sep 17 00:00:00 2001 From: yvan <8574526@qq.com> Date: Fri, 5 Sep 2025 23:35:47 +0800 Subject: [PATCH 4/6] wechat --- .../api/user/mini_user_api.go | 25 ++++++++++--------- .../model/response/response.go | 25 ++++++++++--------- .../service/mini_service.go | 4 +++ server/utils/claims.go | 13 +++++++++- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/server/plugin/wechat-integration/api/user/mini_user_api.go b/server/plugin/wechat-integration/api/user/mini_user_api.go index 9d940e67..c2ef77a4 100644 --- a/server/plugin/wechat-integration/api/user/mini_user_api.go +++ b/server/plugin/wechat-integration/api/user/mini_user_api.go @@ -111,18 +111,19 @@ func (w *MiniUserApi) GetUserInfo(c *gin.Context) { // 构造响应数据 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"), + 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"), + NeedPhoneAuth: user.Phone == nil || *user.Phone == "", // 如果没有手机号则需要授权 } c.JSON(200, wechatResponse.SuccessResponseWithMsg(resp, "获取成功")) diff --git a/server/plugin/wechat-integration/model/response/response.go b/server/plugin/wechat-integration/model/response/response.go index 6b336d5b..750385d4 100644 --- a/server/plugin/wechat-integration/model/response/response.go +++ b/server/plugin/wechat-integration/model/response/response.go @@ -135,18 +135,19 @@ type MiniLoginResponse struct { // 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"` // 更新时间 + 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"` // 更新时间 + NeedPhoneAuth bool `json:"needPhoneAuth"` // 是否需要授权手机号 } // PhoneUpdateResponse 手机号更新响应 diff --git a/server/plugin/wechat-integration/service/mini_service.go b/server/plugin/wechat-integration/service/mini_service.go index 727a8108..ebac5ed7 100644 --- a/server/plugin/wechat-integration/service/mini_service.go +++ b/server/plugin/wechat-integration/service/mini_service.go @@ -56,9 +56,13 @@ func (w *MiniService) Code2Session(code string) (*model.MiniUser, error) { if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // 创建新用户 + // 生成默认昵称:萌宠爱 + OpenID的前6位 + defaultNickname := "萌宠爱" + session.OpenID[:6] + user = model.MiniUser{ OpenID: session.OpenID, SessionKey: &session.SessionKey, + Nickname: &defaultNickname, } if session.UnionID != "" { user.UnionID = &session.UnionID diff --git a/server/utils/claims.go b/server/utils/claims.go index 0744085c..2647e4b9 100644 --- a/server/utils/claims.go +++ b/server/utils/claims.go @@ -68,13 +68,24 @@ func SetUserToken(c *gin.Context, token string, maxAge int) { } func GetUserToken(c *gin.Context) string { + // 首先尝试从 user-token 头部获取 token := c.Request.Header.Get("user-token") + + // 如果没有,尝试从 Authorization Bearer 头部获取 + if token == "" { + authHeader := c.Request.Header.Get("Authorization") + if authHeader != "" && len(authHeader) > 7 && authHeader[:7] == "Bearer " { + token = authHeader[7:] // 去掉 "Bearer " 前缀 + } + } + + // 如果还是没有,尝试从 cookie 获取 if token == "" { j := NewJWT() token, _ = c.Cookie("user-token") claims, err := j.ParseToken(token) if err != nil { - global.GVA_LOG.Error("重新写入cookie token失败,未能成功解析token,请检查请求头是否存在user-token且claims是否为规定结构") + global.GVA_LOG.Error("重新写入cookie token失败,未能成功解析token,请检查请求头是否存在user-token或Authorization Bearer且claims是否为规定结构") return token } SetUserToken(c, token, int((claims.ExpiresAt.Unix()-time.Now().Unix())/60)) From a8b67d750b835d5c73630694a987c9bff381ffc2 Mon Sep 17 00:00:00 2001 From: yvan <8574526@qq.com> Date: Fri, 5 Sep 2025 23:37:31 +0800 Subject: [PATCH 5/6] wechat --- server/plugin/wechat-integration/model/response/response.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/plugin/wechat-integration/model/response/response.go b/server/plugin/wechat-integration/model/response/response.go index 750385d4..cbba370c 100644 --- a/server/plugin/wechat-integration/model/response/response.go +++ b/server/plugin/wechat-integration/model/response/response.go @@ -135,9 +135,6 @@ type MiniLoginResponse struct { // 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"` // 用户手机号 @@ -145,8 +142,6 @@ type MiniUserInfoResponse struct { City *string `json:"city"` // 城市 Province *string `json:"province"` // 省份 Country *string `json:"country"` // 国家 - CreateTime string `json:"createTime"` // 创建时间 - UpdateTime string `json:"updateTime"` // 更新时间 NeedPhoneAuth bool `json:"needPhoneAuth"` // 是否需要授权手机号 } From f35125cbce63f7e74f9b8f85329ee531a8328d22 Mon Sep 17 00:00:00 2001 From: yvan <8574526@qq.com> Date: Fri, 5 Sep 2025 23:37:57 +0800 Subject: [PATCH 6/6] wechat --- server/plugin/wechat-integration/api/user/mini_user_api.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/server/plugin/wechat-integration/api/user/mini_user_api.go b/server/plugin/wechat-integration/api/user/mini_user_api.go index c2ef77a4..668382a6 100644 --- a/server/plugin/wechat-integration/api/user/mini_user_api.go +++ b/server/plugin/wechat-integration/api/user/mini_user_api.go @@ -111,18 +111,12 @@ func (w *MiniUserApi) GetUserInfo(c *gin.Context) { // 构造响应数据 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"), NeedPhoneAuth: user.Phone == nil || *user.Phone == "", // 如果没有手机号则需要授权 }