Initial commit: 网络验证平台

This commit is contained in:
Admin
2026-04-27 17:22:56 +08:00
commit afe67d704e
780 changed files with 88960 additions and 0 deletions
+342
View File
@@ -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
}
+154
View File
@@ -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
}
+151
View File
@@ -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)
}
+192
View File
@@ -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
}
+211
View File
@@ -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
}
+105
View File
@@ -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
}
}
+127
View File
@@ -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
}
+133
View File
@@ -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 ""
}
+280
View File
@@ -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(&notification)
}
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
}