修复登录及验证码功能
This commit is contained in:
@@ -5,7 +5,8 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Config 全局配置,优先从环境变量读取,提供默认值以便开箱即用。
|
||||
// Config 全局基础配置(仅保留服务启动及基础设施参数)
|
||||
// 业务参数(极验、短信、微信、审核时限等)已统一移入 MySQL 数据库 yz_pw_system_configs 表管理
|
||||
type Config struct {
|
||||
Port string
|
||||
JWTSecret string
|
||||
@@ -23,24 +24,6 @@ type Config struct {
|
||||
// 平台管理员账号(首次启动自动创建)
|
||||
AdminUsername string
|
||||
AdminPassword string
|
||||
// 班级审核补齐期限(天),逾期自动删除
|
||||
AuditGraceDays int
|
||||
|
||||
// 极验行为验证(参数后期补,为空则跳过极验校验)
|
||||
GeeTestID string
|
||||
GeeTestKey string
|
||||
|
||||
// 短信验证码(参数后期补,为空则短信接口返回配置缺失)
|
||||
SMSProvider string // aliyun / tencent
|
||||
SMSAccessKey string
|
||||
SMSSecretKey string
|
||||
SMSSignName string
|
||||
SMSTemplateCode string
|
||||
|
||||
// 微信扫码登录(参数后期补,为空则微信接口返回配置缺失)
|
||||
WechatAppID string
|
||||
WechatAppSecret string
|
||||
WechatRedirectURI string
|
||||
|
||||
// Redis(缓存/验证码/限流,连接失败自动降级内存)
|
||||
RedisHost string
|
||||
@@ -53,32 +36,20 @@ var C *Config
|
||||
|
||||
func Load() *Config {
|
||||
C = &Config{
|
||||
Port: getEnv("PORT", "8010"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "photowall-dev-secret-change-in-prod"),
|
||||
JWTExpireHrs: getEnvInt("JWT_EXPIRE_HOURS", 3),
|
||||
UploadDir: getEnv("UPLOAD_DIR", "uploads"),
|
||||
DBHost: getEnv("DB_HOST", "10.31.100.3"),
|
||||
DBPort: getEnv("DB_PORT", "3306"),
|
||||
DBUser: getEnv("DB_USER", "photowall"),
|
||||
DBPassword: getEnv("DB_PASSWORD", "Dfn47yeKpyJfwz8n"),
|
||||
DBName: getEnv("DB_NAME", "photowall"),
|
||||
DBCharset: getEnv("DB_CHARSET", "utf8mb4"),
|
||||
AdminUsername: getEnv("ADMIN_USERNAME", "hero920103"),
|
||||
AdminPassword: getEnv("ADMIN_PASSWORD", "920103"),
|
||||
AuditGraceDays: getEnvInt("AUDIT_GRACE_DAYS", 30),
|
||||
// 极验(后期补参数)
|
||||
GeeTestID: getEnv("GEETEST_ID", ""),
|
||||
GeeTestKey: getEnv("GEETEST_KEY", ""),
|
||||
// 短信(后期补参数)
|
||||
SMSProvider: getEnv("SMS_PROVIDER", "aliyun"),
|
||||
SMSAccessKey: getEnv("SMS_ACCESS_KEY", ""),
|
||||
SMSSecretKey: getEnv("SMS_SECRET_KEY", ""),
|
||||
SMSSignName: getEnv("SMS_SIGN_NAME", ""),
|
||||
SMSTemplateCode: getEnv("SMS_TEMPLATE_CODE", ""),
|
||||
// 微信扫码登录(后期补参数)
|
||||
WechatAppID: getEnv("WECHAT_APP_ID", ""),
|
||||
WechatAppSecret: getEnv("WECHAT_APP_SECRET", ""),
|
||||
WechatRedirectURI: getEnv("WECHAT_REDIRECT_URI", ""),
|
||||
Port: getEnv("PORT", "8010"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "photowall-dev-secret-change-in-prod"),
|
||||
JWTExpireHrs: getEnvInt("JWT_EXPIRE_HOURS", 3),
|
||||
UploadDir: getEnv("UPLOAD_DIR", "uploads"),
|
||||
DBHost: getEnv("DB_HOST", "212.64.112.158"),
|
||||
DBPort: getEnv("DB_PORT", "3388"),
|
||||
// DBHost: getEnv("DB_HOST", "10.31.100.3"),
|
||||
// DBPort: getEnv("DB_PORT", "3306"),
|
||||
DBUser: getEnv("DB_USER", "photowall"),
|
||||
DBPassword: getEnv("DB_PASSWORD", "Dfn47yeKpyJfwz8n"),
|
||||
DBName: getEnv("DB_NAME", "photowall"),
|
||||
DBCharset: getEnv("DB_CHARSET", "utf8mb4"),
|
||||
AdminUsername: getEnv("ADMIN_USERNAME", "hero920103"),
|
||||
AdminPassword: getEnv("ADMIN_PASSWORD", "920103"),
|
||||
// Redis
|
||||
RedisHost: getEnv("REDIS_HOST", "localhost"),
|
||||
RedisPort: getEnv("REDIS_PORT", "6379"),
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"photowall/internal/config"
|
||||
"photowall/internal/model"
|
||||
"photowall/pkg/captcha"
|
||||
"photowall/pkg/hash"
|
||||
@@ -15,12 +14,13 @@ import (
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
db *gorm.DB
|
||||
jm *jwt.Manager
|
||||
db *gorm.DB
|
||||
jm *jwt.Manager
|
||||
configSvc *ConfigService
|
||||
}
|
||||
|
||||
func NewAuthService(db *gorm.DB, jm *jwt.Manager) *AuthService {
|
||||
return &AuthService{db: db, jm: jm}
|
||||
func NewAuthService(db *gorm.DB, jm *jwt.Manager, configSvc *ConfigService) *AuthService {
|
||||
return &AuthService{db: db, jm: jm, configSvc: configSvc}
|
||||
}
|
||||
|
||||
// ============ 请求结构 ============
|
||||
@@ -223,9 +223,12 @@ func (s *AuthService) ResetPassword(req *ResetPasswordReq) error {
|
||||
// ============ 发送短信验证码(预留) ============
|
||||
|
||||
func (s *AuthService) SendSmsCode(req *SendSmsReq) (string, error) {
|
||||
cfg := config.C
|
||||
var accessKey, secretKey string
|
||||
if s.configSvc != nil {
|
||||
_, accessKey, secretKey, _, _ = s.configSvc.GetSmsConfig()
|
||||
}
|
||||
// 参数未配置时返回提示(开发阶段直接返回验证码)
|
||||
if cfg.SMSAccessKey == "" || cfg.SMSSecretKey == "" {
|
||||
if accessKey == "" || secretKey == "" {
|
||||
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
||||
sms := &model.SmsCode{
|
||||
Phone: req.Phone,
|
||||
@@ -256,8 +259,11 @@ type WechatLoginReq struct {
|
||||
}
|
||||
|
||||
func (s *AuthService) WechatLogin(req *WechatLoginReq) (*LoginResp, error) {
|
||||
cfg := config.C
|
||||
if cfg.WechatAppID == "" || cfg.WechatAppSecret == "" {
|
||||
var appID, appSecret string
|
||||
if s.configSvc != nil {
|
||||
appID, appSecret, _ = s.configSvc.GetWechatConfig()
|
||||
}
|
||||
if appID == "" || appSecret == "" {
|
||||
return nil, errors.New("微信登录未配置,请联系管理员")
|
||||
}
|
||||
// TODO: 接入微信开放平台API
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"photowall/internal/config"
|
||||
"photowall/internal/model"
|
||||
"photowall/pkg/sensitive"
|
||||
"strings"
|
||||
@@ -14,11 +13,12 @@ import (
|
||||
)
|
||||
|
||||
type ClassService struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
configSvc *ConfigService
|
||||
}
|
||||
|
||||
func NewClassService(db *gorm.DB) *ClassService {
|
||||
return &ClassService{db: db}
|
||||
func NewClassService(db *gorm.DB, configSvc *ConfigService) *ClassService {
|
||||
return &ClassService{db: db, configSvc: configSvc}
|
||||
}
|
||||
|
||||
// CreateClassReq 创建班级请求
|
||||
@@ -104,6 +104,10 @@ func (s *ClassService) Create(userID uint, req *CreateClassReq) (*model.Class, e
|
||||
teachers := cleanNames(req.TeacherNames)
|
||||
|
||||
// 6. 构建班级
|
||||
graceDays := 30
|
||||
if s.configSvc != nil {
|
||||
graceDays = s.configSvc.GetInt("audit_grace_days", 30)
|
||||
}
|
||||
class := &model.Class{
|
||||
SchoolID: req.SchoolID,
|
||||
CollegeID: req.CollegeID,
|
||||
@@ -113,7 +117,7 @@ func (s *ClassService) Create(userID uint, req *CreateClassReq) (*model.Class, e
|
||||
GraduateYear: req.GraduateYear,
|
||||
AdminUserID: userID,
|
||||
GraduationPhoto: req.GraduationPhoto,
|
||||
AuditDeadline: time.Now().AddDate(0, 0, config.C.AuditGraceDays),
|
||||
AuditDeadline: time.Now().AddDate(0, 0, graceDays),
|
||||
}
|
||||
class.SetStudentNames(students)
|
||||
class.SetTeacherNames(teachers)
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"photowall/internal/model"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConfigService struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
cache map[string]string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewConfigService(db *gorm.DB) *ConfigService {
|
||||
return &ConfigService{db: db}
|
||||
return &ConfigService{
|
||||
db: db,
|
||||
cache: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// defaultConfigs 所有可配置项及说明
|
||||
@@ -23,8 +32,8 @@ var defaultConfigs = []model.SystemConfig{
|
||||
{Key: "audit_grace_days", Value: "30", Label: "审核补齐期限(天)", Description: "班级被打回后,超过该天数未补齐则自动删除", Category: "basic", Placeholder: "30", Sort: 4},
|
||||
|
||||
// ===== 极验行为验证 =====
|
||||
{Key: "geetest_id", Value: "", Label: "极验 ID", Description: "极验行为验证的Captcha ID,用于防机器人和暴力破解", Category: "geetest", Placeholder: "如:64a####################", DocLink: "https://www.geetest.com 注册后在应用管理获取", Sort: 1},
|
||||
{Key: "geetest_key", Value: "", Label: "极验 Key", Description: "极验行为验证的私钥,与ID配对使用", Category: "geetest", Placeholder: "如:64a####################", DocLink: "https://www.geetest.com 注册后在应用管理获取", Sort: 2},
|
||||
{Key: "geetest_id", Value: "662e68b7ce24be211fa97a53975c5d96", Label: "极验 ID", Description: "极验行为验证的Captcha ID,用于防机器人和暴力破解", Category: "geetest", Placeholder: "如:64a####################", DocLink: "https://www.geetest.com 注册后在应用管理获取", Sort: 1},
|
||||
{Key: "geetest_key", Value: "2789075bc2b25a04c0f7c5420bfa3d6f", Label: "极验 Key", Description: "极验行为验证的私钥,与ID配对使用", Category: "geetest", Placeholder: "如:64a####################", DocLink: "https://www.geetest.com 注册后在应用管理获取", Sort: 2},
|
||||
|
||||
// ===== 短信验证码 =====
|
||||
{Key: "sms_provider", Value: "aliyun", Label: "短信服务商", Description: "选择短信服务商,目前支持阿里云和腾讯云", Category: "sms", Placeholder: "aliyun 或 tencent", DocLink: "阿里云: https://dysms.console.aliyun.com | 腾讯云: https://console.cloud.tencent.com/sms", Sort: 1},
|
||||
@@ -43,17 +52,29 @@ var defaultConfigs = []model.SystemConfig{
|
||||
{Key: "max_file_size_mb", Value: "10", Label: "单文件大小上限(MB)", Description: "上传图片的最大大小限制", Category: "storage", Placeholder: "10", Sort: 2},
|
||||
}
|
||||
|
||||
// InitDefaults 初始化默认配置(不存在则创建)
|
||||
// InitDefaults 初始化默认配置(不存在则创建,已存在但为空且默认有初始值则补齐)
|
||||
func (s *ConfigService) InitDefaults() error {
|
||||
for _, c := range defaultConfigs {
|
||||
var count int64
|
||||
s.db.Model(&model.SystemConfig{}).Where("`key` = ?", c.Key).Count(&count)
|
||||
if count == 0 {
|
||||
var existing model.SystemConfig
|
||||
err := s.db.Where("`key` = ?", c.Key).First(&existing).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := s.db.Create(&c).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err == nil && existing.Value == "" && c.Value != "" {
|
||||
s.db.Model(&existing).Update("value", c.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// 预热内存缓存
|
||||
var list []model.SystemConfig
|
||||
if err := s.db.Find(&list).Error; err == nil {
|
||||
s.mu.Lock()
|
||||
for _, item := range list {
|
||||
s.cache[item.Key] = item.Value
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,14 +92,49 @@ func (s *ConfigService) GetByCategory(category string) ([]model.SystemConfig, er
|
||||
return list, err
|
||||
}
|
||||
|
||||
// UpdateReq 更新配置请求
|
||||
// UpdateConfigReq 更新配置请求(支持 map 与 key-value 列表格式)
|
||||
type UpdateConfigReq struct {
|
||||
Configs map[string]string `json:"configs" binding:"required"`
|
||||
Configs map[string]string `json:"configs"`
|
||||
}
|
||||
|
||||
func (r *UpdateConfigReq) UnmarshalJSON(data []byte) error {
|
||||
// 格式1: {"configs": {"k1": "v1", "k2": "v2"}}
|
||||
var auxMap struct {
|
||||
Configs map[string]string `json:"configs"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &auxMap); err == nil && auxMap.Configs != nil {
|
||||
r.Configs = auxMap.Configs
|
||||
return nil
|
||||
}
|
||||
|
||||
// 格式2: {"configs": [{"key": "k1", "value": "v1"}]}
|
||||
var auxList struct {
|
||||
Configs []struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
} `json:"configs"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &auxList); err == nil && auxList.Configs != nil {
|
||||
r.Configs = make(map[string]string, len(auxList.Configs))
|
||||
for _, item := range auxList.Configs {
|
||||
r.Configs[item.Key] = item.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 格式3: 直接传入 map: {"k1": "v1"}
|
||||
var directMap map[string]string
|
||||
if err := json.Unmarshal(data, &directMap); err == nil {
|
||||
r.Configs = directMap
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("invalid configs format")
|
||||
}
|
||||
|
||||
// Update 批量更新配置
|
||||
func (s *ConfigService) Update(req *UpdateConfigReq) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
for key, value := range req.Configs {
|
||||
if err := tx.Model(&model.SystemConfig{}).Where("`key` = ?", key).Update("value", value).Error; err != nil {
|
||||
return err
|
||||
@@ -86,13 +142,66 @@ func (s *ConfigService) Update(req *UpdateConfigReq) error {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
s.mu.Lock()
|
||||
for key, value := range req.Configs {
|
||||
s.cache[key] = value
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetValue 获取单个配置值
|
||||
// GetValue 获取单个配置值(优先走内存缓存)
|
||||
func (s *ConfigService) GetValue(key string) string {
|
||||
s.mu.RLock()
|
||||
val, ok := s.cache[key]
|
||||
s.mu.RUnlock()
|
||||
if ok {
|
||||
return val
|
||||
}
|
||||
|
||||
var c model.SystemConfig
|
||||
if err := s.db.Where("`key` = ?", key).First(&c).Error; err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.cache[key] = c.Value
|
||||
s.mu.Unlock()
|
||||
|
||||
return c.Value
|
||||
}
|
||||
|
||||
// GetInt 获取整数配置值,解析失败或不存在则返回 def
|
||||
func (s *ConfigService) GetInt(key string, def int) int {
|
||||
val := s.GetValue(key)
|
||||
if val == "" {
|
||||
return def
|
||||
}
|
||||
if n, err := strconv.Atoi(val); err == nil {
|
||||
return n
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// GetGeetestConfig 获取极验验证配置
|
||||
func (s *ConfigService) GetGeetestConfig() (id, key string) {
|
||||
return s.GetValue("geetest_id"), s.GetValue("geetest_key")
|
||||
}
|
||||
|
||||
// GetSmsConfig 获取短信配置
|
||||
func (s *ConfigService) GetSmsConfig() (provider, accessKey, secretKey, signName, templateCode string) {
|
||||
return s.GetValue("sms_provider"),
|
||||
s.GetValue("sms_access_key"),
|
||||
s.GetValue("sms_secret_key"),
|
||||
s.GetValue("sms_sign_name"),
|
||||
s.GetValue("sms_template_code")
|
||||
}
|
||||
|
||||
// GetWechatConfig 获取微信登录配置
|
||||
func (s *ConfigService) GetWechatConfig() (appID, appSecret, redirectURI string) {
|
||||
return s.GetValue("wechat_app_id"),
|
||||
s.GetValue("wechat_app_secret"),
|
||||
s.GetValue("wechat_redirect_uri")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user