feat: PhotoWall 毕业照存储系统初始版本
后端: Go(Gin+GORM+MySQL+Redis) - 17张业务表(yz_pw_前缀), 自动迁移 - JWT认证(3小时) + 盐+MD5密码 + 图形验证码 - 班级CRUD/加入(8人姓名验证/邀请码)/审核/30天自动清理 - 系统配置(16项)/菜单管理(动态路由)/数据统计 - 文件MD5去重/数据隔离/账号封禁/敏感词DFA检测 前端: Vue3+Vite+Element Plus+Less+ECharts+FontAwesome - 动态路由(数据库菜单驱动) - 蓝白配色, H5响应式 - 登录/注册/忘记密码/个人中心 - 班级创建向导/详情/加入/列表 - 管理后台: 审核/配置/菜单/统计/用户/敏感词 数据库: MySQL 10.31.100.3:3306/photowall 管理员: hero920103 / 920103
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"photowall/internal/config"
|
||||
"photowall/internal/model"
|
||||
"photowall/pkg/captcha"
|
||||
"photowall/pkg/hash"
|
||||
"photowall/pkg/jwt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
db *gorm.DB
|
||||
jm *jwt.Manager
|
||||
}
|
||||
|
||||
func NewAuthService(db *gorm.DB, jm *jwt.Manager) *AuthService {
|
||||
return &AuthService{db: db, jm: jm}
|
||||
}
|
||||
|
||||
// ============ 请求结构 ============
|
||||
|
||||
type RegisterReq struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=32"`
|
||||
Password string `json:"password" binding:"required,min=6,max=64"`
|
||||
Nickname string `json:"nickname"`
|
||||
Email string `json:"email"`
|
||||
CaptchaID string `json:"captcha_id" binding:"required"`
|
||||
CaptchaCode string `json:"captcha_code" binding:"required"`
|
||||
}
|
||||
|
||||
type LoginReq struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
LoginType string `json:"login_type"` // password / sms
|
||||
Phone string `json:"phone"`
|
||||
SmsCode string `json:"sms_code"`
|
||||
CaptchaID string `json:"captcha_id"`
|
||||
CaptchaCode string `json:"captcha_code"`
|
||||
}
|
||||
|
||||
type LoginResp struct {
|
||||
Token string `json:"token"`
|
||||
User model.User `json:"user"`
|
||||
ExpireIn int `json:"expire_in"`
|
||||
}
|
||||
|
||||
type ForgotPasswordReq struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email" binding:"required"`
|
||||
}
|
||||
|
||||
type ResetPasswordReq struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
ResetCode string `json:"reset_code" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6,max=64"`
|
||||
}
|
||||
|
||||
type SendSmsReq struct {
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Scene string `json:"scene" binding:"required"` // login / register / reset_password
|
||||
}
|
||||
|
||||
// ============ 注册 ============
|
||||
|
||||
func (s *AuthService) Register(req *RegisterReq) (*model.User, error) {
|
||||
// 校验图形验证码
|
||||
if !captcha.Verify(req.CaptchaID, req.CaptchaCode) {
|
||||
return nil, errors.New("验证码错误或已过期")
|
||||
}
|
||||
var count int64
|
||||
if err := s.db.Model(&model.User{}).Where("username = ?", req.Username).Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, errors.New("用户名已被注册")
|
||||
}
|
||||
salt, pwdHash := hash.Password(req.Password)
|
||||
nickname := req.Nickname
|
||||
if nickname == "" {
|
||||
nickname = req.Username
|
||||
}
|
||||
user := &model.User{
|
||||
Username: req.Username,
|
||||
PasswordHash: pwdHash,
|
||||
Salt: salt,
|
||||
Email: req.Email,
|
||||
Nickname: nickname,
|
||||
Role: "user",
|
||||
}
|
||||
if err := s.db.Create(user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ============ 登录 ============
|
||||
|
||||
func (s *AuthService) Login(req *LoginReq) (*LoginResp, error) {
|
||||
// 图形验证码校验(密码登录必须,短信登录可选)
|
||||
if req.LoginType != "sms" {
|
||||
if !captcha.Verify(req.CaptchaID, req.CaptchaCode) {
|
||||
return nil, errors.New("验证码错误或已过期")
|
||||
}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if req.LoginType == "sms" {
|
||||
// 短信验证码登录
|
||||
if req.Phone == "" || req.SmsCode == "" {
|
||||
return nil, errors.New("手机号和验证码不能为空")
|
||||
}
|
||||
// 校验短信验证码
|
||||
var sms model.SmsCode
|
||||
if err := s.db.Where("phone = ? AND scene = ? AND used = ?", req.Phone, "login", false).
|
||||
Order("id DESC").First(&sms).Error; err != nil {
|
||||
return nil, errors.New("验证码错误或已过期")
|
||||
}
|
||||
if sms.Code != req.SmsCode || time.Now().After(sms.ExpireAt) {
|
||||
return nil, errors.New("验证码错误或已过期")
|
||||
}
|
||||
sms.Used = true
|
||||
s.db.Save(&sms)
|
||||
// 查找或创建用户
|
||||
if err := s.db.Where("username = ?", req.Phone).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 自动注册
|
||||
salt, pwdHash := hash.Password(randomCode(8))
|
||||
user = model.User{
|
||||
Username: req.Phone,
|
||||
PasswordHash: pwdHash,
|
||||
Salt: salt,
|
||||
Nickname: "手机用户" + req.Phone[len(req.Phone)-4:],
|
||||
Role: "user",
|
||||
}
|
||||
s.db.Create(&user)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 账号密码登录
|
||||
if req.Username == "" || req.Password == "" {
|
||||
return nil, errors.New("用户名和密码不能为空")
|
||||
}
|
||||
if err := s.db.Where("username = ?", req.Username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if !hash.Verify(user.PasswordHash, user.Salt, req.Password) {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查账号状态
|
||||
if user.Status == "banned" {
|
||||
return nil, fmt.Errorf("账号已被封禁:%s", user.BanReason)
|
||||
}
|
||||
|
||||
token, err := s.jm.Generate(user.ID, user.Username, user.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &LoginResp{Token: token, User: user, ExpireIn: s.jm.ExpireHrs}, nil
|
||||
}
|
||||
|
||||
// ============ 忘记密码 ============
|
||||
|
||||
func (s *AuthService) ForgotPassword(req *ForgotPasswordReq) (string, error) {
|
||||
var user model.User
|
||||
if err := s.db.Where("username = ? AND email = ?", req.Username, req.Email).First(&user).Error; err != nil {
|
||||
return "", errors.New("用户名与邮箱不匹配")
|
||||
}
|
||||
// 生成6位重置码
|
||||
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
||||
reset := &model.PasswordReset{
|
||||
UserID: user.ID,
|
||||
Code: code,
|
||||
ExpireAt: time.Now().Add(30 * time.Minute),
|
||||
}
|
||||
if err := s.db.Create(reset).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 开发阶段:重置码直接返回(生产环境应发送邮件)
|
||||
// TODO: 接入邮件服务后,通过邮箱发送重置码
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// ============ 重置密码 ============
|
||||
|
||||
func (s *AuthService) ResetPassword(req *ResetPasswordReq) error {
|
||||
var user model.User
|
||||
if err := s.db.Where("username = ?", req.Username).First(&user).Error; err != nil {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
var reset model.PasswordReset
|
||||
if err := s.db.Where("user_id = ? AND code = ? AND used = ?", user.ID, req.ResetCode, false).
|
||||
Order("id DESC").First(&reset).Error; err != nil {
|
||||
return errors.New("重置码错误或已过期")
|
||||
}
|
||||
if time.Now().After(reset.ExpireAt) {
|
||||
return errors.New("重置码已过期")
|
||||
}
|
||||
salt, pwdHash := hash.Password(req.NewPassword)
|
||||
user.PasswordHash = pwdHash
|
||||
user.Salt = salt
|
||||
reset.Used = true
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(&reset).Error
|
||||
})
|
||||
}
|
||||
|
||||
// ============ 发送短信验证码(预留) ============
|
||||
|
||||
func (s *AuthService) SendSmsCode(req *SendSmsReq) (string, error) {
|
||||
cfg := config.C
|
||||
// 参数未配置时返回提示(开发阶段直接返回验证码)
|
||||
if cfg.SMSAccessKey == "" || cfg.SMSSecretKey == "" {
|
||||
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
||||
sms := &model.SmsCode{
|
||||
Phone: req.Phone,
|
||||
Code: code,
|
||||
Scene: req.Scene,
|
||||
ExpireAt: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
s.db.Create(sms)
|
||||
// 开发阶段直接返回验证码(生产环境应通过短信服务商发送)
|
||||
return code, nil
|
||||
}
|
||||
// TODO: 接入阿里云/腾讯云短信SDK后发送真实短信
|
||||
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
||||
sms := &model.SmsCode{
|
||||
Phone: req.Phone,
|
||||
Code: code,
|
||||
Scene: req.Scene,
|
||||
ExpireAt: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
s.db.Create(sms)
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// ============ 微信扫码登录(预留) ============
|
||||
|
||||
type WechatLoginReq struct {
|
||||
Code string `json:"code"` // 微信授权code
|
||||
}
|
||||
|
||||
func (s *AuthService) WechatLogin(req *WechatLoginReq) (*LoginResp, error) {
|
||||
cfg := config.C
|
||||
if cfg.WechatAppID == "" || cfg.WechatAppSecret == "" {
|
||||
return nil, errors.New("微信登录未配置,请联系管理员")
|
||||
}
|
||||
// TODO: 接入微信开放平台API
|
||||
// 1. 用 code 换取 access_token 和 openid
|
||||
// 2. 用 openid 查找或创建用户
|
||||
// 3. 签发 JWT
|
||||
return nil, errors.New("微信登录功能开发中")
|
||||
}
|
||||
|
||||
// ============ 工具 ============
|
||||
|
||||
func randomCode(length int) string {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = chars[rand.Intn(len(chars))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user