Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
27c97aa5c1
|
|
@ -0,0 +1,271 @@
|
|||
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"
|
||||
"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} 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 {
|
||||
c.JSON(200, wechatResponse.ErrorResponseWithMsg(wechatResponse.ERROR_PARAM_INVALID, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Code == "" {
|
||||
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))
|
||||
|
||||
// 根据错误类型返回不同的错误码
|
||||
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
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
token, _, err := utils.AppUserLoginToken(user)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("生成token失败!", zap.Error(err))
|
||||
c.JSON(200, wechatResponse.ErrorResponseWithMsg(wechatResponse.ERROR_INTERNAL, "生成token失败"))
|
||||
return
|
||||
}
|
||||
|
||||
resp := wechatResponse.MiniLoginResponse{
|
||||
OpenID: user.OpenID,
|
||||
UnionID: user.UnionID,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
c.JSON(200, wechatResponse.SuccessResponseWithMsg(resp, "登录成功"))
|
||||
}
|
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
// @Tags WechatMiniUser
|
||||
// @Summary 获取用户信息
|
||||
// @Description 获取当前登录用户的详细信息
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} wechatResponse.Response{data=wechatResponse.MiniUserInfoResponse} "获取成功"
|
||||
// @Router /user/wechat/mini/userinfo [get]
|
||||
func (w *MiniUserApi) GetUserInfo(c *gin.Context) {
|
||||
// 从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 == "" {
|
||||
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))
|
||||
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
|
||||
}
|
||||
|
||||
// 构造响应数据
|
||||
resp := wechatResponse.MiniUserInfoResponse{
|
||||
NickName: user.Nickname,
|
||||
Avatar: user.AvatarURL,
|
||||
Gender: user.Gender,
|
||||
City: user.City,
|
||||
Province: user.Province,
|
||||
Country: user.Country,
|
||||
NeedPhoneAuth: user.Phone == nil || *user.Phone == "", // 如果没有手机号则需要授权
|
||||
}
|
||||
|
||||
c.JSON(200, wechatResponse.SuccessResponseWithMsg(resp, "获取成功"))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// @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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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: "获取小程序用户列表",
|
||||
|
|
|
|||
|
|
@ -25,4 +25,7 @@ func Router(engine *gin.Engine) {
|
|||
|
||||
// 私有路由(需要认证)
|
||||
wechatRouter.InitRouter(privateGroup)
|
||||
|
||||
// 用户端路由(用户认证)
|
||||
wechatRouter.InitUserRouter(userGroup)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"` // 初始向量
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,23 @@ type MiniLoginResponse struct {
|
|||
Token string `json:"token"` // JWT token
|
||||
}
|
||||
|
||||
// MiniUserInfoResponse 小程序用户信息响应
|
||||
type MiniUserInfoResponse struct {
|
||||
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"` // 国家
|
||||
NeedPhoneAuth bool `json:"needPhoneAuth"` // 是否需要授权手机号
|
||||
}
|
||||
|
||||
// PhoneUpdateResponse 手机号更新响应
|
||||
type PhoneUpdateResponse struct {
|
||||
Phone string `json:"phone"` // 更新后的手机号
|
||||
}
|
||||
|
||||
// PageResult 分页结果
|
||||
type PageResult struct {
|
||||
List interface{} `json:"list"` // 数据列表
|
||||
|
|
|
|||
|
|
@ -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,40 @@ 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.POST("phone-update", miniUserApi.UpdatePhoneNumber) // 更新手机号(通过加密数据)
|
||||
userMiniGroup.GET("check-unionid", miniUserApi.CheckUnionID) // 检查UnionID
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,13 +52,17 @@ 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) {
|
||||
// 创建新用户
|
||||
// 生成默认昵称:萌宠爱 + OpenID的前6位
|
||||
defaultNickname := "萌宠爱" + session.OpenID[:6]
|
||||
|
||||
user = model.MiniUser{
|
||||
OpenID: session.OpenID,
|
||||
SessionKey: &session.SessionKey,
|
||||
Nickname: &defaultNickname,
|
||||
}
|
||||
if session.UnionID != "" {
|
||||
user.UnionID = &session.UnionID
|
||||
|
|
@ -98,7 +102,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 +169,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 +217,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 +225,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 +254,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 +263,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in New Issue