Files
sunct 3c3bf53ae4 docs(website): 添加简记memo产品原型和UI设计规范文档
- 新增「简记memo」一体化小程序产品原型设计文档
- 新增简记memo完整版UI视觉设计规范和界面细节
- 添加IDEA项目配置文件.gitignore
- 创建404页面HTML文件,包含响应式布局和错误提示
- 添加关于页面HTML文件,展示品牌介绍和团队信息
- 实现AES加解密工具函数,支持请求体加密
- 添加用户协议页面基础框架
2026-07-31 14:12:32 +08:00

983 lines
29 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 小程序用户接口
package app
import (
"context"
"errors"
"fmt"
"simple-memo/global"
"simple-memo/models"
"simple-memo/utils"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// -------------------------- 1. 定义 Handler 接口 --------------------------
// UserHandler 用户模块接口定义
type UserHandler interface {
Login(c *gin.Context) // 微信登录
Logout(c *gin.Context) // 退出登录
UserInfo(c *gin.Context) // 获取用户信息
EditUser(c *gin.Context) // 修改用户信息
SendEmailCode(c *gin.Context) // 发送邮箱验证码(支持多种场景)
SetEmail(c *gin.Context) // 设置邮箱(绑定邮箱)
EmailLogin(c *gin.Context) // 邮箱密码登录
EmailRegister(c *gin.Context) // 邮箱注册
ResetPassword(c *gin.Context) // 重置密码(未登录找回密码)
ChangePassword(c *gin.Context) // 修改密码(已登录场景)
CheckEmailExists(c *gin.Context) // 检查邮箱是否已注册
BindWechat(c *gin.Context) // 绑定微信(Web端,网页授权code)
BindWechatMini(c *gin.Context) // 绑定微信(小程序端,wx.login code
}
// -------------------------- 2. 实现结构体(依赖注入DB --------------------------
// userHandler 接口实现结构体
type userHandler struct {
db *gorm.DB // 注入数据库,方便测试
}
// NewUserHandler 创建用户处理器(对外暴露)
func NewUserHandler() UserHandler {
return &userHandler{
db: global.DB,
}
}
// -------------------------- 3. 请求结构体定义 --------------------------
// LoginReq 登录参数
type LoginReq struct {
Code string `json:"code"`
}
type EditUserReq struct {
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Signature string `json:"signature"`
}
// -------------------------- 4. 接口实现 --------------------------
// Login 微信登录
func (h *userHandler) Login(c *gin.Context) {
var req LoginReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "参数解析失败")
return
}
if req.Code == "" {
utils.Fail(c, "code不能为空")
return
}
// 调用微信接口获取 openid
wxResp, err := utils.MiniProgramCode2Session(req.Code)
if err != nil {
utils.Fail(c, "微信登录失败:"+err.Error())
return
}
// 查询或创建用户
var user models.User
err = h.db.Where("openid = ?", wxResp.OpenID).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
// 创建新用户
user = models.User{
Openid: wxResp.OpenID,
UserCode: utils.GenerateUserCode(),
}
err = h.db.Create(&user).Error
}
if err != nil {
utils.Fail(c, "用户操作失败")
return
}
// 生成 token
token, expiresAt, err := utils.GenerateTokenWithExpire(user.ID)
if err != nil {
utils.Fail(c, "token生成失败")
return
}
// 返回数据
utils.Ok(c, gin.H{
"token": token,
"expiresAt": expiresAt,
"expiresIn": utils.GetTokenExpireSeconds(),
})
}
// Logout 退出登录
func (h *userHandler) Logout(c *gin.Context) {
// 从上下文获取用户ID和token(JWTAuth中间件已注入)
userId, exists := c.Get("userID")
if !exists {
utils.Fail(c, "用户未登录")
return
}
token, tokenExists := c.Get("token")
if !tokenExists {
utils.Fail(c, "获取token失败")
return
}
// 解析 token 获取过期时间
claims, err := utils.ParseToken(token.(string))
if err != nil {
utils.Fail(c, "token无效")
return
}
// 计算剩余过期时间
ttl := time.Until(claims.ExpiresAt.Time)
if ttl > 0 {
// 将 token 加入 Redis 黑名单
ctx := context.Background()
err = global.Redis.Set(ctx, "token_blacklist:"+token.(string), "1", ttl).Err()
if err != nil {
global.Logger.Error("添加token到黑名单失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "退出登录失败")
return
}
global.Logger.Info("用户退出登录", append(utils.LogContextFields(c), zap.Uint("userID", userId.(uint)))...)
}
utils.Ok(c, "退出登录成功")
}
// UserInfo 获取用户信息
func (h *userHandler) UserInfo(c *gin.Context) {
userId, exists := c.Get("userID")
if !exists {
utils.Fail(c, "用户未登录")
return
}
var user models.User
err := h.db.First(&user, userId).Error
if err != nil {
utils.Fail(c, "获取用户信息失败")
return
}
// 返回脱敏用户信息
utils.Ok(c, gin.H{
"id": user.ID,
"nickname": user.Nickname,
"avatar": user.Avatar,
"signature": user.Signature,
"email": user.Email,
"user_code": user.UserCode,
"is_bind_wechat": user.Openid != "", // 是否绑定微信
"is_bind_email": user.Email != "", // 是否绑定邮箱
"createdAt": user.CreatedAt,
})
}
// EditUser 修改用户信息
func (h *userHandler) EditUser(c *gin.Context) {
userId, exists := c.Get("userID")
if !exists {
utils.Fail(c, "用户未登录")
c.Abort()
return
}
var editUserReq EditUserReq
if err := c.ShouldBindJSON(&editUserReq); err != nil {
utils.Fail(c, "参数解析失败")
return
}
// 如果 昵称或者头像为空,则不更新
if editUserReq.Nickname == "" && editUserReq.Avatar == "" && editUserReq.Signature == "" {
utils.Ok(c, nil)
return
}
// 构建更新数据(支持同时更新多个字段)
data := make(map[string]any)
if editUserReq.Nickname != "" {
data["nickname"] = utils.FilterSensitive(editUserReq.Nickname) // 敏感词过滤
}
if editUserReq.Avatar != "" {
data["avatar"] = editUserReq.Avatar
}
if editUserReq.Signature != "" {
data["signature"] = utils.FilterSensitive(editUserReq.Signature) // 敏感词过滤
}
if len(data) == 0 {
utils.Ok(c, nil)
return
}
err := h.db.Model(&models.User{}).Where("id = ?", userId).Updates(data).Error
if err != nil {
utils.Fail(c, "用户信息更新失败")
return
}
utils.Ok(c, nil)
}
// -------------------------- 4. 邮箱验证接口 --------------------------
// SendEmailCodeReq 发送邮箱验证码请求
type SendEmailCodeReq struct {
Email string `json:"email" binding:"required,email"`
Type string `json:"type" binding:"omitempty,oneof=register login bind reset_password change_email"` // type为空时:已登录状态下绑定邮箱
CaptchaId string `json:"captchaId"` // 图形验证码ID(注册/登录/找回密码场景必填)
Captcha string `json:"captcha"` // 图形验证码(注册/登录/找回密码场景必填)
}
// SendEmailCode 发送邮箱验证码(支持多种场景)
// type=register: 注册,检查邮箱未被注册
// type=login: 登录,检查邮箱已注册且有密码
// type=bind/空: 已登录状态下绑定邮箱,不检查邮箱状态
// type=reset_password: 找回密码,检查邮箱已注册且有密码
// type=change_email: 修改邮箱(已登录),检查新邮箱未被其他用户使用
func (h *userHandler) SendEmailCode(c *gin.Context) {
var req SendEmailCodeReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "请输入有效的邮箱地址")
return
}
ctx := context.Background()
// 1. 获取客户端IP
clientIP := c.ClientIP()
if clientIP == "" {
clientIP = c.GetHeader("X-Forwarded-For")
}
if clientIP == "" {
clientIP = c.GetHeader("X-Real-IP")
}
switch req.Type {
case "register":
// 注册场景:检查邮箱未被注册,需要验证图形验证码
// 验证图形验证码
if req.CaptchaId == "" || req.Captcha == "" {
utils.Fail(c, "请输入图形验证码")
return
}
if !store.Verify(req.CaptchaId, req.Captcha, true) {
utils.Fail(c, "图形验证码错误")
return
}
var user models.User
err := h.db.Where("email = ?", req.Email).First(&user).Error
if err == nil {
utils.Fail(c, "该邮箱已被注册")
return
}
case "login":
// 登录场景:检查邮箱已注册且有密码,需要验证图形验证码
// 验证图形验证码
if req.CaptchaId == "" || req.Captcha == "" {
utils.Fail(c, "请输入图形验证码")
return
}
if !store.Verify(req.CaptchaId, req.Captcha, true) {
utils.Fail(c, "图形验证码错误")
return
}
var user models.User
err := h.db.Where("email = ?", req.Email).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.Fail(c, "该邮箱未注册")
} else {
utils.Fail(c, "查询用户失败")
}
return
}
if user.Password == "" {
utils.Fail(c, "该邮箱未设置密码,请先注册")
return
}
case "reset_password":
// 找回密码场景:检查邮箱已注册,需要验证图形验证码
// 验证图形验证码
if req.CaptchaId == "" || req.Captcha == "" {
utils.Fail(c, "请输入图形验证码")
return
}
if !store.Verify(req.CaptchaId, req.Captcha, true) {
utils.Fail(c, "图形验证码错误")
return
}
var user models.User
err := h.db.Where("email = ?", req.Email).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.Fail(c, "该邮箱未注册")
} else {
utils.Fail(c, "查询用户失败")
}
return
}
if user.Password == "" {
utils.Fail(c, "该邮箱未设置密码,请先注册")
return
}
case "change_email":
// 修改邮箱场景:需要用户已登录,检查新邮箱未被其他用户使用,需要验证图形验证码
userId, exists := c.Get("userID")
if !exists {
utils.Fail(c, "用户未登录")
return
}
// 验证图形验证码
if req.CaptchaId == "" || req.Captcha == "" {
utils.Fail(c, "请输入图形验证码")
return
}
if !store.Verify(req.CaptchaId, req.Captcha, true) {
utils.Fail(c, "图形验证码错误")
return
}
// 检查新邮箱是否已被其他用户使用
var existingUser models.User
err := h.db.Where("email = ? AND id != ?", req.Email, userId).First(&existingUser).Error
if err == nil {
utils.Fail(c, "该邮箱已被其他用户使用")
return
}
case "bind", "":
// 绑定邮箱场景:需要用户已登录,不检查邮箱状态,不需要图形验证码
_, exists := c.Get("userID")
if !exists {
utils.Fail(c, "用户未登录")
return
}
default:
utils.Fail(c, "无效的type参数")
return
}
// 2. IP级别的频率限制:同一IP每分钟最多发送5次
ipRateLimitKey := "email_ip_rate_limit:" + clientIP
ipCount, err := global.Redis.Incr(ctx, ipRateLimitKey).Result()
if err != nil {
global.Logger.Error("IP频率限制计数失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "发送验证码失败")
return
}
if ipCount == 1 {
global.Redis.Expire(ctx, ipRateLimitKey, 60*time.Second)
} else if ipCount > 5 {
ttl, _ := global.Redis.TTL(ctx, ipRateLimitKey).Result()
utils.Fail(c, fmt.Sprintf("当前IP发送过于频繁,请%d秒后再试", int(ttl.Seconds())))
return
}
// 3. 邮箱级别的频率限制:同一邮箱每60秒只能发送一次
emailRateLimitKey := "email_rate_limit:" + req.Email
emailCount, err := global.Redis.Exists(ctx, emailRateLimitKey).Result()
if err == nil && emailCount > 0 {
ttl, _ := global.Redis.TTL(ctx, emailRateLimitKey).Result()
utils.Fail(c, fmt.Sprintf("该邮箱发送太频繁,请%d秒后再试", int(ttl.Seconds())))
return
}
// 4. 邮箱每日发送次数限制:同一邮箱每天最多发送10次
now := time.Now()
dayKey := "email_daily_limit:" + req.Email + ":" + now.Format("2006-01-02")
dailyCount, err := global.Redis.Incr(ctx, dayKey).Result()
if err != nil {
global.Logger.Error("邮箱每日限制计数失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "发送验证码失败")
return
}
if dailyCount == 1 {
global.Redis.Expire(ctx, dayKey, 24*time.Hour)
} else if dailyCount > 10 {
utils.Fail(c, "该邮箱今日发送次数已达上限,请明天再试")
return
}
// 5. IP每日发送次数限制:同一IP每天最多发送30次
ipDayKey := "email_ip_daily_limit:" + clientIP + ":" + now.Format("2006-01-02")
ipDailyCount, err := global.Redis.Incr(ctx, ipDayKey).Result()
if err != nil {
global.Logger.Error("IP每日限制计数失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "发送验证码失败")
return
}
if ipDailyCount == 1 {
global.Redis.Expire(ctx, ipDayKey, 24*time.Hour)
} else if ipDailyCount > 30 {
utils.Fail(c, "当前IP今日发送次数已达上限,请明天再试")
return
}
code := utils.GenerateRandomString(6)
err = global.Redis.Set(ctx, "email_code:"+req.Email, code, 5*time.Minute).Err()
if err != nil {
global.Logger.Error("保存邮箱验证码失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "发送验证码失败")
return
}
global.Redis.Set(ctx, emailRateLimitKey, "1", 60*time.Second)
err = utils.SendEmailCode(req.Email, code)
if err != nil {
global.Logger.Error("发送邮件失败", append(utils.LogContextFields(c), zap.String("email", req.Email), zap.Error(err))...)
global.Redis.Del(ctx, "email_code:"+req.Email)
utils.Fail(c, "发送验证码失败")
return
}
global.Logger.Info("验证码发送成功", append(utils.LogContextFields(c), zap.String("email", req.Email), zap.String("code", code))...)
utils.Ok(c, "验证码已发送,请注意查收")
}
// SetEmailReq 设置邮箱请求
type SetEmailReq struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required,len=6"`
}
// SetEmail 设置邮箱(需验证验证码)
func (h *userHandler) SetEmail(c *gin.Context) {
userId, exists := c.Get("userID")
if !exists {
utils.Fail(c, "用户未登录")
return
}
var req SetEmailReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "请输入有效的邮箱和验证码")
return
}
ctx := context.Background()
storedCode, err := global.Redis.Get(ctx, "email_code:"+req.Email).Result()
if err != nil || storedCode != req.Code {
utils.Fail(c, "验证码错误或已过期")
return
}
global.Redis.Del(ctx, "email_code:"+req.Email)
var existingUser models.User
err = h.db.Where("email = ? AND id != ?", req.Email, userId).First(&existingUser).Error
if err == nil {
utils.Fail(c, "该邮箱已被其他用户使用")
return
}
err = h.db.Model(&models.User{}).Where("id = ?", userId).Update("email", req.Email).Error
if err != nil {
global.Logger.Error("更新邮箱失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "设置邮箱失败")
return
}
utils.Ok(c, "邮箱设置成功")
}
// -------------------------- 5. 网页端邮箱登录接口 --------------------------
// EmailRegisterReq 邮箱注册请求
type EmailRegisterReq struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Code string `json:"code" binding:"required,len=6"`
CaptchaId string `json:"captchaId"` // 图形验证码ID
Captcha string `json:"captcha"` // 图形验证码
}
// EmailLoginReq 邮箱登录请求
type EmailLoginReq struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
CaptchaId string `json:"captchaId"` // 图形验证码ID
Captcha string `json:"captcha"` // 图形验证码
}
// EmailRegister 邮箱注册
func (h *userHandler) EmailRegister(c *gin.Context) {
var req EmailRegisterReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "请输入有效的邮箱和密码")
return
}
// 注:图形验证码在发送邮箱验证码时已验证,此处不再重复验证
// 验证邮箱验证码(必填)
ctx := context.Background()
storedCode, err := global.Redis.Get(ctx, "email_code:"+req.Email).Result()
if err != nil || storedCode != req.Code {
utils.Fail(c, "邮箱验证码错误或已过期")
return
}
global.Redis.Del(ctx, "email_code:"+req.Email)
var existingUser models.User
err = h.db.Where("email = ?", req.Email).First(&existingUser).Error
if err == nil {
utils.Fail(c, "该邮箱已被注册")
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
global.Logger.Error("密码加密失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "注册失败")
return
}
userCode := utils.GenerateUserCode()
user := models.User{
Email: req.Email,
Password: string(hashedPassword),
Nickname: "用户" + utils.GenerateRandomString(6),
UserCode: userCode,
}
err = h.db.Create(&user).Error
if err != nil {
global.Logger.Error("创建用户失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "注册失败")
return
}
token, expiresAt, err := utils.GenerateTokenWithExpire(user.ID)
if err != nil {
utils.Fail(c, "token生成失败")
return
}
utils.Ok(c, gin.H{
"token": token,
"expiresAt": expiresAt,
"expiresIn": utils.GetTokenExpireSeconds(),
"nickname": user.Nickname,
"user_code": user.UserCode,
"avatar": user.Avatar,
})
}
// EmailLogin 邮箱登录
func (h *userHandler) EmailLogin(c *gin.Context) {
var req EmailLoginReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "请输入有效的邮箱和密码")
return
}
// 验证图形验证码(必填)
if req.CaptchaId == "" || req.Captcha == "" {
utils.Fail(c, "请输入图形验证码")
return
}
if !store.Verify(req.CaptchaId, req.Captcha, true) {
utils.Fail(c, "图形验证码错误")
return
}
var user models.User
err := h.db.Where("email = ?", req.Email).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.Fail(c, "邮箱或密码错误")
} else {
utils.Fail(c, "登录失败")
}
return
}
if user.Password == "" {
utils.Fail(c, "该邮箱未设置密码,请先注册")
return
}
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
if err != nil {
utils.Fail(c, "邮箱或密码错误")
return
}
token, expiresAt, err := utils.GenerateTokenWithExpire(user.ID)
if err != nil {
utils.Fail(c, "登录失败")
return
}
utils.Ok(c, gin.H{
"token": token,
"expiresAt": expiresAt,
"expiresIn": utils.GetTokenExpireSeconds(),
"nickname": user.Nickname,
"avatar": user.Avatar,
"email": user.Email,
})
}
// CheckEmailExistsReq 检查邮箱是否存在请求
type CheckEmailExistsReq struct {
Email string `json:"email" binding:"required,email"`
}
// CheckEmailExists 检查邮箱是否已注册
func (h *userHandler) CheckEmailExists(c *gin.Context) {
var req CheckEmailExistsReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "请输入有效的邮箱地址")
return
}
var user models.User
err := h.db.Where("email = ?", req.Email).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.Ok(c, gin.H{
"exists": false,
"hasPassword": false,
})
return
}
utils.Fail(c, "查询失败")
return
}
utils.Ok(c, gin.H{
"exists": true,
"hasPassword": user.Password != "",
})
}
// ResetPasswordReq 重置密码请求(未登录找回密码场景)
type ResetPasswordReq struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
ConfirmPassword string `json:"confirmPassword"` // 确认密码(可选,前端已验证)
Code string `json:"code" binding:"required,len=6"` // 邮箱验证码(必填)
}
// ResetPassword 重置密码(未登录找回密码场景)
func (h *userHandler) ResetPassword(c *gin.Context) {
var req ResetPasswordReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "请输入有效的邮箱、密码和验证码")
return
}
// 验证密码一致性(如果前端传递了 confirmPassword
if req.ConfirmPassword != "" && req.Password != req.ConfirmPassword {
utils.Fail(c, "两次输入的密码不一致")
return
}
ctx := context.Background()
storedCode, err := global.Redis.Get(ctx, "email_code:"+req.Email).Result()
if err != nil || storedCode != req.Code {
utils.Fail(c, "验证码错误或已过期")
return
}
global.Redis.Del(ctx, "email_code:"+req.Email)
var user models.User
err = h.db.Where("email = ?", req.Email).First(&user).Error
if err != nil {
utils.Fail(c, "该邮箱未注册")
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
global.Logger.Error("密码加密失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "重置密码失败")
return
}
err = h.db.Model(&user).Update("password", string(hashedPassword)).Error
if err != nil {
global.Logger.Error("更新密码失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "重置密码失败")
return
}
utils.Ok(c, "密码重置成功")
}
// ChangePasswordReq 修改密码请求(已登录场景)
type ChangePasswordReq struct {
Password string `json:"password" binding:"required,min=6"` // 新密码
ConfirmPassword string `json:"confirmPassword" binding:"required"` // 确认密码
NewEmail string `json:"newEmail" binding:"omitempty,email"` // 新邮箱(可选)
CaptchaId string `json:"captchaId" binding:"required"` // 图形验证码ID
Captcha string `json:"captcha" binding:"required"` // 图形验证码
Code string `json:"code" binding:"omitempty,len=6"` // 邮箱验证码(修改邮箱时必填)
}
// ChangePassword 修改密码(已登录场景)
// 支持两种场景:
// 1. 只修改密码(不修改邮箱):只需图形验证码
// 2. 修改邮箱+密码:需要新邮箱的验证码
func (h *userHandler) ChangePassword(c *gin.Context) {
userId, exists := c.Get("userID")
if !exists {
utils.Fail(c, "用户未登录")
return
}
var req ChangePasswordReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "请输入有效的参数")
return
}
// 1. 验证图形验证码(必填)
if !store.Verify(req.CaptchaId, req.Captcha, true) {
utils.Fail(c, "图形验证码错误")
return
}
// 2. 验证密码一致性
if req.Password != req.ConfirmPassword {
utils.Fail(c, "两次输入的密码不一致")
return
}
ctx := context.Background()
// 3. 如果提供了新邮箱,需要验证邮箱验证码
if req.NewEmail != "" {
if req.Code == "" {
utils.Fail(c, "修改邮箱需要邮箱验证码")
return
}
storedCode, err := global.Redis.Get(ctx, "email_code:"+req.NewEmail).Result()
if err != nil || storedCode != req.Code {
utils.Fail(c, "邮箱验证码错误或已过期")
return
}
global.Redis.Del(ctx, "email_code:"+req.NewEmail)
// 检查新邮箱是否已被其他用户使用
var existingUser models.User
err = h.db.Where("email = ? AND id != ?", req.NewEmail, userId).First(&existingUser).Error
if err == nil {
utils.Fail(c, "该邮箱已被其他用户使用")
return
}
}
// 4. 更新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
global.Logger.Error("密码加密失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "修改密码失败")
return
}
// 构建更新数据
updateData := map[string]interface{}{
"password": string(hashedPassword),
}
// 如果提供了新邮箱,同时更新邮箱
if req.NewEmail != "" {
updateData["email"] = req.NewEmail
}
err = h.db.Model(&models.User{}).Where("id = ?", userId).Updates(updateData).Error
if err != nil {
global.Logger.Error("更新用户信息失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "修改失败")
return
}
// 5. 返回成功消息
if req.NewEmail != "" {
utils.Ok(c, "邮箱和密码修改成功")
} else {
utils.Ok(c, "密码修改成功")
}
}
// -------------------------- 6. 微信绑定接口 --------------------------
// BindWechatReq 绑定微信请求(Web端已登录)
type BindWechatReq struct {
Code string `json:"code" binding:"required"` // 微信网页授权code
}
// BindWechat 绑定微信(已登录场景,Web端使用)
// 流程:用户在Web端已登录 → 扫描微信二维码 → 微信回传code → 调用此接口绑定openid
func (h *userHandler) BindWechat(c *gin.Context) {
userId, exists := c.Get("userID")
if !exists {
utils.Fail(c, "用户未登录")
return
}
var req BindWechatReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "参数解析失败")
return
}
if req.Code == "" {
utils.Fail(c, "授权code不能为空")
return
}
// 调用微信网页授权接口获取 openid
wxResp, err := utils.WebCode2Session(req.Code)
if err != nil {
global.Logger.Error("微信网页授权失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "微信授权失败:"+err.Error())
return
}
if wxResp.OpenID == "" {
utils.Fail(c, "获取微信用户信息失败")
return
}
// 检查该 openid 是否已被其他用户绑定
var existingUser models.User
err = h.db.Where("openid = ? AND id != ?", wxResp.OpenID, userId).First(&existingUser).Error
if err == nil {
utils.Fail(c, "该微信账号已被其他用户绑定")
return
}
// 检查当前用户是否已绑定微信
var currentUser models.User
err = h.db.First(&currentUser, userId).Error
if err != nil {
utils.Fail(c, "用户不存在")
return
}
if currentUser.Openid != "" {
utils.Fail(c, "您已绑定微信,请先解绑")
return
}
// 绑定微信 openid 到当前用户
err = h.db.Model(&models.User{}).Where("id = ?", userId).Update("openid", wxResp.OpenID).Error
if err != nil {
global.Logger.Error("绑定微信失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "绑定微信失败")
return
}
global.Logger.Info("微信绑定成功", append(utils.LogContextFields(c),
zap.Uint("userID", userId.(uint)),
zap.String("openid", wxResp.OpenID),
)...)
utils.Ok(c, "微信绑定成功")
}
// -------------------------- 7. 小程序微信绑定接口 --------------------------
// BindWechatMiniReq 绑定微信请求(小程序端已登录)
type BindWechatMiniReq struct {
Code string `json:"code" binding:"required"` // 小程序 wx.login() 返回的临时code
}
// BindWechatMini 小程序绑定微信(已登录场景)
// 流程:小程序用户用邮箱注册并登录 → 点击"绑定微信" → wx.login() 获取code → 调用此接口绑定openid
//
// 与 Web 端 BindWechat 的关键区别:
//
// - Web 端:code 来自微信网页OAuth2.0扫码授权 → 调用 sns/oauth2/access_token 换取 → WebCode2Session
//
// - 小程序端:code 来自 wx.login() → 调用 sns/jscode2session 换取 → MiniProgramCode2Session
//
// 两者 code 完全不同,API 端点不同,不可混用!
func (h *userHandler) BindWechatMini(c *gin.Context) {
userId, exists := c.Get("userID")
if !exists {
utils.Fail(c, "用户未登录")
return
}
var req BindWechatMiniReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "参数解析失败")
return
}
if req.Code == "" {
utils.Fail(c, "授权code不能为空,请先调用wx.login获取")
return
}
// 调用小程序专用接口:sns/jscode2session
wxResp, err := utils.MiniProgramCode2Session(req.Code)
if err != nil {
global.Logger.Error("小程序微信授权失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "微信授权失败,请稍后重试")
return
}
if wxResp.OpenID == "" {
utils.Fail(c, "获取微信用户信息失败")
return
}
// 检查该 openid 是否已被其他用户绑定
var existingUser models.User
err = h.db.Where("openid = ? AND id != ?", wxResp.OpenID, userId).First(&existingUser).Error
if err == nil {
utils.Fail(c, "该微信账号已被其他用户绑定")
return
}
// 检查当前用户是否已绑定微信
var currentUser models.User
err = h.db.First(&currentUser, userId).Error
if err != nil {
utils.Fail(c, "用户不存在")
return
}
if currentUser.Openid != "" {
utils.Fail(c, "您已绑定微信,请先解绑")
return
}
// 更新 openid 和相关字段
updates := map[string]interface{}{
"openid": wxResp.OpenID,
}
// 若存在 unionid 一并保存(跨平台识别用)
if wxResp.UnionID != "" {
updates["unionid"] = wxResp.UnionID
}
err = h.db.Model(&models.User{}).Where("id = ?", userId).Updates(updates).Error
if err != nil {
global.Logger.Error("小程序绑定微信失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "绑定微信失败,请稍后重试")
return
}
global.Logger.Info("小程序微信绑定成功", append(utils.LogContextFields(c),
zap.Uint("userID", userId.(uint)),
zap.String("openid", wxResp.OpenID),
)...)
utils.Ok(c, "微信绑定成功")
}