docs(website): 添加简记memo产品原型和UI设计规范文档

- 新增「简记memo」一体化小程序产品原型设计文档
- 新增简记memo完整版UI视觉设计规范和界面细节
- 添加IDEA项目配置文件.gitignore
- 创建404页面HTML文件,包含响应式布局和错误提示
- 添加关于页面HTML文件,展示品牌介绍和团队信息
- 实现AES加解密工具函数,支持请求体加密
- 添加用户协议页面基础框架
This commit is contained in:
sunct
2026-07-31 14:12:32 +08:00
commit 3c3bf53ae4
115 changed files with 21304 additions and 0 deletions
+982
View File
@@ -0,0 +1,982 @@
// 小程序用户接口
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, "微信绑定成功")
}