Initial commit: 网络验证平台
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
"verification-platform-backend/pkg/jwt"
|
||||
"verification-platform-backend/pkg/utils"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuthService 认证服务
|
||||
type AuthService struct{}
|
||||
|
||||
// NewAuthService 创建认证服务实例
|
||||
func NewAuthService() *AuthService {
|
||||
return &AuthService{}
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (s *AuthService) Login(username, password, agentPath string) (map[string]interface{}, error) {
|
||||
// 查找用户
|
||||
var user model.User
|
||||
err := database.DB.Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||
if err != nil {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
user.LastLoginAt = &time.Time{}
|
||||
*user.LastLoginAt = time.Now()
|
||||
database.DB.Save(&user)
|
||||
|
||||
// 生成JWT令牌
|
||||
token, err := jwt.GenerateToken(user.ID, user.Username, user.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回用户信息和令牌
|
||||
return map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"role": user.Role,
|
||||
"status": user.Status,
|
||||
},
|
||||
"token": token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
func (s *AuthService) Register(username, email, password string) error {
|
||||
return s.RegisterWithRole(username, email, "", password, "developer")
|
||||
}
|
||||
|
||||
// RegisterWithRole 用户注册(带角色)
|
||||
func (s *AuthService) RegisterWithRole(username, email, phone, password, role string) error {
|
||||
if username == "" {
|
||||
return errors.New("用户名不能为空")
|
||||
}
|
||||
if password == "" {
|
||||
return errors.New("密码不能为空")
|
||||
}
|
||||
|
||||
var existingUser model.User
|
||||
err := database.DB.Where("username = ?", username).First(&existingUser).Error
|
||||
if err == nil {
|
||||
return errors.New("用户名已存在")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if email != "" {
|
||||
var emailStr string = email
|
||||
err = database.DB.Where("email = ?", emailStr).First(&existingUser).Error
|
||||
if err == nil {
|
||||
return errors.New("邮箱已被注册")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if phone != "" {
|
||||
err = database.DB.Where("device_id = ?", phone).First(&existingUser).Error
|
||||
if err == nil {
|
||||
return errors.New("手机号已被注册")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if role == "" {
|
||||
role = "developer"
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Username: username,
|
||||
DeviceID: phone,
|
||||
Password: string(hashedPassword),
|
||||
Role: role,
|
||||
Status: "active",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if email != "" {
|
||||
user.Email = &email
|
||||
}
|
||||
|
||||
err = database.DB.Create(&user).Error
|
||||
if err != nil {
|
||||
// 处理数据库唯一约束错误
|
||||
errMsg := err.Error()
|
||||
if strings.Contains(errMsg, "UNIQUE constraint failed") {
|
||||
if strings.Contains(errMsg, "users.username") {
|
||||
return errors.New("用户名已存在")
|
||||
}
|
||||
if strings.Contains(errMsg, "users.email") {
|
||||
return errors.New("邮箱已被注册")
|
||||
}
|
||||
if strings.Contains(errMsg, "users.device_id") {
|
||||
return errors.New("手机号已被注册")
|
||||
}
|
||||
return errors.New("该账号已被注册")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProfile 获取用户资料
|
||||
func (s *AuthService) GetProfile(userID uint) (*model.User, error) {
|
||||
var user model.User
|
||||
err := database.DB.First(&user, userID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 不返回密码
|
||||
user.Password = ""
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateProfile 更新用户资料
|
||||
func (s *AuthService) UpdateProfile(userID uint, email, phone string) error {
|
||||
// 检查邮箱是否已被其他用户使用
|
||||
if email != "" {
|
||||
var existingUser model.User
|
||||
err := database.DB.Where("email = ? AND id != ?", email, userID).First(&existingUser).Error
|
||||
if err == nil {
|
||||
return errors.New("邮箱已被其他用户使用")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
updates := map[string]interface{}{
|
||||
"email": email,
|
||||
"phone": phone,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
err := database.DB.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
|
||||
// 获取用户
|
||||
var user model.User
|
||||
err := database.DB.First(&user, userID).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword))
|
||||
if err != nil {
|
||||
return errors.New("原密码错误")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.Password = string(hashedPassword)
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
err = database.DB.Save(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForgotPassword 忘记密码
|
||||
func (s *AuthService) ForgotPassword(email string) error {
|
||||
// 查找用户
|
||||
var user model.User
|
||||
err := database.DB.Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("邮箱未注册")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成重置token
|
||||
resetToken := utils.GenerateRandomString(32)
|
||||
user.ResetToken = resetToken
|
||||
expiresAt := time.Now().Add(24 * time.Hour) // 24小时有效期
|
||||
user.ResetTokenExpiresAt = &expiresAt
|
||||
|
||||
err = database.DB.Save(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 这里应该发送邮件,简化处理
|
||||
_ = resetToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword 重置密码
|
||||
func (s *AuthService) ResetPassword(token, newPassword string) error {
|
||||
// 查找用户
|
||||
var user model.User
|
||||
err := database.DB.Where("reset_token = ? AND reset_token_expires_at > ?", token, time.Now()).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("重置链接无效或已过期")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.Password = string(hashedPassword)
|
||||
user.ResetToken = ""
|
||||
var emptyTime time.Time
|
||||
user.ResetTokenExpiresAt = &emptyTime
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
err = database.DB.Save(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCaptcha 获取验证码
|
||||
func (s *AuthService) GetCaptcha() (map[string]interface{}, error) {
|
||||
// 生成随机验证码
|
||||
captchaText := utils.GenerateRandomString(6)
|
||||
|
||||
// 生成验证码ID
|
||||
captchaID := utils.GenerateRandomString(16)
|
||||
|
||||
// 存储验证码到数据库
|
||||
captcha := model.Captcha{
|
||||
CaptchaID: captchaID,
|
||||
Code: captchaText,
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&captcha).Error; err != nil {
|
||||
return nil, fmt.Errorf("存储验证码失败")
|
||||
}
|
||||
|
||||
// 创建一个简单的验证码图片
|
||||
// 这里使用SVG格式生成一个简单的验证码图片
|
||||
svg := generateCaptchaSVG(captchaText)
|
||||
|
||||
// 将SVG转换为base64
|
||||
captchaImage := "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))
|
||||
|
||||
return map[string]interface{}{
|
||||
"captcha_id": captchaID,
|
||||
"captcha_image": captchaImage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateCaptchaSVG 生成验证码SVG图片
|
||||
func generateCaptchaSVG(text string) string {
|
||||
width := 120
|
||||
height := 40
|
||||
|
||||
// 生成随机颜色
|
||||
colors := []string{"#FF5722", "#4CAF50", "#2196F3", "#FF9800", "#9C27B0"}
|
||||
bgColor := "#F5F5F5"
|
||||
|
||||
// 创建SVG
|
||||
svg := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">
|
||||
<rect width="100%%" height="100%%" fill="%s"/>
|
||||
<text x="50%%" y="50%%" text-anchor="middle" dominant-baseline="middle"
|
||||
font-family="Arial, sans-serif" font-size="20" font-weight="bold" fill="%s">%s</text>
|
||||
</svg>`, width, height, width, height, bgColor, colors[0], text)
|
||||
|
||||
return svg
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EmailService struct{}
|
||||
|
||||
func NewEmailService() *EmailService {
|
||||
return &EmailService{}
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
FromName string
|
||||
FromEmail string
|
||||
UseSSL bool
|
||||
}
|
||||
|
||||
func (s *EmailService) SendEmail(config EmailConfig, to, subject, body string) error {
|
||||
if config.Host == "" || config.User == "" || config.Password == "" {
|
||||
return fmt.Errorf("邮件配置不完整")
|
||||
}
|
||||
|
||||
from := config.FromEmail
|
||||
if from == "" {
|
||||
from = config.User
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", config.Host, config.Port)
|
||||
|
||||
msg := s.buildMessage(config.FromName, from, to, subject, body)
|
||||
|
||||
auth := smtp.PlainAuth("", config.User, config.Password, config.Host)
|
||||
|
||||
if config.UseSSL || config.Port == 465 || config.Port == 587 {
|
||||
return s.sendWithTLS(addr, auth, from, []string{to}, []byte(msg))
|
||||
}
|
||||
|
||||
return smtp.SendMail(addr, auth, from, []string{to}, []byte(msg))
|
||||
}
|
||||
|
||||
func (s *EmailService) sendWithTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte) error {
|
||||
host := strings.Split(addr, ":")[0]
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: host,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS连接失败: %v", err)
|
||||
}
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建SMTP客户端失败: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP认证失败: %v", err)
|
||||
}
|
||||
|
||||
if err = client.Mail(from); err != nil {
|
||||
return fmt.Errorf("设置发件人失败: %v", err)
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err = client.Rcpt(addr); err != nil {
|
||||
return fmt.Errorf("设置收件人失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("准备邮件数据失败: %v", err)
|
||||
}
|
||||
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入邮件内容失败: %v", err)
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("关闭邮件写入失败: %v", err)
|
||||
}
|
||||
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
func (s *EmailService) buildMessage(fromName, from, to, subject, body string) string {
|
||||
msg := ""
|
||||
|
||||
if fromName != "" {
|
||||
encodedName := mime.QEncoding.Encode("UTF-8", fromName)
|
||||
msg += fmt.Sprintf("From: %s <%s>\r\n", encodedName, from)
|
||||
} else {
|
||||
msg += fmt.Sprintf("From: %s\r\n", from)
|
||||
}
|
||||
|
||||
msg += fmt.Sprintf("To: %s\r\n", to)
|
||||
msg += fmt.Sprintf("Subject: %s\r\n", mime.QEncoding.Encode("UTF-8", subject))
|
||||
msg += "MIME-version: 1.0;\r\nContent-Type: text/html; charset=\"UTF-8\";\r\n\r\n"
|
||||
msg += body
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func (s *EmailService) SendTestEmail(config EmailConfig, to, siteName string) error {
|
||||
subject := fmt.Sprintf("测试邮件 - %s", siteName)
|
||||
body := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px; background-color: #f5f5f5;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #333; margin-bottom: 20px;">邮件发送测试成功</h2>
|
||||
<p style="color: #666; line-height: 1.6;">这是一封测试邮件,如果您收到此邮件,说明您的SMTP配置正确。</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #999; font-size: 12px;">此邮件由 %s 系统自动发送,请勿回复。</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, siteName)
|
||||
return s.SendEmail(config, to, subject, body)
|
||||
}
|
||||
|
||||
func ParseBool(value string) bool {
|
||||
return value == "true" || value == "1" || strings.ToLower(value) == "yes"
|
||||
}
|
||||
|
||||
func ParseInt(value string, defaultValue int) int {
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
val, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return val
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LogService struct{}
|
||||
|
||||
type LogParams struct {
|
||||
UserID *uint
|
||||
ApplicationID *uint
|
||||
AppUserID *uint
|
||||
LogType string
|
||||
Action string
|
||||
Resource string
|
||||
ResourceID *uint
|
||||
Details string
|
||||
IPAddress string
|
||||
UserAgent string
|
||||
DeviceID string
|
||||
Status string
|
||||
Level string
|
||||
ErrorMessage string
|
||||
StackTrace string
|
||||
}
|
||||
|
||||
func NewLogService() *LogService {
|
||||
return &LogService{}
|
||||
}
|
||||
|
||||
func (s *LogService) CreateLog(params LogParams) error {
|
||||
log := model.Log{
|
||||
UserID: params.UserID,
|
||||
ApplicationID: params.ApplicationID,
|
||||
AppUserID: params.AppUserID,
|
||||
LogType: params.LogType,
|
||||
Action: params.Action,
|
||||
Resource: params.Resource,
|
||||
ResourceID: params.ResourceID,
|
||||
Details: params.Details,
|
||||
IPAddress: params.IPAddress,
|
||||
UserAgent: params.UserAgent,
|
||||
DeviceID: params.DeviceID,
|
||||
Status: params.Status,
|
||||
Level: params.Level,
|
||||
ErrorMessage: params.ErrorMessage,
|
||||
StackTrace: params.StackTrace,
|
||||
}
|
||||
|
||||
if log.LogType == "" {
|
||||
log.LogType = "operation"
|
||||
}
|
||||
if log.Status == "" {
|
||||
log.Status = "success"
|
||||
}
|
||||
if log.Level == "" {
|
||||
log.Level = "info"
|
||||
}
|
||||
|
||||
return database.DB.Create(&log).Error
|
||||
}
|
||||
|
||||
func (s *LogService) CreateLogFromContext(c *gin.Context, params LogParams) error {
|
||||
params.IPAddress = c.ClientIP()
|
||||
params.UserAgent = c.GetHeader("User-Agent")
|
||||
|
||||
if userID, exists := c.Get("user_id"); exists && params.UserID == nil {
|
||||
if uid, ok := userID.(uint); ok {
|
||||
params.UserID = &uid
|
||||
}
|
||||
}
|
||||
|
||||
return s.CreateLog(params)
|
||||
}
|
||||
|
||||
func (s *LogService) LogOperation(c *gin.Context, action, resource string, resourceID *uint, details string, err error) error {
|
||||
params := LogParams{
|
||||
LogType: "operation",
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
ResourceID: resourceID,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
params.Status = "failed"
|
||||
params.Level = "error"
|
||||
params.ErrorMessage = err.Error()
|
||||
}
|
||||
|
||||
return s.CreateLogFromContext(c, params)
|
||||
}
|
||||
|
||||
func (s *LogService) LogVerification(c *gin.Context, applicationID *uint, appUserID *uint, action, details string, deviceID string, err error) error {
|
||||
params := LogParams{
|
||||
LogType: "verification",
|
||||
ApplicationID: applicationID,
|
||||
AppUserID: appUserID,
|
||||
Action: action,
|
||||
Resource: "verification",
|
||||
Details: details,
|
||||
DeviceID: deviceID,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
params.Status = "failed"
|
||||
params.Level = "warning"
|
||||
params.ErrorMessage = err.Error()
|
||||
}
|
||||
|
||||
return s.CreateLogFromContext(c, params)
|
||||
}
|
||||
|
||||
func (s *LogService) LogException(c *gin.Context, action, errorMessage, stackTrace string) error {
|
||||
params := LogParams{
|
||||
LogType: "exception",
|
||||
Action: action,
|
||||
Resource: "system",
|
||||
Status: "failed",
|
||||
Level: "error",
|
||||
ErrorMessage: errorMessage,
|
||||
StackTrace: stackTrace,
|
||||
}
|
||||
|
||||
return s.CreateLogFromContext(c, params)
|
||||
}
|
||||
|
||||
var logService = NewLogService()
|
||||
|
||||
func CreateLog(params LogParams) error {
|
||||
return logService.CreateLog(params)
|
||||
}
|
||||
|
||||
func CreateLogFromContext(c *gin.Context, params LogParams) error {
|
||||
return logService.CreateLogFromContext(c, params)
|
||||
}
|
||||
|
||||
func LogOperation(c *gin.Context, action, resource string, resourceID *uint, details string, err error) error {
|
||||
return logService.LogOperation(c, action, resource, resourceID, details, err)
|
||||
}
|
||||
|
||||
func LogVerification(c *gin.Context, applicationID *uint, appUserID *uint, action, details string, DeviceFingerprint string, err error) error {
|
||||
return logService.LogVerification(c, applicationID, appUserID, action, details, DeviceFingerprint, err)
|
||||
}
|
||||
|
||||
func LogException(c *gin.Context, action, errorMessage, stackTrace string) error {
|
||||
return logService.LogException(c, action, errorMessage, stackTrace)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
)
|
||||
|
||||
type PackageFeature string
|
||||
|
||||
const (
|
||||
FeatureCloudData PackageFeature = "cloud_data"
|
||||
FeatureDynamicCode PackageFeature = "dynamic_code"
|
||||
FeatureEmail PackageFeature = "email"
|
||||
FeatureExtension PackageFeature = "extension"
|
||||
FeatureAgent PackageFeature = "agent"
|
||||
)
|
||||
|
||||
func GetUserPackagePermission(userID uint) (*model.PackagePermission, error) {
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
return nil, fmt.Errorf("用户没有当前套餐")
|
||||
}
|
||||
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND package_id = ? AND status = ?", userID, *user.CurrentPackageID, "active").First(&userPackage).Error; err != nil {
|
||||
return nil, fmt.Errorf("用户套餐已过期或无效")
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", *user.CurrentPackageID).First(&permission).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &permission, nil
|
||||
}
|
||||
|
||||
func CheckPackageFeature(userID uint, feature PackageFeature) (bool, error) {
|
||||
permission, err := GetUserPackagePermission(userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch feature {
|
||||
case FeatureCloudData:
|
||||
return permission.AllowCloudData, nil
|
||||
case FeatureDynamicCode:
|
||||
return permission.AllowDynamicCode, nil
|
||||
case FeatureEmail:
|
||||
return permission.AllowEmail, nil
|
||||
case FeatureExtension:
|
||||
return permission.AllowExtension, nil
|
||||
case FeatureAgent:
|
||||
return permission.AllowAgent, nil
|
||||
default:
|
||||
return false, fmt.Errorf("未知的功能: %s", feature)
|
||||
}
|
||||
}
|
||||
|
||||
func GetApplicationDisabledStatus(appID uint) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type ApplicationWithStatus struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
AppKey string `json:"app_key"`
|
||||
IconURL string `json:"icon_url"`
|
||||
Status string `json:"status"`
|
||||
BillingType string `json:"billing_type"`
|
||||
LoginPolicy string `json:"login_policy"`
|
||||
MaxDevices int `json:"max_devices"`
|
||||
MultiOpen bool `json:"multi_open"`
|
||||
MultiOpenMode string `json:"multi_open_mode"`
|
||||
EnableTrial bool `json:"enable_trial"`
|
||||
TrialDays int `json:"trial_days"`
|
||||
EnableFreePeriod bool `json:"enable_free_period"`
|
||||
FreePeriodType string `json:"free_period_type"`
|
||||
FreePeriodStart string `json:"free_period_start"`
|
||||
FreePeriodEnd string `json:"free_period_end"`
|
||||
FreePeriodWeekdays string `json:"free_period_weekdays"`
|
||||
FreePeriodStartTime string `json:"free_period_start_time"`
|
||||
FreePeriodEndTime string `json:"free_period_end_time"`
|
||||
HeartbeatInterval int `json:"heartbeat_interval"`
|
||||
HeartbeatTimeout int `json:"heartbeat_timeout"`
|
||||
MaxAttempts int `json:"max_attempts"`
|
||||
LockDuration int `json:"lock_duration"`
|
||||
DeductionMode string `json:"deduction_mode"`
|
||||
DeductionType string `json:"deduction_type"`
|
||||
DeductionInterval int `json:"deduction_interval"`
|
||||
DeductionUnit string `json:"deduction_unit"`
|
||||
DeductionAmount float64 `json:"deduction_amount"`
|
||||
AllowRegister bool `json:"allow_register"`
|
||||
RegisterMethods string `json:"register_methods"`
|
||||
EnableEmailVerify bool `json:"enable_email_verify"`
|
||||
RequireEmailVerify bool `json:"require_email_verify"`
|
||||
EnablePasswordReset bool `json:"enable_password_reset"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
DisabledByPackage bool `json:"disabled_by_package"`
|
||||
}
|
||||
|
||||
func GetApplicationsWithDisabledStatus(userID uint) ([]ApplicationWithStatus, error) {
|
||||
var user model.User
|
||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Order("created_at ASC").Find(&apps).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]ApplicationWithStatus, len(apps))
|
||||
for i, app := range apps {
|
||||
result[i] = ApplicationWithStatus{
|
||||
ID: app.ID,
|
||||
UserID: app.UserID,
|
||||
Name: app.Name,
|
||||
Description: app.Description,
|
||||
AppKey: app.AppKey,
|
||||
IconURL: app.IconURL,
|
||||
Status: app.Status,
|
||||
BillingType: app.BillingType,
|
||||
LoginPolicy: app.LoginPolicy,
|
||||
MaxDevices: app.MaxDevices,
|
||||
MultiOpen: app.MultiOpen,
|
||||
MultiOpenMode: app.MultiOpenMode,
|
||||
EnableTrial: app.EnableTrial,
|
||||
TrialDays: app.TrialDays,
|
||||
EnableFreePeriod: app.EnableFreePeriod,
|
||||
FreePeriodType: app.FreePeriodType,
|
||||
FreePeriodStart: app.FreePeriodStart,
|
||||
FreePeriodEnd: app.FreePeriodEnd,
|
||||
FreePeriodWeekdays: app.FreePeriodWeekdays,
|
||||
FreePeriodStartTime: app.FreePeriodStartTime,
|
||||
FreePeriodEndTime: app.FreePeriodEndTime,
|
||||
HeartbeatInterval: app.HeartbeatInterval,
|
||||
HeartbeatTimeout: app.HeartbeatTimeout,
|
||||
MaxAttempts: app.MaxAttempts,
|
||||
LockDuration: app.LockDuration,
|
||||
DeductionMode: app.DeductionMode,
|
||||
DeductionType: app.DeductionType,
|
||||
DeductionInterval: app.DeductionInterval,
|
||||
DeductionUnit: app.DeductionUnit,
|
||||
DeductionAmount: app.DeductionAmount,
|
||||
AllowRegister: app.AllowRegister,
|
||||
RegisterMethods: app.RegisterMethods,
|
||||
EnableEmailVerify: app.EnableEmailVerify,
|
||||
RequireEmailVerify: app.RequireEmailVerify,
|
||||
EnablePasswordReset: app.EnablePasswordReset,
|
||||
CreatedAt: app.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: app.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
for i := range result {
|
||||
result[i].DisabledByPackage = true
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where("user_id = ? AND package_id = ? AND status = ?", userID, *user.CurrentPackageID, "active").First(&userPackage).Error; err != nil {
|
||||
for i := range result {
|
||||
result[i].DisabledByPackage = true
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", *user.CurrentPackageID).First(&permission).Error; err != nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if permission.MaxApplications <= 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
result[i].DisabledByPackage = i >= permission.MaxApplications
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BEpusdtConfig struct {
|
||||
ApiURL string
|
||||
ApiToken string
|
||||
TradeType string
|
||||
Fiat string
|
||||
Timeout int
|
||||
Rate string
|
||||
}
|
||||
|
||||
type BEpusdtClient struct {
|
||||
config BEpusdtConfig
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type CreateTransactionRequest struct {
|
||||
OrderID string `json:"order_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
NotifyURL string `json:"notify_url"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
Signature string `json:"signature"`
|
||||
TradeType string `json:"trade_type,omitempty"`
|
||||
Fiat string `json:"fiat,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
Rate string `json:"rate,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
|
||||
type CreateTransactionResponse struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Fiat string `json:"fiat"`
|
||||
TradeID string `json:"trade_id"`
|
||||
OrderID string `json:"order_id"`
|
||||
Amount interface{} `json:"amount"`
|
||||
ActualAmount interface{} `json:"actual_amount"`
|
||||
Status int `json:"status"`
|
||||
Token string `json:"token"`
|
||||
ExpirationTime int `json:"expiration_time"`
|
||||
PaymentURL string `json:"payment_url"`
|
||||
} `json:"data"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
type CallbackData struct {
|
||||
TradeID string `json:"trade_id"`
|
||||
OrderID string `json:"order_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
ActualAmount float64 `json:"actual_amount"`
|
||||
Token string `json:"token"`
|
||||
BlockTransactionID string `json:"block_transaction_id"`
|
||||
Signature string `json:"signature"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func NewBEpusdtClient(config BEpusdtConfig) *BEpusdtClient {
|
||||
if config.Fiat == "" {
|
||||
config.Fiat = "CNY"
|
||||
}
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 600
|
||||
}
|
||||
|
||||
return &BEpusdtClient{
|
||||
config: config,
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BEpusdtClient) GenerateSignature(params map[string]string) string {
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
if params[k] != "" && k != "signature" && k != "sign_type" {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var parts []string
|
||||
for _, k := range keys {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
|
||||
}
|
||||
signStr := strings.Join(parts, "&") + c.config.ApiToken
|
||||
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(signStr))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (c *BEpusdtClient) CreateTransaction(orderID string, amount float64, notifyURL, redirectURL, name string) (*CreateTransactionResponse, error) {
|
||||
params := map[string]string{
|
||||
"order_id": orderID,
|
||||
"amount": fmt.Sprintf("%.2f", amount),
|
||||
"notify_url": notifyURL,
|
||||
"redirect_url": redirectURL,
|
||||
}
|
||||
|
||||
if c.config.TradeType != "" {
|
||||
params["trade_type"] = c.config.TradeType
|
||||
}
|
||||
if c.config.Fiat != "" {
|
||||
params["fiat"] = c.config.Fiat
|
||||
}
|
||||
if name != "" {
|
||||
params["name"] = name
|
||||
}
|
||||
if c.config.Timeout > 0 {
|
||||
params["timeout"] = fmt.Sprintf("%d", c.config.Timeout)
|
||||
}
|
||||
if c.config.Rate != "" {
|
||||
params["rate"] = c.config.Rate
|
||||
}
|
||||
|
||||
params["signature"] = c.GenerateSignature(params)
|
||||
|
||||
jsonData, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
apiURL := strings.TrimRight(c.config.ApiURL, "/")
|
||||
reqURL := apiURL + "/api/v1/order/create-transaction"
|
||||
|
||||
req, err := http.NewRequest("POST", reqURL, strings.NewReader(string(jsonData)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var result CreateTransactionResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if result.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("API error: %s (code: %d)", result.Message, result.StatusCode)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *BEpusdtClient) VerifyCallback(data CallbackData) bool {
|
||||
params := map[string]string{
|
||||
"trade_id": data.TradeID,
|
||||
"order_id": data.OrderID,
|
||||
"amount": fmt.Sprintf("%.2f", data.Amount),
|
||||
"actual_amount": fmt.Sprintf("%.2f", data.ActualAmount),
|
||||
"token": data.Token,
|
||||
"block_transaction_id": data.BlockTransactionID,
|
||||
"status": fmt.Sprintf("%d", data.Status),
|
||||
}
|
||||
|
||||
expectedSign := c.GenerateSignature(params)
|
||||
return strings.EqualFold(expectedSign, data.Signature)
|
||||
}
|
||||
|
||||
func ParseCallbackFromQuery(query url.Values) CallbackData {
|
||||
var data CallbackData
|
||||
data.TradeID = query.Get("trade_id")
|
||||
data.OrderID = query.Get("order_id")
|
||||
data.Token = query.Get("token")
|
||||
data.BlockTransactionID = query.Get("block_transaction_id")
|
||||
data.Signature = query.Get("signature")
|
||||
|
||||
if amount := query.Get("amount"); amount != "" {
|
||||
fmt.Sscanf(amount, "%f", &data.Amount)
|
||||
}
|
||||
if actualAmount := query.Get("actual_amount"); actualAmount != "" {
|
||||
fmt.Sscanf(actualAmount, "%f", &data.ActualAmount)
|
||||
}
|
||||
if status := query.Get("status"); status != "" {
|
||||
fmt.Sscanf(status, "%d", &data.Status)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func ParseCallbackFromJSON(body []byte) (CallbackData, error) {
|
||||
var data CallbackData
|
||||
err := json.Unmarshal(body, &data)
|
||||
return data, err
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"verification-platform-backend/internal/model"
|
||||
)
|
||||
|
||||
type PaymentResult struct {
|
||||
OrderID string
|
||||
TradeID string
|
||||
PaymentURL string
|
||||
ActualAmount string
|
||||
Token string
|
||||
ExpirationTime int
|
||||
}
|
||||
|
||||
type PaymentService interface {
|
||||
CreateOrder(orderID string, amount float64, notifyURL, redirectURL, name string) (*PaymentResult, error)
|
||||
VerifyCallback(data interface{}) bool
|
||||
}
|
||||
|
||||
func GetPaymentService(channel model.PaymentChannel, callbackBaseURL string) (PaymentService, error) {
|
||||
switch channel.Type {
|
||||
case "bepusdt":
|
||||
config, err := parseBEpusdtConfig(channel.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse bepusdt config: %w", err)
|
||||
}
|
||||
return NewBEpusdtAdapter(config, callbackBaseURL), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported payment type: %s", channel.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func parseBEpusdtConfig(configStr string) (BEpusdtConfig, error) {
|
||||
var config BEpusdtConfig
|
||||
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
||||
return config, fmt.Errorf("invalid config format: %w", err)
|
||||
}
|
||||
|
||||
if config.ApiURL == "" {
|
||||
return config, fmt.Errorf("api_url is required")
|
||||
}
|
||||
if config.ApiToken == "" {
|
||||
return config, fmt.Errorf("api_token is required")
|
||||
}
|
||||
if config.TradeType == "" {
|
||||
config.TradeType = "usdt.trc20"
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func interfaceToString(v interface{}) string {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
case float64:
|
||||
return strconv.FormatFloat(val, 'f', -1, 64)
|
||||
case int:
|
||||
return strconv.Itoa(val)
|
||||
default:
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
|
||||
type BEpusdtAdapter struct {
|
||||
client *BEpusdtClient
|
||||
callbackBaseURL string
|
||||
}
|
||||
|
||||
func NewBEpusdtAdapter(config BEpusdtConfig, callbackBaseURL string) *BEpusdtAdapter {
|
||||
return &BEpusdtAdapter{
|
||||
client: NewBEpusdtClient(config),
|
||||
callbackBaseURL: callbackBaseURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *BEpusdtAdapter) CreateOrder(orderID string, amount float64, notifyURL, redirectURL, name string) (*PaymentResult, error) {
|
||||
resp, err := a.client.CreateTransaction(orderID, amount, notifyURL, redirectURL, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PaymentResult{
|
||||
OrderID: resp.Data.OrderID,
|
||||
TradeID: resp.Data.TradeID,
|
||||
PaymentURL: resp.Data.PaymentURL,
|
||||
ActualAmount: interfaceToString(resp.Data.ActualAmount),
|
||||
Token: resp.Data.Token,
|
||||
ExpirationTime: resp.Data.ExpirationTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *BEpusdtAdapter) VerifyCallback(data interface{}) bool {
|
||||
switch v := data.(type) {
|
||||
case CallbackData:
|
||||
return a.client.VerifyCallback(v)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type SettingService struct{}
|
||||
|
||||
func NewSettingService() *SettingService {
|
||||
return &SettingService{}
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSettings() (map[string]interface{}, error) {
|
||||
var settings []model.Setting
|
||||
if err := database.DB.Find(&settings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, setting := range settings {
|
||||
if _, ok := result[setting.Category]; !ok {
|
||||
result[setting.Category] = make(map[string]interface{})
|
||||
}
|
||||
result[setting.Category].(map[string]interface{})[setting.Key] = setting.Value
|
||||
}
|
||||
|
||||
fmt.Println("========================================")
|
||||
fmt.Println("GetSettings 返回的数据:")
|
||||
for cat, data := range result {
|
||||
fmt.Printf(" 分类: %s\n", cat)
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
for k, v := range dataMap {
|
||||
fmt.Printf(" %s: %v\n", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println("========================================")
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSettingsByCategory(category string) (map[string]interface{}, error) {
|
||||
var settings []model.Setting
|
||||
if err := database.DB.Where("category = ?", category).Find(&settings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, setting := range settings {
|
||||
result[setting.Key] = setting.Value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateSettings(category string, data map[string]interface{}) error {
|
||||
fmt.Println("========================================")
|
||||
fmt.Printf("UpdateSettings 被调用: category=%s\n", category)
|
||||
for key, value := range data {
|
||||
var strValue string
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
strValue = v
|
||||
case bool:
|
||||
if v {
|
||||
strValue = "true"
|
||||
} else {
|
||||
strValue = "false"
|
||||
}
|
||||
case int:
|
||||
strValue = fmt.Sprintf("%d", v)
|
||||
case int64:
|
||||
strValue = fmt.Sprintf("%d", v)
|
||||
case float64:
|
||||
strValue = fmt.Sprintf("%.0f", v)
|
||||
case float32:
|
||||
strValue = fmt.Sprintf("%.0f", v)
|
||||
default:
|
||||
fmt.Printf(" 跳过: %s (类型: %T, 值: %v)\n", key, value, value)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" 保存: %s = %s\n", key, strValue)
|
||||
|
||||
setting := model.Setting{
|
||||
Category: category,
|
||||
Key: key,
|
||||
Value: strValue,
|
||||
}
|
||||
|
||||
if err := database.DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value", "category", "updated_at"}),
|
||||
}).Create(&setting).Error; err != nil {
|
||||
fmt.Printf(" 保存失败: %v\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Println("========================================")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSetting(category, key string) (string, error) {
|
||||
var setting model.Setting
|
||||
if err := database.DB.Where("category = ? AND key = ?", category, key).First(&setting).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetSetting(category, key, value string) error {
|
||||
setting := model.Setting{
|
||||
Category: category,
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
return database.DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value", "category", "updated_at"}),
|
||||
}).Create(&setting).Error
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SmsService struct{}
|
||||
|
||||
func NewSmsService() *SmsService {
|
||||
return &SmsService{}
|
||||
}
|
||||
|
||||
type SmsConfig struct {
|
||||
Provider string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
SignName string
|
||||
TemplateCode string
|
||||
}
|
||||
|
||||
func (s *SmsService) SendSms(config SmsConfig, phone, code string) error {
|
||||
switch config.Provider {
|
||||
case "aliyun":
|
||||
return s.sendAliyunSms(config, phone, code)
|
||||
case "tencent":
|
||||
return s.sendTencentSms(config, phone, code)
|
||||
case "huawei":
|
||||
return s.sendHuaweiSms(config, phone, code)
|
||||
default:
|
||||
return fmt.Errorf("不支持的短信服务商: %s", config.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SmsService) sendAliyunSms(config SmsConfig, phone, code string) error {
|
||||
params := url.Values{}
|
||||
params.Set("AccessKeyId", config.AccessKey)
|
||||
params.Set("Action", "SendSms")
|
||||
params.Set("Format", "JSON")
|
||||
params.Set("PhoneNumbers", phone)
|
||||
params.Set("RegionId", "cn-hangzhou")
|
||||
params.Set("SignName", config.SignName)
|
||||
params.Set("SignatureMethod", "HMAC-SHA1")
|
||||
params.Set("SignatureNonce", fmt.Sprintf("%d", getTimeStamp()))
|
||||
params.Set("SignatureVersion", "1.0")
|
||||
params.Set("TemplateCode", config.TemplateCode)
|
||||
params.Set("TemplateParam", fmt.Sprintf(`{"code":"%s"}`, code))
|
||||
params.Set("Timestamp", getTimeStampStr())
|
||||
params.Set("Version", "2017-05-25")
|
||||
|
||||
signature := s.generateAliyunSignature(params, config.SecretKey)
|
||||
params.Set("Signature", signature)
|
||||
|
||||
resp, err := http.Get(fmt.Sprintf("https://dysmsapi.aliyuncs.com/?%s", params.Encode()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求阿里云短信API失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Code string `json:"Code"`
|
||||
Message string `json:"Message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if result.Code != "OK" {
|
||||
return fmt.Errorf("发送失败: %s", result.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SmsService) generateAliyunSignature(params url.Values, secretKey string) string {
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
encodedParams := make([]string, 0, len(params))
|
||||
for _, k := range keys {
|
||||
encodedParams = append(encodedParams, fmt.Sprintf("%s=%s", specialUrlEncode(k), specialUrlEncode(params.Get(k))))
|
||||
}
|
||||
sortParams := strings.Join(encodedParams, "&")
|
||||
|
||||
stringToSign := fmt.Sprintf("GET&%s&%s", specialUrlEncode("/"), specialUrlEncode(sortParams))
|
||||
|
||||
signature := hmacSHA1(secretKey+"&", stringToSign)
|
||||
return signature
|
||||
}
|
||||
|
||||
func (s *SmsService) sendTencentSms(config SmsConfig, phone, code string) error {
|
||||
return fmt.Errorf("腾讯云短信服务暂未实现")
|
||||
}
|
||||
|
||||
func (s *SmsService) sendHuaweiSms(config SmsConfig, phone, code string) error {
|
||||
return fmt.Errorf("华为云短信服务暂未实现")
|
||||
}
|
||||
|
||||
func (s *SmsService) SendTestSms(config SmsConfig, phone string) error {
|
||||
code := "123456"
|
||||
return s.SendSms(config, phone, code)
|
||||
}
|
||||
|
||||
func specialUrlEncode(s string) string {
|
||||
encoded := url.QueryEscape(s)
|
||||
encoded = strings.ReplaceAll(encoded, "+", "%20")
|
||||
encoded = strings.ReplaceAll(encoded, "*", "%2A")
|
||||
encoded = strings.ReplaceAll(encoded, "%7E", "~")
|
||||
return encoded
|
||||
}
|
||||
|
||||
func hmacSHA1(key, data string) string {
|
||||
return data
|
||||
}
|
||||
|
||||
func getTimeStamp() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func getTimeStampStr() string {
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"verification-platform-backend/internal/database"
|
||||
"verification-platform-backend/internal/model"
|
||||
)
|
||||
|
||||
type UsageAlertService struct{}
|
||||
|
||||
func NewUsageAlertService() *UsageAlertService {
|
||||
return &UsageAlertService{}
|
||||
}
|
||||
|
||||
type AlertType string
|
||||
|
||||
const (
|
||||
AlertTypeAPICalls AlertType = "api_calls"
|
||||
AlertTypeStorage AlertType = "storage"
|
||||
AlertTypePackageExp AlertType = "package_expiry"
|
||||
)
|
||||
|
||||
type AlertLevel string
|
||||
|
||||
const (
|
||||
AlertLevelWarning AlertLevel = "warning"
|
||||
AlertLevelCritical AlertLevel = "critical"
|
||||
)
|
||||
|
||||
type UsageAlert struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Type AlertType `json:"type"`
|
||||
Level AlertLevel `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Usage float64 `json:"usage"`
|
||||
Limit float64 `json:"limit"`
|
||||
Percent float64 `json:"percent"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) CheckAndCreateAlerts(userID uint) ([]UsageAlert, error) {
|
||||
var alerts []UsageAlert
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Preload("CurrentPackage").First(&user, userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.CurrentPackageID == nil {
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
var permission model.PackagePermission
|
||||
if err := database.DB.Where("package_id = ?", user.CurrentPackageID).First(&permission).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if alert := s.checkAPICallsUsage(&user, &permission); alert != nil {
|
||||
alerts = append(alerts, *alert)
|
||||
}
|
||||
|
||||
if alert := s.checkStorageUsage(&user, &permission); alert != nil {
|
||||
alerts = append(alerts, *alert)
|
||||
}
|
||||
|
||||
if alert := s.checkPackageExpiry(&user); alert != nil {
|
||||
alerts = append(alerts, *alert)
|
||||
}
|
||||
|
||||
for _, alert := range alerts {
|
||||
s.sendNotification(&alert)
|
||||
}
|
||||
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) checkAPICallsUsage(user *model.User, permission *model.PackagePermission) *UsageAlert {
|
||||
if permission.MaxApiCalls == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
usagePercent := float64(user.ApiCallsUsed) / float64(permission.MaxApiCalls) * 100
|
||||
|
||||
var lastAlert model.UsageAlertRecord
|
||||
hasRecentAlert := database.DB.Where(
|
||||
"user_id = ? AND type = ? AND created_at > ?",
|
||||
user.ID,
|
||||
AlertTypeAPICalls,
|
||||
time.Now().Add(-24*time.Hour),
|
||||
).First(&lastAlert).Error == nil
|
||||
|
||||
if hasRecentAlert {
|
||||
return nil
|
||||
}
|
||||
|
||||
if usagePercent >= 90 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypeAPICalls,
|
||||
Level: AlertLevelCritical,
|
||||
Message: fmt.Sprintf("API调用次数已使用 %.1f%%,即将达到上限", usagePercent),
|
||||
Usage: float64(user.ApiCallsUsed),
|
||||
Limit: float64(permission.MaxApiCalls),
|
||||
Percent: usagePercent,
|
||||
}
|
||||
}
|
||||
|
||||
if usagePercent >= 80 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypeAPICalls,
|
||||
Level: AlertLevelWarning,
|
||||
Message: fmt.Sprintf("API调用次数已使用 %.1f%%,请注意用量", usagePercent),
|
||||
Usage: float64(user.ApiCallsUsed),
|
||||
Limit: float64(permission.MaxApiCalls),
|
||||
Percent: usagePercent,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) checkStorageUsage(user *model.User, permission *model.PackagePermission) *UsageAlert {
|
||||
if permission.MaxStorage == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
usagePercent := float64(user.StorageUsed) / float64(int64(permission.MaxStorage)*1024*1024) * 100
|
||||
|
||||
var lastAlert model.UsageAlertRecord
|
||||
hasRecentAlert := database.DB.Where(
|
||||
"user_id = ? AND type = ? AND created_at > ?",
|
||||
user.ID,
|
||||
AlertTypeStorage,
|
||||
time.Now().Add(-24*time.Hour),
|
||||
).First(&lastAlert).Error == nil
|
||||
|
||||
if hasRecentAlert {
|
||||
return nil
|
||||
}
|
||||
|
||||
if usagePercent >= 90 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypeStorage,
|
||||
Level: AlertLevelCritical,
|
||||
Message: fmt.Sprintf("存储空间已使用 %.1f%%,即将达到上限", usagePercent),
|
||||
Usage: float64(user.StorageUsed) / 1024 / 1024,
|
||||
Limit: float64(permission.MaxStorage),
|
||||
Percent: usagePercent,
|
||||
}
|
||||
}
|
||||
|
||||
if usagePercent >= 80 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypeStorage,
|
||||
Level: AlertLevelWarning,
|
||||
Message: fmt.Sprintf("存储空间已使用 %.1f%%,请注意用量", usagePercent),
|
||||
Usage: float64(user.StorageUsed) / 1024 / 1024,
|
||||
Limit: float64(permission.MaxStorage),
|
||||
Percent: usagePercent,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) checkPackageExpiry(user *model.User) *UsageAlert {
|
||||
var userPackage model.UserPackage
|
||||
if err := database.DB.Where(
|
||||
"user_id = ? AND package_id = ? AND status = ?",
|
||||
user.ID,
|
||||
user.CurrentPackageID,
|
||||
"active",
|
||||
).First(&userPackage).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if userPackage.ExpiredAt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
daysUntilExpiry := time.Until(*userPackage.ExpiredAt).Hours() / 24
|
||||
|
||||
var lastAlert model.UsageAlertRecord
|
||||
hasRecentAlert := database.DB.Where(
|
||||
"user_id = ? AND type = ? AND created_at > ?",
|
||||
user.ID,
|
||||
AlertTypePackageExp,
|
||||
time.Now().Add(-24*time.Hour),
|
||||
).First(&lastAlert).Error == nil
|
||||
|
||||
if hasRecentAlert {
|
||||
return nil
|
||||
}
|
||||
|
||||
if daysUntilExpiry <= 3 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypePackageExp,
|
||||
Level: AlertLevelCritical,
|
||||
Message: fmt.Sprintf("套餐将在 %.0f 天后过期,请及时续费", daysUntilExpiry),
|
||||
Usage: daysUntilExpiry,
|
||||
Limit: 0,
|
||||
Percent: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if daysUntilExpiry <= 7 {
|
||||
return &UsageAlert{
|
||||
UserID: user.ID,
|
||||
Type: AlertTypePackageExp,
|
||||
Level: AlertLevelWarning,
|
||||
Message: fmt.Sprintf("套餐将在 %.0f 天后过期,请及时续费", daysUntilExpiry),
|
||||
Usage: daysUntilExpiry,
|
||||
Limit: 0,
|
||||
Percent: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) sendNotification(alert *UsageAlert) {
|
||||
alertRecord := model.UsageAlertRecord{
|
||||
UserID: alert.UserID,
|
||||
Type: string(alert.Type),
|
||||
Level: string(alert.Level),
|
||||
Message: alert.Message,
|
||||
Usage: alert.Usage,
|
||||
Limit: alert.Limit,
|
||||
Percent: alert.Percent,
|
||||
Status: "sent",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
database.DB.Create(&alertRecord)
|
||||
|
||||
notification := model.Notification{
|
||||
UserID: alert.UserID,
|
||||
Title: "用量告警",
|
||||
Content: alert.Message,
|
||||
Type: "usage_alert",
|
||||
IsRead: false,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
database.DB.Create(¬ification)
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) CheckAllUsers() error {
|
||||
var users []model.User
|
||||
if err := database.DB.Where("current_package_id IS NOT NULL").Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
_, err := s.CheckAndCreateAlerts(user.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("检查用户 %d 用量告警失败: %v\n", user.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) GetUserAlerts(userID uint, limit int) ([]model.UsageAlertRecord, error) {
|
||||
var alerts []model.UsageAlertRecord
|
||||
err := database.DB.Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Find(&alerts).Error
|
||||
return alerts, err
|
||||
}
|
||||
|
||||
func (s *UsageAlertService) MarkAlertAsRead(alertID uint) error {
|
||||
return database.DB.Model(&model.UsageAlertRecord{}).
|
||||
Where("id = ?", alertID).
|
||||
Update("status", "read").Error
|
||||
}
|
||||
Reference in New Issue
Block a user