feat():learning后台管理项目初始化

This commit is contained in:
yuj
2025-12-04 16:23:46 +08:00
parent 39886d50d2
commit 88e048f4d1
154 changed files with 28966 additions and 6 deletions

170
internal/services/README.md Normal file
View File

@@ -0,0 +1,170 @@
# 服务层
本模块负责业务逻辑的实现,提供各种业务服务。
## 功能特性
- 用户管理服务
- 角色管理服务
- 菜单管理服务
- 日志管理服务
- 统一的错误处理
- 业务逻辑封装
## 模块结构
```
services/
├── user_service.go # 用户服务
├── role_service.go # 角色服务
├── menu_service.go # 菜单服务
├── log_service.go # 日志服务
└── README.md # 说明文档
```
## 服务接口
### 用户服务 (UserService)
```go
type UserService interface {
Login(req *models.LoginRequest) (*models.LoginResponse, error)
Create(req *models.UserCreateRequest) (*models.User, error)
GetByID(id uint) (*models.User, error)
Update(id uint, req *models.UserUpdateRequest) (*models.User, error)
Delete(id uint) error
List(req *models.UserListRequest) ([]models.User, int64, error)
ChangePassword(id uint, req *models.UserChangePasswordRequest) error
UpdateStatus(id uint, status int) error
}
```
### 角色服务 (RoleService)
```go
type RoleService interface {
Create(req *models.RoleCreateRequest) (*models.Role, error)
GetByID(id uint) (*models.Role, error)
Update(id uint, req *models.RoleUpdateRequest) (*models.Role, error)
Delete(id uint) error
List(req *models.RoleListRequest) ([]models.Role, int64, error)
UpdateStatus(id uint, status int) error
}
```
### 菜单服务 (MenuService)
```go
type MenuService interface {
Create(req *models.MenuCreateRequest) (*models.Menu, error)
GetByID(id uint) (*models.Menu, error)
Update(id uint, req *models.MenuUpdateRequest) (*models.Menu, error)
Delete(id uint) error
List(req *models.MenuListRequest) ([]models.Menu, int64, error)
GetTree() ([]models.Menu, error)
UpdateStatus(id uint, status int) error
UpdateSort(id uint, sort int) error
}
```
### 日志服务 (LogService)
```go
type LogService interface {
CreateLoginLog(log *models.LoginLog) error
CreateOperationLog(log *models.OperationLog) error
GetLoginLogs(req *models.LoginLogListRequest) ([]models.LoginLog, int64, error)
GetOperationLogs(req *models.OperationLogListRequest) ([]models.OperationLog, int64, error)
DeleteLoginLogs(beforeDate string) error
DeleteOperationLogs(beforeDate string) error
}
```
## 使用方法
### 创建服务实例
```go
// 创建用户服务
userService := services.NewUserService(
storage.NewUserStorage(),
utils.NewJWTManager("secret-key"),
logger,
)
// 创建角色服务
roleService := services.NewRoleService(
storage.NewRoleStorage(),
logger,
)
// 创建菜单服务
menuService := services.NewMenuService(
storage.NewMenuStorage(),
logger,
)
// 创建日志服务
logService := services.NewLogService(
storage.NewLogStorage(),
logger,
)
```
### 使用服务
```go
// 用户登录
loginResp, err := userService.Login(&models.LoginRequest{
Username: "admin",
Password: "password",
})
// 创建用户
user, err := userService.Create(&models.UserCreateRequest{
Username: "newuser",
Email: "newuser@example.com",
Password: "password",
Role: "user",
})
// 获取用户列表
users, total, err := userService.List(&models.UserListRequest{
PageRequest: models.PageRequest{Page: 1, Size: 10},
Username: "admin",
})
// 获取菜单树
menus, err := menuService.GetTree()
// 记录操作日志
err := logService.CreateOperationLog(&models.OperationLog{
UserID: 1,
Username: "admin",
Module: "user",
Operation: "create",
Method: "POST",
Path: "/api/users",
IP: "127.0.0.1",
Status: 1,
})
```
## 错误处理
所有服务都遵循统一的错误处理模式:
- 业务逻辑错误返回具体的错误信息
- 数据库错误记录日志并返回通用错误信息
- 参数验证错误返回具体的验证信息
## 日志记录
服务层会自动记录关键操作的日志:
- 用户登录/登出
- 用户创建/更新/删除
- 角色创建/更新/删除
- 菜单创建/更新/删除
- 系统操作日志

View File

@@ -0,0 +1,70 @@
package services
import (
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"time"
)
// AuditLogService 审计日志服务接口
type AuditLogService interface {
Create(req *models.CreateAuditLogRequest) error
GetByID(id uint) (*models.AuditLog, error)
List(req *models.AuditLogListRequest) (*models.AuditLogListResponse, error)
}
type auditLogService struct {
storage storage.AuditLogStorage
}
// NewAuditLogService 创建审计日志服务实例
func NewAuditLogService(storage storage.AuditLogStorage) AuditLogService {
return &auditLogService{
storage: storage,
}
}
// Create 创建审计日志
func (s *auditLogService) Create(req *models.CreateAuditLogRequest) error {
log := &models.AuditLog{
OperationType: req.OperationType,
OperationTime: time.Now(),
OperatorID: req.OperatorID,
OperatorEmail: req.OperatorEmail,
TargetType: req.TargetType,
TargetID: req.TargetID,
TargetEmail: req.TargetEmail,
OperationDetails: req.OperationDetails,
IPAddress: req.IPAddress,
UserAgent: req.UserAgent,
Status: req.Status,
ErrorMessage: req.ErrorMessage,
}
// 设置默认状态
if log.Status == "" {
log.Status = models.AuditLogStatusSuccess
}
return s.storage.Create(log)
}
// GetByID 根据ID获取审计日志
func (s *auditLogService) GetByID(id uint) (*models.AuditLog, error) {
return s.storage.GetByID(id)
}
// List 查询审计日志列表
func (s *auditLogService) List(req *models.AuditLogListRequest) (*models.AuditLogListResponse, error) {
logs, total, err := s.storage.List(req)
if err != nil {
return nil, err
}
return &models.AuditLogListResponse{
List: logs,
Total: total,
Page: req.Page,
Size: req.Size,
}, nil
}

View File

@@ -0,0 +1,443 @@
package services
import (
"bytes"
"crypto/tls"
"fmt"
"html/template"
"math"
"net/smtp"
"time"
"github.com/jordan-wright/email"
"goalfymax-admin/internal/config"
)
type EmailService struct {
host string
port int
username string
password string
sender string
inviteURLPrefix string
}
// NewEmailService 创建邮件服务实例
func NewEmailService() *EmailService {
cfg := config.GetConfig()
return &EmailService{
host: cfg.Email.Host,
port: cfg.Email.Port,
username: cfg.Email.Username,
password: cfg.Email.Password,
sender: cfg.Email.Sender,
inviteURLPrefix: cfg.Email.InviteURLPrefix,
}
}
// SendInviteCodeApprovalEmail 发送邀请码审批通过邮件
func (s *EmailService) SendInviteCodeApprovalEmail(toEmail, inviteCode, language string, expiresAt *time.Time) error {
var subject string
if language == "en" {
subject = "Your GoalfyAI Beta Access Invitation"
} else {
subject = "GoalfyAI 内测邀请函"
}
// 构造邮件内容
htmlContent := s.generateApprovalEmailHTML(inviteCode, language, expiresAt)
return s.sendEmail(toEmail, subject, htmlContent)
}
// SendInviteCodeRejectionEmail 发送邀请码申请拒绝邮件
func (s *EmailService) SendInviteCodeRejectionEmail(toEmail, rejectReason string) error {
subject := "关于您的 GoalfyAI 申请"
// 构造邮件内容
htmlContent := s.generateRejectionEmailHTML(rejectReason)
return s.sendEmail(toEmail, subject, htmlContent)
}
// generateApprovalEmailHTML 生成审批通过的邮件HTML
func (s *EmailService) generateApprovalEmailHTML(inviteCode, language string, expiresAt *time.Time) string {
if language == "en" {
return s.GenerateApprovalEmailEN(inviteCode, expiresAt)
}
return s.GenerateApprovalEmailZH(inviteCode, expiresAt)
}
// formatExpiryTimeEN 格式化过期时间为英文显示(全部显示为小时,向上取整)
func formatExpiryTimeEN(expiresAt *time.Time) string {
if expiresAt == nil {
return "until used"
}
now := time.Now()
if expiresAt.Before(now) {
return "expired"
}
duration := expiresAt.Sub(now)
hours := int(math.Ceil(duration.Hours())) // 向上取整
if hours <= 0 {
hours = 1 // 不足一小时算一小时
}
if hours == 1 {
return "1 hour"
}
return fmt.Sprintf("%d hours", hours)
}
// formatExpiryTimeZH 格式化过期时间为中文显示(全部显示为小时,向上取整)
func formatExpiryTimeZH(expiresAt *time.Time) string {
if expiresAt == nil {
return "永久有效"
}
now := time.Now()
if expiresAt.Before(now) {
return "已过期"
}
duration := expiresAt.Sub(now)
hours := int(math.Ceil(duration.Hours())) // 向上取整
if hours <= 0 {
hours = 1 // 不足一小时算一小时
}
return fmt.Sprintf("%d小时", hours)
}
// GenerateApprovalEmailEN 生成英文版审批通过邮件(导出用于测试)
func (s *EmailService) GenerateApprovalEmailEN(inviteCode string, expiresAt *time.Time) string {
expiryHours := formatExpiryTimeEN(expiresAt)
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<p>Thank you again for your interest in GoalfyAI!</p>
<p>We're excited to let you know that your request for beta access has been approved.<br>
You can now activate your GoalfyAI account using the link below:</p>
<p>👉 <a href="{{.InviteURL}}">Activate Your Account</a><br>
<span style="color: #666; font-size: 14px;">(This link is valid for {{.ExpiryHours}})</span></p>
<p>With this invite, you'll be among the first to explore our intelligent task execution system—designed for long-range, professional workflows. We'd love to hear your feedback as we continue to refine the experience.</p>
<p>Need help getting started? Visit our website for tips, use cases, and product updates:<br>
🌐 <a href="https://www.goalfyai.com">GoalfyAI.com</a></p>
<p>Thanks again for joining us on this journey.<br>
Let's build the future of intelligent tasks—together.</p>
<p>Warm regards,<br>
The GoalfyAI Team</p>
<hr style="border: none; border-top: 1px solid #ddd; margin: 40px 0;">
<p style="text-align: center; color: #999; font-size: 12px; line-height: 1.5;">
This email is sent automatically. Please do not reply.<br>
For any questions, please contact <a href="mailto:hi@goalfyai.com" style="color: #999;">hi@goalfyai.com</a>
</p>
</body>
</html>
`
tmpl, _ := template.New("approval_en").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]string{
"InviteCode": inviteCode,
"ExpiryHours": expiryHours,
"InviteURL": s.inviteURLPrefix + inviteCode,
})
return buf.String()
}
// GenerateApprovalEmailZH 生成中文版审批通过邮件(导出用于测试)
func (s *EmailService) GenerateApprovalEmailZH(inviteCode string, expiresAt *time.Time) string {
expiryHours := formatExpiryTimeZH(expiresAt)
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: 'Microsoft YaHei', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<p>感谢您对 GoalfyAI 的关注与支持!</p>
<p>我们很高兴通知您,您的内测申请已通过审核。<br>
请通过以下链接激活您的 GoalfyAI 账户:</p>
<p>👉 <a href="{{.InviteURL}}">点击激活账户</a><br>
<span style="color: #666; font-size: 14px;">(该链接在 {{.ExpiryHours}} 内有效)</span></p>
<p>通过本次邀请,您将率先体验我们为长周期专业任务打造的智能任务系统。我们也非常欢迎您在使用过程中给予反馈,帮助我们持续优化产品体验。</p>
<p>如需了解更多使用建议、典型场景或最新进展,欢迎访问官网:<br>
🌐 <a href="https://www.goalfyai.com">GoalfyAI.com</a></p>
<p>感谢您的加入,<br>
让我们一同开启智能任务的新篇章!</p>
<p>此致,<br>
GoalfyAI 团队</p>
<hr style="border: none; border-top: 1px solid #ddd; margin: 40px 0;">
<p style="text-align: center; color: #999; font-size: 12px; line-height: 1.5;">
本邮件为自动化发送,请勿回复。<br>
如有疑问请联系 <a href="mailto:hi@goalfyai.com" style="color: #999;">hi@goalfyai.com</a>
</p>
</body>
</html>
`
tmpl, _ := template.New("approval_zh").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]string{
"InviteCode": inviteCode,
"ExpiryHours": expiryHours,
"InviteURL": s.inviteURLPrefix + inviteCode,
})
return buf.String()
}
// generateRejectionEmailHTML 生成申请拒绝的邮件HTML
func (s *EmailService) generateRejectionEmailHTML(rejectReason string) string {
if rejectReason == "" {
rejectReason = "感谢您对 GoalfyAI 的关注。经过审核,您的账户申请暂未通过。"
}
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
color: #1a1a1a;
background-color: #f5f5f5;
padding: 40px 20px;
}
.email-wrapper {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.header {
background-color: #000000;
padding: 32px 40px;
text-align: center;
}
.header-logo {
font-size: 28px;
font-weight: 700;
color: #ffffff;
letter-spacing: -0.5px;
}
.content {
padding: 40px;
}
.greeting {
font-size: 16px;
color: #1a1a1a;
margin-bottom: 24px;
}
.message {
font-size: 15px;
color: #4a4a4a;
margin-bottom: 24px;
line-height: 1.7;
}
.reason-box {
background-color: #fafafa;
border-left: 3px solid #4a4a4a;
padding: 20px 24px;
margin: 24px 0;
border-radius: 0 4px 4px 0;
}
.reason-box p {
font-size: 15px;
color: #1a1a1a;
line-height: 1.7;
}
.support-box {
background-color: #fafafa;
border-radius: 6px;
padding: 20px 24px;
margin-top: 32px;
text-align: center;
}
.support-box p {
font-size: 14px;
color: #4a4a4a;
margin: 0;
}
.support-box a {
color: #000000;
text-decoration: none;
font-weight: 600;
}
.footer {
background-color: #fafafa;
padding: 32px 40px;
text-align: center;
border-top: 1px solid #e5e5e5;
}
.footer-brand {
font-size: 14px;
color: #1a1a1a;
font-weight: 600;
margin-bottom: 8px;
}
.footer-contact {
font-size: 13px;
color: #666666;
margin-top: 8px;
}
.footer-contact a {
color: #000000;
text-decoration: none;
}
@media only screen and (max-width: 600px) {
.content {
padding: 24px 20px;
}
.header {
padding: 24px 20px;
}
.reason-box {
padding: 16px 20px;
}
}
</style>
</head>
<body>
<div class="email-wrapper">
<div class="header">
<div class="header-logo">GoalfyAI</div>
</div>
<div class="content">
<div class="greeting">您好,</div>
<div class="message">
感谢您对 GoalfyAI 的关注和申请。
</div>
<div class="reason-box">
<p>{{.RejectReason}}</p>
</div>
<div class="message" style="margin-top: 24px;">
我们期待未来有机会为您提供服务。
</div>
<div class="support-box">
<p>如有任何疑问,欢迎联系我们</p>
<p style="margin-top: 8px;">
<a href="mailto:goalfymax@goalfyai.com">goalfymax@goalfyai.com</a>
</p>
</div>
</div>
<div class="footer">
<div class="footer-brand">GoalfyAI 团队</div>
<div class="footer-contact">
© 2025 GoalfyAI. All rights reserved.
</div>
</div>
</div>
</body>
</html>
`
tmpl, _ := template.New("rejection").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]string{
"RejectReason": rejectReason,
})
return buf.String()
}
// sendEmail 发送邮件的通用方法
func (s *EmailService) sendEmail(toEmail, subject, htmlContent string) error {
e := email.NewEmail()
e.From = s.sender
e.To = []string{toEmail}
e.Subject = subject
e.HTML = []byte(htmlContent)
// 创建SMTP认证
auth := smtp.PlainAuth("", s.username, s.password, s.host)
// 配置TLS
tlsConfig := &tls.Config{
ServerName: s.host,
}
// 发送邮件
addr := fmt.Sprintf("%s:%d", s.host, s.port)
// 如果是465端口使用SSL
if s.port == 465 {
return e.SendWithTLS(addr, auth, tlsConfig)
}
// 否则使用STARTTLS
return e.SendWithStartTLS(addr, auth, tlsConfig)
}
// SendBatchEmails 批量发送邮件(异步)
func (s *EmailService) SendBatchEmails(emails []string, subject, htmlContent string) []error {
errors := make([]error, len(emails))
for i, email := range emails {
errors[i] = s.sendEmail(email, subject, htmlContent)
// 避免发送过快
time.Sleep(100 * time.Millisecond)
}
return errors
}
// TestConnection 测试邮件服务器连接
func (s *EmailService) TestConnection() error {
addr := fmt.Sprintf("%s:%d", s.host, s.port)
// 尝试连接SMTP服务器
client, err := smtp.Dial(addr)
if err != nil {
return fmt.Errorf("failed to connect to SMTP server: %w", err)
}
defer client.Close()
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,321 @@
package services
import (
"bytes"
"encoding/json"
"fmt"
"goalfymax-admin/pkg/utils"
"io"
"net/http"
"time"
"goalfymax-admin/internal/config"
"goalfymax-admin/internal/models"
"go.uber.org/zap"
)
// GatewayClient 网关客户端
type GatewayClient struct {
baseURL string
timeout time.Duration
logger *utils.Logger
token string
}
// NewGatewayClient 创建网关客户端
func NewGatewayClient(baseURL string, timeout time.Duration, logger *utils.Logger) *GatewayClient {
return &GatewayClient{
baseURL: baseURL,
timeout: timeout,
logger: logger,
}
}
// acquireToken 从配置的登录接口获取新的网关 token不做过期判定
func (c *GatewayClient) acquireToken() (string, error) {
return "admin_control_0807", nil
cfg := config.GetConfig()
loginURL := cfg.Gateway.Auth.LoginURL
key := cfg.Gateway.Auth.Key
payload, _ := json.Marshal(map[string]string{"key": key})
req, err := http.NewRequest("POST", loginURL, bytes.NewBuffer(payload))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: c.timeout}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("login status: %d %s", resp.StatusCode, string(body))
}
var out struct {
Success bool `json:"success"`
Token string `json:"token"`
}
if err := json.Unmarshal(body, &out); err != nil {
return "", err
}
if !out.Success || out.Token == "" {
return "", fmt.Errorf("login failed: %s", string(body))
}
c.logger.Info("login succeeded", zap.String("token", out.Token))
c.token = out.Token
return c.token, nil
}
// doWithAuth 发送请求自动注入token若401则重取token并重试一次
func (c *GatewayClient) doWithAuth(req *http.Request) (*http.Response, error) {
if c.token == "" {
var err error
if c.token, err = c.acquireToken(); err != nil {
return nil, err
}
}
req.Header.Set("Authorization", "Bearer "+c.token)
client := &http.Client{Timeout: c.timeout}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusUnauthorized {
// 读尽响应体以复用连接
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if _, err := c.acquireToken(); err != nil {
return nil, err
}
// 重试一次
// 重新构建请求体仅当是可重读的bytes.Buffer这里假设上层构造的Body为bytes.Buffer或nil
// 如果是一次性流,上层应改为传入可重读体
if req.GetBody != nil {
bodyRc, _ := req.GetBody()
req.Body = bodyRc
}
req.Header.Set("Authorization", "Bearer "+c.token)
return client.Do(req)
}
return resp, nil
}
// GetQuotaHistory 获取配额历史数据
func (c *GatewayClient) GetQuotaHistory(req *models.QuotaHistoryRequest) (*models.QuotaHistoryResponse, error) {
// 构建请求URL
url := fmt.Sprintf("%s/aigateway-admin/api/quotas/history", c.baseURL)
// 序列化请求数据
jsonData, err := json.Marshal(req)
if err != nil {
c.logger.Error("序列化请求数据失败", zap.Error(err))
return nil, fmt.Errorf("序列化请求数据失败: %w", err)
}
// 创建HTTP请求
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
c.logger.Error("创建HTTP请求失败", zap.Error(err))
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
// 发送请求
c.logger.Debug("发送配额历史查询请求",
zap.String("url", url),
zap.String("data", string(jsonData)),
)
resp, err := c.doWithAuth(httpReq)
if err != nil {
c.logger.Error("发送HTTP请求失败", zap.Error(err))
return nil, fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
c.logger.Error("读取响应数据失败", zap.Error(err))
return nil, fmt.Errorf("读取响应数据失败: %w", err)
}
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
c.logger.Error("网关返回错误状态码",
zap.Int("status_code", resp.StatusCode),
zap.String("response", string(body)),
)
return nil, fmt.Errorf("网关返回错误状态码: %d", resp.StatusCode)
}
// 解析响应
var response models.QuotaHistoryResponse
if err := json.Unmarshal(body, &response); err != nil {
c.logger.Error("解析响应数据失败", zap.Error(err))
return nil, fmt.Errorf("解析响应数据失败: %w", err)
}
c.logger.Info("配额历史查询成功",
zap.Int("data_count", len(response.Data)),
zap.Bool("success", response.Success),
)
return &response, nil
}
// GetQuotaRules 获取配额规则列表代理到网关携带Authorization
func (c *GatewayClient) GetQuotaRules(authToken string) (*models.QuotaRulesResponse, error) {
url := fmt.Sprintf("%s/aigateway-admin/api/quotas/rules", c.baseURL)
httpReq, err := http.NewRequest("GET", url, nil)
if err != nil {
c.logger.Error("创建HTTP请求失败", zap.Error(err))
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
}
httpReq.Header.Set("Accept", "application/json")
if authToken != "" {
httpReq.Header.Set("Authorization", authToken)
}
c.logger.Debug("请求配额规则列表", zap.String("url", url))
resp, err := c.doWithAuth(httpReq)
if err != nil {
c.logger.Error("发送HTTP请求失败", zap.Error(err))
return nil, fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
c.logger.Error("读取响应数据失败", zap.Error(err))
return nil, fmt.Errorf("读取响应数据失败: %w", err)
}
if resp.StatusCode != http.StatusOK {
c.logger.Error("网关返回错误状态码", zap.Int("status_code", resp.StatusCode), zap.String("response", string(body)))
return nil, fmt.Errorf("网关返回错误状态码: %d", resp.StatusCode)
}
var response models.QuotaRulesResponse
if err := json.Unmarshal(body, &response); err != nil {
c.logger.Error("解析响应数据失败", zap.Error(err))
return nil, fmt.Errorf("解析响应数据失败: %w", err)
}
c.logger.Info("获取配额规则成功")
return &response, nil
}
// CreateQuotaRule 创建配额规则(代理网关)
func (c *GatewayClient) CreateQuotaRule(authToken string, body any) (*models.QuotaRulesResponse, error) {
url := fmt.Sprintf("%s/aigateway-admin/api/quotas/rules", c.baseURL)
payload, _ := json.Marshal(body)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if authToken != "" {
req.Header.Set("Authorization", authToken)
}
resp, err := c.doWithAuth(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gateway status: %d %s", resp.StatusCode, string(b))
}
var out models.QuotaRulesResponse
if err := json.Unmarshal(b, &out); err != nil {
return nil, err
}
return &out, nil
}
// UpdateQuotaRule 更新配额规则(代理网关)
func (c *GatewayClient) UpdateQuotaRule(authToken string, id string, body any) (*models.QuotaRulesResponse, error) {
url := fmt.Sprintf("%s/aigateway-admin/api/quotas/rules/%s", c.baseURL, id)
payload, _ := json.Marshal(body)
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(payload))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if authToken != "" {
req.Header.Set("Authorization", authToken)
}
resp, err := c.doWithAuth(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gateway status: %d %s", resp.StatusCode, string(b))
}
var out models.QuotaRulesResponse
if err := json.Unmarshal(b, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteQuotaRule 删除配额规则(代理网关)
func (c *GatewayClient) DeleteQuotaRule(authToken string, id string) (*models.QuotaRulesResponse, error) {
url := fmt.Sprintf("%s/aigateway-admin/api/quotas/rules/%s", c.baseURL, id)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return nil, err
}
if authToken != "" {
req.Header.Set("Authorization", authToken)
}
resp, err := c.doWithAuth(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gateway status: %d %s", resp.StatusCode, string(b))
}
var out models.QuotaRulesResponse
if err := json.Unmarshal(b, &out); err != nil {
return nil, err
}
return &out, nil
}
// HealthCheck 健康检查
func (c *GatewayClient) HealthCheck() error {
url := fmt.Sprintf("%s/aigateway-admin/health", c.baseURL)
client := &http.Client{
Timeout: c.timeout,
}
resp, err := client.Get(url)
if err != nil {
c.logger.Error("网关健康检查失败", zap.Error(err))
return fmt.Errorf("网关健康检查失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.logger.Error("网关健康检查返回错误状态码", zap.Int("status_code", resp.StatusCode))
return fmt.Errorf("网关健康检查返回错误状态码: %d", resp.StatusCode)
}
c.logger.Info("网关健康检查成功")
return nil
}

View File

@@ -0,0 +1,512 @@
package services
import (
"context"
"errors"
"fmt"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/redis"
"goalfymax-admin/pkg/utils"
"strconv"
goredis "github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
type GoalfyMaxUserService interface {
List(req *models.GoalfyMaxUserListRequest) ([]models.GoalfyMaxUser, int64, error)
GetByID(id uint) (*models.GoalfyMaxUser, error)
Create(req *models.GoalfyMaxUserCreateRequest) (*models.GoalfyMaxUser, error)
Update(id uint, req *models.GoalfyMaxUserUpdateRequest) (*models.GoalfyMaxUser, error)
Delete(id uint) error
Ban(id uint, req *models.GoalfyMaxUserBanRequest, adminID int) error
Unban(id uint) error
AddBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error
DeductBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error
}
type goalfyMaxUserService struct {
storage storage.GoalfyMaxUserStorage
messagePushService MessagePushService
ssoAdminService SSOAdminService
redisClient *redis.Client
balanceOperationLogStorage storage.BalanceOperationLogStorage
auditLogService AuditLogService
logger *utils.Logger
}
func NewGoalfyMaxUserService(s storage.GoalfyMaxUserStorage, messagePushService MessagePushService, ssoAdminService SSOAdminService, redisClient *redis.Client, balanceOperationLogStorage storage.BalanceOperationLogStorage, auditLogService AuditLogService, logger *utils.Logger) GoalfyMaxUserService {
return &goalfyMaxUserService{
storage: s,
messagePushService: messagePushService,
ssoAdminService: ssoAdminService,
redisClient: redisClient,
balanceOperationLogStorage: balanceOperationLogStorage,
auditLogService: auditLogService,
logger: logger,
}
}
func (s *goalfyMaxUserService) List(req *models.GoalfyMaxUserListRequest) ([]models.GoalfyMaxUser, int64, error) {
users, total, err := s.storage.List(req)
if err != nil {
return nil, 0, err
}
// 如果Redis客户端可用查询每个用户的余额
if s.redisClient != nil {
ctx := context.Background()
// 余额换算比例1美元 = 100,000000 (即100000000)
const balanceMultiplier = 100000000.0
for i := range users {
// 默认余额为0
balanceUSD := 0.0
// Redis key格式: GW:QU_{用户ID}
redisKey := fmt.Sprintf("GW:QU_%d", users[i].UserID)
// 从Redis获取余额
balanceStr, err := s.redisClient.Rdb.Get(ctx, redisKey).Result()
if err == nil {
// 解析余额值
balanceValue, err := strconv.ParseInt(balanceStr, 10, 64)
if err == nil {
// 转换为美元除以100000000
balanceUSD = float64(balanceValue) / balanceMultiplier
}
}
// 无论是否查询成功都设置余额查询失败则为0
users[i].Balance = &balanceUSD
}
} else {
// 如果Redis客户端不可用设置所有用户余额为0
zeroBalance := 0.0
for i := range users {
users[i].Balance = &zeroBalance
}
}
return users, total, nil
}
func (s *goalfyMaxUserService) GetByID(id uint) (*models.GoalfyMaxUser, error) {
return s.storage.GetByID(id)
}
func (s *goalfyMaxUserService) Create(req *models.GoalfyMaxUserCreateRequest) (*models.GoalfyMaxUser, error) {
// 检查用户名是否已存在
_, err := s.storage.GetByUsername(req.Username)
if err == nil {
return nil, errors.New("用户名已存在")
}
// 检查邮箱是否已存在
_, err = s.storage.GetByEmail(req.Email)
if err == nil {
return nil, errors.New("邮箱已存在")
}
// 1. 先调用SSO创建用户
ctx := context.Background()
ssoReq := &SSOAdminUserCreateRequest{
Username: req.Username,
Email: req.Email,
Phone: "", // 默认空手机号,可以根据需要修改
Password: req.Password, // 使用用户输入的密码
}
ssoUser, err := s.ssoAdminService.CreateUser(ctx, ssoReq)
if err != nil {
s.logger.Error("create goalfymax user failed", zap.Error(err))
return nil, fmt.Errorf("SSO创建用户失败: %w", err)
}
// 2. 设置系统角色:默认 custom而非固定角色ID
if err := s.ssoAdminService.SetSystemRole(ctx, ssoUser.ID, "custom"); err != nil {
// 如果设置失败,记录错误但不阻止用户创建
if s.logger != nil {
s.logger.Error("设置系统角色失败", zap.Int("user_id", ssoUser.ID), zap.Error(err))
}
}
// 3. 在本地数据库创建用户记录使用SSO返回的用户ID
// 如果未指定用户等级,默认为 normal
userLevelCode := req.UserLevelCode
if userLevelCode == "" {
userLevelCode = "normal"
}
// 如果未指定版本,默认为 1用户版
version := req.Version
if version == 0 {
version = 1
}
user := &models.GoalfyMaxUser{
UserID: ssoUser.ID, // 使用SSO返回的用户ID
Username: req.Username,
Email: req.Email,
Nickname: req.Nickname,
Avatar: req.Avatar,
UserLevelCode: userLevelCode,
Version: version,
IsBanned: false,
}
if err := s.storage.Create(user); err != nil {
// 如果本地创建失败需要清理SSO用户这里简化处理实际应该调用SSO删除接口
if s.logger != nil {
s.logger.Warn("本地用户创建失败但SSO用户已创建", zap.Int("sso_user_id", ssoUser.ID), zap.Error(err))
}
return nil, fmt.Errorf("创建用户失败: %w", err)
}
return user, nil
}
func (s *goalfyMaxUserService) Update(id uint, req *models.GoalfyMaxUserUpdateRequest) (*models.GoalfyMaxUser, error) {
user, err := s.storage.GetByID(id)
if err != nil {
return nil, errors.New("用户不存在")
}
if req.Nickname != "" {
user.Nickname = req.Nickname
}
if req.Email != "" {
user.Email = req.Email
}
if req.Avatar != "" {
user.Avatar = req.Avatar
}
if req.UserLevelCode != "" {
user.UserLevelCode = req.UserLevelCode
}
if req.Version != nil {
user.Version = *req.Version
}
if req.GoalfyHubPermission != nil {
user.GoalfyHubPermission = *req.GoalfyHubPermission
}
if err := s.storage.Update(user); err != nil {
return nil, err
}
return user, nil
}
func (s *goalfyMaxUserService) Delete(id uint) error {
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
return s.storage.Delete(id)
}
func (s *goalfyMaxUserService) Ban(id uint, req *models.GoalfyMaxUserBanRequest, adminID int) error {
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 执行封禁操作
if err := s.storage.SetBanned(id, req.Reason, adminID); err != nil {
return err
}
// 发送封禁通知
ctx := context.Background()
banMessage := &models.MessagePushRequest{
Title: "封禁通知",
Content: fmt.Sprintf("您的账户已被封禁。封禁原因:%s。如有疑问请联系客服。", req.Reason),
UserIDs: []int{user.UserID},
}
// 异步发送通知,不阻塞封禁操作
go func() {
if _, err := s.messagePushService.SendMessage(ctx, banMessage, adminID, "系统管理员"); err != nil {
// 记录错误日志,但不影响封禁操作
if s.logger != nil {
s.logger.Error("发送封禁通知失败", zap.Int("user_id", user.UserID), zap.Int("admin_id", adminID), zap.Error(err))
}
}
}()
return nil
}
func (s *goalfyMaxUserService) Unban(id uint) error {
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 执行解封操作
if err := s.storage.Unban(id); err != nil {
return err
}
// 发送解封通知
ctx := context.Background()
unbanMessage := &models.MessagePushRequest{
Title: "解封通知",
Content: "您的账户已被解封,现在可以正常使用所有功能。感谢您的理解与配合。",
UserIDs: []int{user.UserID},
}
// 异步发送通知,不阻塞解封操作
go func() {
if _, err := s.messagePushService.SendMessage(ctx, unbanMessage, 0, "系统管理员"); err != nil {
// 记录错误日志,但不影响解封操作
if s.logger != nil {
s.logger.Error("发送解封通知失败", zap.Int("user_id", user.UserID), zap.Error(err))
}
}
}()
return nil
}
func (s *goalfyMaxUserService) AddBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error {
// 检查用户是否存在
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 检查Redis客户端是否可用
if s.redisClient == nil {
return errors.New("Redis客户端不可用无法增加余额")
}
ctx := context.Background()
// 余额换算比例1美元 = 100,000000 (即100000000)
const balanceMultiplier = 100000000.0
// Redis key格式: GW:QU_{用户ID}
redisKey := fmt.Sprintf("GW:QU_%d", user.UserID)
// 获取操作前余额
balanceBeforeUSD := 0.0
balanceBeforeStr, err := s.redisClient.Rdb.Get(ctx, redisKey).Result()
if err == nil {
balanceBeforeValue, err := strconv.ParseInt(balanceBeforeStr, 10, 64)
if err == nil {
balanceBeforeUSD = float64(balanceBeforeValue) / balanceMultiplier
}
}
// 将美元金额转换为Redis存储的数值
amountToAdd := int64(req.Amount * balanceMultiplier)
// 使用Redis的INCRBY命令增加余额如果key不存在会自动创建并设置为0然后加上amountToAdd
newBalance, err := s.redisClient.Rdb.IncrBy(ctx, redisKey, amountToAdd).Result()
if err != nil {
return fmt.Errorf("增加余额失败: %w", err)
}
// 计算操作后余额
balanceAfterUSD := float64(newBalance) / balanceMultiplier
// 异步记录操作日志(保留原有的余额操作日志)
go func() {
logEntry := &models.BalanceOperationLog{
UserID: user.UserID,
OperationType: models.OperationTypeAdd,
Amount: req.Amount,
BalanceBefore: balanceBeforeUSD,
BalanceAfter: balanceAfterUSD,
OperatorID: operatorID,
OperatorName: operatorEmail,
Remark: "", // 可以后续扩展
}
if err := s.balanceOperationLogStorage.Create(logEntry); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录余额操作日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeAdd),
zap.Float64("amount", req.Amount),
zap.Error(err))
}
}
}()
// 异步记录审计日志
if s.auditLogService != nil {
go func() {
operationDetails := models.OperationDetails{
"action": "recharge",
"amount": req.Amount,
"currency": "USD",
"before_balance": balanceBeforeUSD,
"after_balance": balanceAfterUSD,
"remark": "",
}
auditLogReq := &models.CreateAuditLogRequest{
OperationType: models.OperationTypeBalanceAdjustment,
OperatorID: operatorID,
OperatorEmail: operatorEmail,
TargetType: models.TargetTypeUser,
TargetID: &user.UserID,
TargetEmail: user.Email,
OperationDetails: operationDetails,
IPAddress: ipAddress,
UserAgent: userAgent,
Status: models.AuditLogStatusSuccess,
}
if err := s.auditLogService.Create(auditLogReq); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录审计日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeBalanceAdjustment),
zap.Error(err))
}
}
}()
}
// 记录操作日志
if s.logger != nil {
s.logger.Info("用户余额增加成功",
zap.Int("user_id", user.UserID),
zap.String("username", user.Username),
zap.Float64("amount", req.Amount),
zap.Float64("balance_before", balanceBeforeUSD),
zap.Float64("balance_after", balanceAfterUSD),
zap.Int64("redis_balance_raw", newBalance),
zap.Int("operator_id", operatorID),
zap.String("operator_email", operatorEmail))
}
return nil
}
func (s *goalfyMaxUserService) DeductBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error {
// 检查用户是否存在
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 检查Redis客户端是否可用
if s.redisClient == nil {
return errors.New("Redis客户端不可用无法减少余额")
}
ctx := context.Background()
// 余额换算比例1美元 = 100,000000 (即100000000)
const balanceMultiplier = 100000000.0
// Redis key格式: GW:QU_{用户ID}
redisKey := fmt.Sprintf("GW:QU_%d", user.UserID)
// 将美元金额转换为Redis存储的数值
amountToDeduct := int64(req.Amount * balanceMultiplier)
// 先获取当前余额,检查是否足够
currentBalance, err := s.redisClient.Rdb.Get(ctx, redisKey).Int64()
if err != nil {
// 如果key不存在表示余额为0不能减少
if err == goredis.Nil {
return errors.New("余额不足,无法减少")
}
return fmt.Errorf("查询余额失败: %w", err)
}
// 计算操作前余额
balanceBeforeUSD := float64(currentBalance) / balanceMultiplier
// 检查余额是否足够
if currentBalance < amountToDeduct {
return errors.New("余额不足,无法减少")
}
// 使用Redis的DECRBY命令减少余额
newBalance, err := s.redisClient.Rdb.DecrBy(ctx, redisKey, amountToDeduct).Result()
if err != nil {
return fmt.Errorf("减少余额失败: %w", err)
}
// 计算操作后余额
balanceAfterUSD := float64(newBalance) / balanceMultiplier
// 异步记录操作日志(保留原有的余额操作日志)
go func() {
logEntry := &models.BalanceOperationLog{
UserID: user.UserID,
OperationType: models.OperationTypeDeduct,
Amount: req.Amount,
BalanceBefore: balanceBeforeUSD,
BalanceAfter: balanceAfterUSD,
OperatorID: operatorID,
OperatorName: operatorEmail,
Remark: "", // 可以后续扩展
}
if err := s.balanceOperationLogStorage.Create(logEntry); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录余额操作日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeDeduct),
zap.Float64("amount", req.Amount),
zap.Error(err))
}
}
}()
// 异步记录审计日志
if s.auditLogService != nil {
go func() {
operationDetails := models.OperationDetails{
"action": "deduct",
"amount": req.Amount,
"currency": "USD",
"before_balance": balanceBeforeUSD,
"after_balance": balanceAfterUSD,
"remark": "",
}
auditLogReq := &models.CreateAuditLogRequest{
OperationType: models.OperationTypeBalanceAdjustment,
OperatorID: operatorID,
OperatorEmail: operatorEmail,
TargetType: models.TargetTypeUser,
TargetID: &user.UserID,
TargetEmail: user.Email,
OperationDetails: operationDetails,
IPAddress: ipAddress,
UserAgent: userAgent,
Status: models.AuditLogStatusSuccess,
}
if err := s.auditLogService.Create(auditLogReq); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录审计日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeBalanceAdjustment),
zap.Error(err))
}
}
}()
}
// 记录操作日志
if s.logger != nil {
s.logger.Info("用户余额减少成功",
zap.Int("user_id", user.UserID),
zap.String("username", user.Username),
zap.Float64("amount", req.Amount),
zap.Float64("balance_before", balanceBeforeUSD),
zap.Float64("balance_after", balanceAfterUSD),
zap.Int64("redis_balance_raw", newBalance),
zap.Int("operator_id", operatorID),
zap.String("operator_email", operatorEmail))
}
return nil
}

View File

@@ -0,0 +1,310 @@
package services
import (
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
"goalfymax-admin/internal/models"
)
type InviteCodeApplicationService struct {
db *gorm.DB
emailService *EmailService
}
// NewInviteCodeApplicationService 创建邀请码申请服务
func NewInviteCodeApplicationService(db *gorm.DB) *InviteCodeApplicationService {
return &InviteCodeApplicationService{
db: db,
emailService: NewEmailService(),
}
}
// SubmitApplication 提交邀请码申请(官网使用)
func (s *InviteCodeApplicationService) SubmitApplication(req *models.InviteCodeApplicationCreateRequest) (*models.InviteCodeApplication, error) {
// 检查是否已经有待处理或已通过的申请
var existingApp models.InviteCodeApplication
err := s.db.Where("email = ? AND status IN (?, ?)",
req.Email,
models.ApplicationStatusPending,
models.ApplicationStatusApproved,
).First(&existingApp).Error
if err == nil {
// 如果找到了记录,说明已经有申请
if existingApp.Status == models.ApplicationStatusPending {
return nil, errors.New("您已经提交过申请,请等待审核")
}
if existingApp.Status == models.ApplicationStatusApproved {
return nil, errors.New("您的申请已通过,请检查邮箱")
}
}
// 设置默认语言
language := req.Language
if language == "" {
language = "zh"
}
// 创建新的申请
application := &models.InviteCodeApplication{
Email: req.Email,
Reason: req.Reason,
Language: language,
Status: models.ApplicationStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.db.Create(application).Error; err != nil {
return nil, fmt.Errorf("创建申请失败: %w", err)
}
return application, nil
}
// GetApplicationList 获取申请列表(后台管理使用)
func (s *InviteCodeApplicationService) GetApplicationList(req *models.InviteCodeApplicationListRequest) (*models.InviteCodeApplicationListResponse, error) {
var applications []models.InviteCodeApplication
var total int64
query := s.db.Model(&models.InviteCodeApplication{})
// 添加过滤条件
if req.Email != "" {
query = query.Where("email LIKE ?", "%"+req.Email+"%")
}
if req.Status != "" {
query = query.Where("status = ?", req.Status)
}
if req.StartTime != "" {
query = query.Where("created_at >= ?", req.StartTime)
}
if req.EndTime != "" {
endTime, _ := time.Parse("2006-01-02", req.EndTime)
query = query.Where("created_at < ?", endTime.AddDate(0, 0, 1))
}
// 获取总数
query.Count(&total)
// 分页查询
offset := (req.Page - 1) * req.Size
err := query.
Preload("InviteCode").
Order("created_at DESC").
Limit(req.Size).
Offset(offset).
Find(&applications).Error
if err != nil {
return nil, fmt.Errorf("查询申请列表失败: %w", err)
}
return &models.InviteCodeApplicationListResponse{
List: applications,
Total: total,
}, nil
}
// GetStatistics 获取申请统计
func (s *InviteCodeApplicationService) GetStatistics() (*models.InviteCodeApplicationStatistics, error) {
var stats models.InviteCodeApplicationStatistics
// 待处理数量
var pendingCount int64
s.db.Model(&models.InviteCodeApplication{}).
Where("status = ?", models.ApplicationStatusPending).
Count(&pendingCount)
stats.TotalPending = int(pendingCount)
// 已通过数量
var approvedCount int64
s.db.Model(&models.InviteCodeApplication{}).
Where("status = ?", models.ApplicationStatusApproved).
Count(&approvedCount)
stats.TotalApproved = int(approvedCount)
// 已拒绝数量
var rejectedCount int64
s.db.Model(&models.InviteCodeApplication{}).
Where("status = ?", models.ApplicationStatusRejected).
Count(&rejectedCount)
stats.TotalRejected = int(rejectedCount)
// 今日申请数量
var todayCount int64
todayStart := time.Now().Truncate(24 * time.Hour)
s.db.Model(&models.InviteCodeApplication{}).
Where("created_at >= ?", todayStart).
Count(&todayCount)
stats.TodayApplied = int(todayCount)
return &stats, nil
}
// ApproveApplication 审批通过申请
func (s *InviteCodeApplicationService) ApproveApplication(req *models.InviteCodeApplicationApproveRequest, approvedBy string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// 获取申请信息
var application models.InviteCodeApplication
if err := tx.First(&application, req.ApplicationID).Error; err != nil {
return fmt.Errorf("申请不存在: %w", err)
}
if application.Status != models.ApplicationStatusPending {
return errors.New("申请状态不是待处理,无法审批")
}
// 设置有效期默认3天72小时
validDays := req.ValidDays
if validDays <= 0 {
validDays = 3
}
expiresAt := time.Now().AddDate(0, 0, validDays)
// 创建邀请码
inviteCode := &models.InviteCode{
Code: s.generateInviteCode(),
IsUsed: false,
ClientID: "xRpT9mgNpt2YvoY9z4FToA", // 默认为正式版客户端
ExpiresAt: &expiresAt,
CreatedAt: time.Now(),
}
if err := tx.Create(inviteCode).Error; err != nil {
return fmt.Errorf("创建邀请码失败: %w", err)
}
// 更新申请状态
now := time.Now()
updates := map[string]interface{}{
"status": models.ApplicationStatusApproved,
"invite_code_id": inviteCode.ID,
"approved_at": now,
"approved_by": approvedBy,
"updated_at": now,
}
if err := tx.Model(&application).Updates(updates).Error; err != nil {
return fmt.Errorf("更新申请状态失败: %w", err)
}
// 发送邮件(异步,不影响事务)
go func() {
// 获取语言设置,默认为中文
lang := application.Language
if lang == "" {
lang = "zh"
}
if err := s.emailService.SendInviteCodeApprovalEmail(application.Email, inviteCode.Code, lang, &expiresAt); err != nil {
// 记录邮件发送失败,但不回滚事务
fmt.Printf("发送审批通过邮件失败: %v\n", err)
} else {
// 更新邮件发送时间
emailSentAt := time.Now()
tx.Model(&application).Update("email_sent_at", emailSentAt)
}
}()
return nil
})
}
// RejectApplication 审批拒绝申请
func (s *InviteCodeApplicationService) RejectApplication(req *models.InviteCodeApplicationRejectRequest, approvedBy string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// 获取申请信息
var application models.InviteCodeApplication
if err := tx.First(&application, req.ApplicationID).Error; err != nil {
return fmt.Errorf("申请不存在: %w", err)
}
if application.Status != models.ApplicationStatusPending {
return errors.New("申请状态不是待处理,无法审批")
}
// 更新申请状态
now := time.Now()
updates := map[string]interface{}{
"status": models.ApplicationStatusRejected,
"reject_reason": req.RejectReason,
"approved_at": now,
"approved_by": approvedBy,
"updated_at": now,
}
if err := tx.Model(&application).Updates(updates).Error; err != nil {
return fmt.Errorf("更新申请状态失败: %w", err)
}
// 拒绝操作不发送邮件通知
return nil
})
}
// BatchApproveApplications 批量审批通过
func (s *InviteCodeApplicationService) BatchApproveApplications(req *models.InviteCodeApplicationBatchApproveRequest, approvedBy string) error {
for _, appID := range req.ApplicationIDs {
approveReq := &models.InviteCodeApplicationApproveRequest{
ApplicationID: appID,
ValidDays: req.ValidDays,
}
if err := s.ApproveApplication(approveReq, approvedBy); err != nil {
// 记录错误但继续处理其他申请
fmt.Printf("审批申请 %d 失败: %v\n", appID, err)
}
}
return nil
}
// BatchRejectApplications 批量审批拒绝
func (s *InviteCodeApplicationService) BatchRejectApplications(req *models.InviteCodeApplicationBatchRejectRequest, approvedBy string) error {
for _, appID := range req.ApplicationIDs {
rejectReq := &models.InviteCodeApplicationRejectRequest{
ApplicationID: appID,
RejectReason: req.RejectReason,
}
if err := s.RejectApplication(rejectReq, approvedBy); err != nil {
// 记录错误但继续处理其他申请
fmt.Printf("拒绝申请 %d 失败: %v\n", appID, err)
}
}
return nil
}
// generateInviteCode 生成唯一的邀请码格式GFY-XXXXXXXX-XXXX
func (s *InviteCodeApplicationService) generateInviteCode() string {
// 生成UUID并转换为大写
uuidStr := uuid.New().String()
// 移除UUID中的连字符并转为大写
cleaned := ""
for _, c := range uuidStr {
if c != '-' {
cleaned += string(c)
}
}
cleaned = strings.ToUpper(cleaned)
// 截取需要的部分GFY-8位-4位
// 格式GFY-XXXXXXXX-XXXX
part1 := cleaned[0:8] // 8位
part2 := cleaned[8:12] // 4位
return fmt.Sprintf("GFY-%s-%s", part1, part2)
}
// GetPendingApplicationsCount 获取待处理申请数量
func (s *InviteCodeApplicationService) GetPendingApplicationsCount() (int64, error) {
var count int64
err := s.db.Model(&models.InviteCodeApplication{}).
Where("status = ?", models.ApplicationStatusPending).
Count(&count).Error
return count, err
}

View File

@@ -0,0 +1,291 @@
package services
import (
"fmt"
"math/rand"
"strings"
"time"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
)
type InviteCodeService interface {
Create(req *models.InviteCodeCreateRequest) ([]*models.InviteCode, error)
GetByID(id uint) (*models.InviteCode, error)
GetByCode(code string) (*models.InviteCode, error)
List(req *models.InviteCodeListRequest) (*models.InviteCodeListResponse, error)
Update(id uint, req *models.InviteCodeUpdateRequest) (*models.InviteCode, error)
Delete(id uint) error
GetStatistics() (*models.InviteCodeStatistics, error)
MarkAsUsed(code string) error
ValidateInviteCode(code string) error
}
type inviteCodeService struct {
storage storage.InviteCodeStorage
emailService *EmailService
}
func NewInviteCodeService(storage storage.InviteCodeStorage) InviteCodeService {
return &inviteCodeService{
storage: storage,
emailService: NewEmailService(),
}
}
func (s *inviteCodeService) Create(req *models.InviteCodeCreateRequest) ([]*models.InviteCode, error) {
if req == nil {
return nil, fmt.Errorf("请求参数不能为空")
}
// 设置过期时间
var expiresAt *time.Time
if req.ExpiresAt != nil {
expiresAt = req.ExpiresAt
} else {
// 默认30天后过期
defaultExpiry := time.Now().AddDate(0, 0, 30)
expiresAt = &defaultExpiry
}
// 设置用户等级ID如果未提供则默认为1
var userLevelID *uint
if req.UserLevelID != nil {
userLevelID = req.UserLevelID
} else {
defaultUserLevelID := uint(1)
userLevelID = &defaultUserLevelID
}
// 如果提供了邮箱列表,为每个邮箱创建一个邀请码
if len(req.Emails) > 0 {
// 对邮箱列表进行去重处理
emailMap := make(map[string]bool)
uniqueEmails := make([]string, 0, len(req.Emails))
for _, email := range req.Emails {
// 去除空格并转换为小写进行去重
email = strings.TrimSpace(strings.ToLower(email))
if email == "" {
continue
}
// 如果邮箱已存在,跳过
if emailMap[email] {
continue
}
emailMap[email] = true
uniqueEmails = append(uniqueEmails, email)
}
var inviteCodes []*models.InviteCode
for _, email := range uniqueEmails {
// 生成唯一邀请码
var code string
for {
code = s.generateCode()
existingCode, err := s.storage.GetByCode(code)
if err != nil || existingCode == nil {
break // 邀请码不存在,可以使用
}
}
inviteCode := &models.InviteCode{
Code: code,
ClientID: req.ClientID,
Email: email,
UserLevelID: userLevelID,
ExpiresAt: expiresAt,
CreatedAt: time.Now(),
}
if err := s.storage.Create(inviteCode); err != nil {
return nil, fmt.Errorf("创建邀请码失败: %w", err)
}
// 发送邀请邮件(如果提供了邮箱)
// 使用 goroutine 异步发送,避免阻塞
if email != "" && s.emailService != nil {
go func(emailAddr, code string) {
// 使用默认语言 "zh" 发送邮件
if err := s.emailService.SendInviteCodeApprovalEmail(emailAddr, code, "zh", expiresAt); err != nil {
// 记录错误但不影响创建流程,邮件发送失败不影响邀请码创建
fmt.Printf("[InviteCodeService] 发送邀请邮件失败: %v (邀请码: %s, 邮箱: %s)\n", err, code, emailAddr)
} else {
fmt.Printf("[InviteCodeService] 邀请邮件发送成功 (邀请码: %s, 邮箱: %s)\n", code, emailAddr)
}
}(email, inviteCode.Code)
}
inviteCodes = append(inviteCodes, inviteCode)
}
if len(inviteCodes) == 0 {
return nil, fmt.Errorf("没有有效的邮箱地址")
}
return inviteCodes, nil
}
// 如果没有提供邮箱,只创建一个邀请码(向后兼容)
var code string
for {
code = s.generateCode()
existingCode, err := s.storage.GetByCode(code)
if err != nil || existingCode == nil {
break // 邀请码不存在,可以使用
}
}
inviteCode := &models.InviteCode{
Code: code,
ClientID: req.ClientID,
Email: "",
UserLevelID: userLevelID,
ExpiresAt: expiresAt,
CreatedAt: time.Now(),
}
if err := s.storage.Create(inviteCode); err != nil {
return nil, fmt.Errorf("创建邀请码失败: %w", err)
}
return []*models.InviteCode{inviteCode}, nil
}
func (s *inviteCodeService) GetByID(id uint) (*models.InviteCode, error) {
return s.storage.GetByID(id)
}
func (s *inviteCodeService) GetByCode(code string) (*models.InviteCode, error) {
return s.storage.GetByCode(code)
}
func (s *inviteCodeService) List(req *models.InviteCodeListRequest) (*models.InviteCodeListResponse, error) {
inviteCodes, total, err := s.storage.List(req)
if err != nil {
return nil, fmt.Errorf("获取邀请码列表失败: %w", err)
}
return &models.InviteCodeListResponse{
List: inviteCodes,
Total: total,
}, nil
}
func (s *inviteCodeService) Update(id uint, req *models.InviteCodeUpdateRequest) (*models.InviteCode, error) {
// 获取现有邀请码
inviteCode, err := s.storage.GetByID(id)
if err != nil {
return nil, fmt.Errorf("邀请码不存在")
}
// 检查是否已被使用
if inviteCode.IsUsed {
return nil, fmt.Errorf("已使用的邀请码无法修改")
}
// 更新客户端ID
if req != nil && req.ClientID != "" {
inviteCode.ClientID = req.ClientID
}
// 更新邮箱
if req != nil && req.Email != "" {
inviteCode.Email = req.Email
}
// 更新用户等级ID
if req != nil && req.UserLevelID != nil {
inviteCode.UserLevelID = req.UserLevelID
}
// 更新过期时间
if req != nil && req.ExpiresAt != nil {
inviteCode.ExpiresAt = req.ExpiresAt
}
// 保存更新
if err := s.storage.Update(inviteCode); err != nil {
return nil, fmt.Errorf("更新邀请码失败: %w", err)
}
return inviteCode, nil
}
func (s *inviteCodeService) Delete(id uint) error {
_, err := s.storage.GetByID(id)
if err != nil {
return fmt.Errorf("邀请码不存在")
}
if err := s.storage.Delete(id); err != nil {
return fmt.Errorf("删除邀请码失败: %w", err)
}
return nil
}
func (s *inviteCodeService) GetStatistics() (*models.InviteCodeStatistics, error) {
return s.storage.GetStatistics()
}
func (s *inviteCodeService) MarkAsUsed(code string) error {
inviteCode, err := s.storage.GetByCode(code)
if err != nil {
return fmt.Errorf("邀请码不存在")
}
if inviteCode.IsUsed {
return fmt.Errorf("邀请码已被使用")
}
// 检查是否过期
if s.storage.IsExpired(inviteCode) {
return fmt.Errorf("邀请码已过期")
}
inviteCode.IsUsed = true
if err := s.storage.Update(inviteCode); err != nil {
return fmt.Errorf("标记邀请码为已使用失败: %w", err)
}
return nil
}
// ValidateInviteCode 验证邀请码是否有效(未使用且未过期)
func (s *inviteCodeService) ValidateInviteCode(code string) error {
inviteCode, err := s.storage.GetByCode(code)
if err != nil {
return fmt.Errorf("邀请码不存在")
}
if inviteCode.IsUsed {
return fmt.Errorf("邀请码已被使用")
}
// 检查是否过期
if s.storage.IsExpired(inviteCode) {
return fmt.Errorf("邀请码已过期")
}
return nil
}
func (s *inviteCodeService) generateCode() string {
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// 生成8位随机字符
part1 := make([]byte, 8)
for i := range part1 {
part1[i] = chars[rand.Intn(len(chars))]
}
// 生成4位随机字符
part2 := make([]byte, 4)
for i := range part2 {
part2[i] = chars[rand.Intn(len(chars))]
}
return fmt.Sprintf("GFY-%s-%s", string(part1), string(part2))
}

View File

@@ -0,0 +1,85 @@
package services
import (
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// LogService 日志服务接口
type LogService interface {
CreateLoginLog(log *models.LoginLog) error
CreateOperationLog(log *models.OperationLog) error
GetLoginLogs(req *models.LoginLogListRequest) ([]models.LoginLog, int64, error)
GetOperationLogs(req *models.OperationLogListRequest) ([]models.OperationLog, int64, error)
DeleteLoginLogs(beforeDate string) error
DeleteOperationLogs(beforeDate string) error
}
type logService struct {
logStorage storage.LogStorage
logger *utils.Logger
}
// NewLogService 创建日志服务实例
func NewLogService(logStorage storage.LogStorage, logger *utils.Logger) LogService {
return &logService{
logStorage: logStorage,
logger: logger,
}
}
// CreateLoginLog 创建登录日志
func (s *logService) CreateLoginLog(log *models.LoginLog) error {
err := s.logStorage.CreateLoginLog(log)
if err != nil {
s.logger.Error("创建登录日志失败", zap.Error(err))
return err
}
return nil
}
// CreateOperationLog 创建操作日志
func (s *logService) CreateOperationLog(log *models.OperationLog) error {
err := s.logStorage.CreateOperationLog(log)
if err != nil {
s.logger.Error("创建操作日志失败", zap.Error(err))
return err
}
return nil
}
// GetLoginLogs 获取登录日志列表
func (s *logService) GetLoginLogs(req *models.LoginLogListRequest) ([]models.LoginLog, int64, error) {
return s.logStorage.GetLoginLogs(req)
}
// GetOperationLogs 获取操作日志列表
func (s *logService) GetOperationLogs(req *models.OperationLogListRequest) ([]models.OperationLog, int64, error) {
return s.logStorage.GetOperationLogs(req)
}
// DeleteLoginLogs 删除指定日期之前的登录日志
func (s *logService) DeleteLoginLogs(beforeDate string) error {
err := s.logStorage.DeleteLoginLogs(beforeDate)
if err != nil {
s.logger.Error("删除登录日志失败", zap.Error(err))
return err
}
s.logger.Info("登录日志删除成功", zap.String("beforeDate", beforeDate))
return nil
}
// DeleteOperationLogs 删除指定日期之前的操作日志
func (s *logService) DeleteOperationLogs(beforeDate string) error {
err := s.logStorage.DeleteOperationLogs(beforeDate)
if err != nil {
s.logger.Error("删除操作日志失败", zap.Error(err))
return err
}
s.logger.Info("操作日志删除成功", zap.String("beforeDate", beforeDate))
return nil
}

View File

@@ -0,0 +1,393 @@
package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"goalfymax-admin/internal/config"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"html"
"html/template"
"io"
"net/http"
"time"
"unicode/utf8"
)
// MessagePushService 消息推送服务接口
type MessagePushService interface {
SendMessage(ctx context.Context, req *models.MessagePushRequest, senderID int, senderName string) (*models.MessagePushResponse, error)
GetPushLogs(ctx context.Context, req *models.MessagePushListRequest) (*models.MessagePushListResponse, error)
GetPushLogByID(ctx context.Context, id int64) (*models.MessagePushLog, error)
SearchUsers(ctx context.Context, req *models.UserSearchRequest) (*models.UserSearchResponse, error)
}
type messagePushService struct {
storage storage.MessagePushStorage
goalfymaxClient *http.Client
goalfymaxURL string
retryCount int
retryInterval time.Duration
emailService *EmailService
goalfyMaxUserStorage storage.GoalfyMaxUserStorage
}
// NewMessagePushService 创建消息推送服务实例
func NewMessagePushService() MessagePushService {
cfg := config.GetConfig()
return &messagePushService{
storage: storage.NewMessagePushStorage(),
goalfymaxClient: &http.Client{
Timeout: time.Duration(cfg.MessagePush.Timeout) * time.Second,
},
goalfymaxURL: cfg.MessagePush.GoalfyMaxBaseURL,
retryCount: cfg.MessagePush.RetryCount,
retryInterval: time.Duration(cfg.MessagePush.RetryInterval) * time.Millisecond,
emailService: NewEmailService(),
goalfyMaxUserStorage: storage.NewGoalfyMaxUserStorage(),
}
}
// SendMessage 发送消息
func (s *messagePushService) SendMessage(ctx context.Context, req *models.MessagePushRequest, senderID int, senderName string) (*models.MessagePushResponse, error) {
// 参数校验
if len(req.UserIDs) == 0 {
return nil, fmt.Errorf("目标用户不能为空")
}
if len(req.Title) == 0 {
return nil, fmt.Errorf("消息标题不能为空")
}
titleRuneCount := utf8.RuneCountInString(req.Title)
if titleRuneCount > 100 {
return nil, fmt.Errorf("消息标题不能超过100字符")
}
if len(req.Content) == 0 {
return nil, fmt.Errorf("消息内容不能为空")
}
contentRuneCount := utf8.RuneCountInString(req.Content)
if contentRuneCount > 2000 {
return nil, fmt.Errorf("消息内容不能超过2000字符")
}
// 序列化目标用户
targetUsersJSON, err := storage.SerializeTargetUsers(req.UserIDs)
if err != nil {
return nil, fmt.Errorf("序列化目标用户失败: %w", err)
}
// 创建推送记录
log := &models.MessagePushLog{
Title: req.Title,
Content: req.Content,
TargetUsers: targetUsersJSON,
SenderID: senderID,
SenderName: senderName,
Status: 0, // 待发送
}
if err := s.storage.Create(ctx, log); err != nil {
return nil, fmt.Errorf("创建推送记录失败: %w", err)
}
// 异步发送消息
go s.sendToGoalfyMaxAsync(context.Background(), log.ID, req.Title, req.Content, req.UserIDs)
return &models.MessagePushResponse{
LogID: log.ID,
SuccessCount: 0,
FailCount: 0,
}, nil
}
// sendToGoalfyMaxAsync 异步发送到GoalfyMax
func (s *messagePushService) sendToGoalfyMaxAsync(ctx context.Context, logID int64, title string, content string, userIDs []int) {
// 更新状态为发送中
s.storage.UpdateStatus(ctx, logID, 1, 0, 0, "")
successCount := 0
failCount := 0
var errorMessage string
// 调用GoalfyMax接口
err := s.callGoalfyMaxAPI(ctx, title, content, userIDs)
if err != nil {
failCount = len(userIDs)
errorMessage = err.Error()
s.storage.UpdateStatus(ctx, logID, 3, successCount, failCount, errorMessage)
return
}
// 发送成功,同时发送邮件
s.sendEmails(ctx, title, content, userIDs)
// 发送成功
successCount = len(userIDs)
s.storage.UpdateStatus(ctx, logID, 2, successCount, failCount, "")
}
// sendEmails 发送邮件给用户
func (s *messagePushService) sendEmails(ctx context.Context, title string, content string, userIDs []int) {
// 生成邮件HTML内容
htmlContent := s.generateMessageEmailHTML(title, content)
// 循环发送邮件给每个用户
for _, userID := range userIDs {
user, err := s.goalfyMaxUserStorage.GetByUserID(userID)
if err != nil {
// 如果获取用户失败,跳过该用户
continue
}
if user.Email != "" {
// 发送邮件
if err := s.emailService.sendEmail(user.Email, title, htmlContent); err != nil {
// 记录错误但不中断其他邮件的发送
continue
}
// 避免发送过快,每次发送后稍作延迟
time.Sleep(100 * time.Millisecond)
}
}
}
// generateMessageEmailHTML 生成消息通知邮件的HTML内容
func (s *messagePushService) generateMessageEmailHTML(title, content string) string {
// 转义HTML特殊字符然后将换行符转换为<br>
escapedContent := html.EscapeString(content)
// 将换行符转换为HTML换行
contentHTML := fmt.Sprintf(`<div style="white-space: pre-wrap;">%s</div>`, escapedContent)
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
color: #1a1a1a;
background-color: #f5f5f5;
padding: 40px 20px;
}
.email-wrapper {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.header {
background-color: #000000;
padding: 32px 40px;
text-align: center;
}
.header-logo {
font-size: 28px;
font-weight: 700;
color: #ffffff;
letter-spacing: -0.5px;
}
.content {
padding: 40px;
}
.title {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 24px;
line-height: 1.4;
}
.message-content {
font-size: 15px;
color: #4a4a4a;
line-height: 1.7;
background-color: #fafafa;
padding: 20px 24px;
border-radius: 6px;
margin-bottom: 24px;
}
.footer {
background-color: #fafafa;
padding: 32px 40px;
text-align: center;
border-top: 1px solid #e5e5e5;
}
.footer-brand {
font-size: 14px;
color: #1a1a1a;
font-weight: 600;
margin-bottom: 8px;
}
.footer-contact {
font-size: 13px;
color: #666666;
margin-top: 8px;
}
@media only screen and (max-width: 600px) {
.content {
padding: 24px 20px;
}
.header {
padding: 24px 20px;
}
}
</style>
</head>
<body>
<div class="email-wrapper">
<div class="header">
<div class="header-logo">GoalfyAI</div>
</div>
<div class="content">
<div class="title">{{.Title}}</div>
<div class="message-content">{{.Content}}</div>
</div>
<div class="footer">
<div class="footer-brand">GoalfyAI 团队</div>
<div class="footer-contact">
© 2025 GoalfyAI. All rights reserved.
</div>
</div>
</div>
</body>
</html>
`
tmpl, _ := template.New("message").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]interface{}{
"Title": html.EscapeString(title),
"Content": template.HTML(contentHTML),
})
return buf.String()
}
// callGoalfyMaxAPI 调用GoalfyMax API
func (s *messagePushService) callGoalfyMaxAPI(ctx context.Context, title string, content string, userIDs []int) error {
// 构建请求体
reqBody := map[string]interface{}{
"title": title,
"content": content,
"user_ids": userIDs,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("序列化请求失败: %w", err)
}
var lastErr error
for i := 0; i < s.retryCount; i++ {
// 创建HTTP请求
req, err := http.NewRequestWithContext(ctx, "POST", s.goalfymaxURL+"/api/notifications/send", bytes.NewBuffer(jsonData))
if err != nil {
lastErr = fmt.Errorf("创建请求失败: %w", err)
continue
}
// 设置固定Token
req.Header.Set("Authorization", "Bearer admin-message-push")
req.Header.Set("Content-Type", "application/json")
// 发送请求
resp, err := s.goalfymaxClient.Do(req)
if err != nil {
lastErr = fmt.Errorf("发送请求失败 (尝试 %d/%d): %w", i+1, s.retryCount, err)
if i < s.retryCount-1 {
time.Sleep(s.retryInterval)
}
continue
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
lastErr = fmt.Errorf("读取响应失败: %w", err)
if i < s.retryCount-1 {
time.Sleep(s.retryInterval)
}
continue
}
if resp.StatusCode != http.StatusOK {
lastErr = fmt.Errorf("GoalfyMax接口返回错误: %d, 响应: %s", resp.StatusCode, string(body))
if i < s.retryCount-1 {
time.Sleep(s.retryInterval)
}
continue
}
// 成功返回nil
return nil
}
// 所有重试都失败
return lastErr
}
// GetPushLogs 获取推送记录列表
func (s *messagePushService) GetPushLogs(ctx context.Context, req *models.MessagePushListRequest) (*models.MessagePushListResponse, error) {
// 参数校验
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 || req.PageSize > 100 {
req.PageSize = 10
}
// 调用存储层
logs, total, err := s.storage.List(ctx, req)
if err != nil {
return nil, fmt.Errorf("获取推送记录失败: %w", err)
}
return &models.MessagePushListResponse{
List: logs,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// GetPushLogByID 根据ID获取推送记录
func (s *messagePushService) GetPushLogByID(ctx context.Context, id int64) (*models.MessagePushLog, error) {
if id <= 0 {
return nil, fmt.Errorf("推送记录ID无效")
}
log, err := s.storage.GetByID(ctx, id)
if err != nil {
return nil, err
}
return log, nil
}
// SearchUsers 搜索用户
func (s *messagePushService) SearchUsers(ctx context.Context, req *models.UserSearchRequest) (*models.UserSearchResponse, error) {
// 参数校验
if req.Limit <= 0 || req.Limit > 1000 {
req.Limit = 20
}
users, err := s.storage.SearchUsers(ctx, req.Keyword, req.Limit)
if err != nil {
return nil, fmt.Errorf("搜索用户失败: %w", err)
}
return &models.UserSearchResponse{
Users: users,
Total: len(users),
}, nil
}

View File

@@ -0,0 +1,131 @@
package services
import (
"errors"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// PageService 页面服务接口
type PageService interface {
Create(req *models.PageCreateRequest) (*models.Page, error)
GetByID(id uint) (*models.Page, error)
Update(id uint, req *models.PageUpdateRequest) (*models.Page, error)
Delete(id uint) error
List(req *models.PageListRequest) ([]models.Page, int64, error)
}
type pageService struct {
pageStorage storage.PageStorage
logger *utils.Logger
}
// NewPageService 创建页面服务实例
func NewPageService(pageStorage storage.PageStorage, logger *utils.Logger) PageService {
return &pageService{
pageStorage: pageStorage,
logger: logger,
}
}
// Create 创建页面
func (s *pageService) Create(req *models.PageCreateRequest) (*models.Page, error) {
// 检查页面路径是否已存在
_, err := s.pageStorage.GetByPath(req.Path)
if err == nil {
return nil, errors.New("页面路径已存在")
}
// 创建页面
page := &models.Page{
Name: req.Name,
Path: req.Path,
Icon: req.Icon,
SortOrder: req.SortOrder,
IsActive: req.IsActive,
}
err = s.pageStorage.Create(page)
if err != nil {
s.logger.Error("创建页面失败", zap.Error(err))
return nil, errors.New("创建页面失败")
}
s.logger.Info("页面创建成功", zap.String("name", page.Name))
return page, nil
}
// GetByID 根据ID获取页面
func (s *pageService) GetByID(id uint) (*models.Page, error) {
page, err := s.pageStorage.GetByID(id)
if err != nil {
s.logger.Error("获取页面失败", zap.Uint("id", id), zap.Error(err))
return nil, errors.New("页面不存在")
}
return page, nil
}
// Update 更新页面
func (s *pageService) Update(id uint, req *models.PageUpdateRequest) (*models.Page, error) {
// 获取现有页面
page, err := s.pageStorage.GetByID(id)
if err != nil {
s.logger.Error("页面不存在", zap.Uint("id", id), zap.Error(err))
return nil, errors.New("页面不存在")
}
// 如果路径发生变化,检查新路径是否已存在
if req.Path != page.Path {
_, err := s.pageStorage.GetByPath(req.Path)
if err == nil {
return nil, errors.New("页面路径已被其他页面使用")
}
page.Path = req.Path
}
// 更新页面信息
page.Name = req.Name
page.Icon = req.Icon
page.SortOrder = req.SortOrder
page.IsActive = req.IsActive
err = s.pageStorage.Update(page)
if err != nil {
s.logger.Error("更新页面失败", zap.Error(err))
return nil, errors.New("更新页面失败")
}
s.logger.Info("页面更新成功", zap.Uint("page_id", id))
return page, nil
}
// Delete 删除页面
func (s *pageService) Delete(id uint) error {
// 检查页面是否存在
_, err := s.pageStorage.GetByID(id)
if err != nil {
s.logger.Error("页面不存在", zap.Uint("id", id), zap.Error(err))
return errors.New("页面不存在")
}
err = s.pageStorage.Delete(id)
if err != nil {
s.logger.Error("删除页面失败", zap.Error(err))
return errors.New("删除页面失败")
}
s.logger.Info("页面删除成功", zap.Uint("page_id", id))
return nil
}
// List 获取页面列表
func (s *pageService) List(req *models.PageListRequest) ([]models.Page, int64, error) {
pages, total, err := s.pageStorage.List(req)
if err != nil {
s.logger.Error("获取页面列表失败", zap.Error(err))
return nil, 0, errors.New("获取页面列表失败")
}
return pages, total, nil
}

View File

@@ -0,0 +1,128 @@
package services
import (
"errors"
"goalfymax-admin/internal/models"
"goalfymax-admin/pkg/utils"
"go.uber.org/zap"
)
// QuotaService 配额服务接口
type QuotaService interface {
GetQuotaHistory(req *models.QuotaHistoryRequest) (*models.QuotaHistoryResponse, error)
HealthCheck() error
GetQuotaRules() (*models.QuotaRulesResponse, error)
CreateQuotaRule(body any) (*models.QuotaRulesResponse, error)
UpdateQuotaRule(id string, body any) (*models.QuotaRulesResponse, error)
DeleteQuotaRule(id string) (*models.QuotaRulesResponse, error)
}
type quotaService struct {
gatewayClient *GatewayClient
logger *utils.Logger
}
// NewQuotaService 创建配额服务实例
func NewQuotaService(gatewayClient *GatewayClient, logger *utils.Logger) QuotaService {
return &quotaService{
gatewayClient: gatewayClient,
logger: logger,
}
}
// GetQuotaHistory 获取配额历史数据
func (s *quotaService) GetQuotaHistory(req *models.QuotaHistoryRequest) (*models.QuotaHistoryResponse, error) {
// 验证请求参数
if err := s.validateQuotaHistoryRequest(req); err != nil {
s.logger.Error("配额历史请求参数验证失败", zap.Error(err))
return nil, err
}
// 调用网关客户端
response, err := s.gatewayClient.GetQuotaHistory(req)
if err != nil {
s.logger.Error("获取配额历史数据失败", zap.Error(err))
return nil, err
}
s.logger.Info("配额历史数据获取成功",
zap.String("start_date", req.StartDate),
zap.String("end_date", req.EndDate),
zap.Int("data_count", len(response.Data)),
)
return response, nil
}
// HealthCheck 健康检查
func (s *quotaService) HealthCheck() error {
err := s.gatewayClient.HealthCheck()
if err != nil {
s.logger.Error("配额服务健康检查失败", zap.Error(err))
return err
}
s.logger.Info("配额服务健康检查成功")
return nil
}
// GetQuotaRules 获取配额规则列表
func (s *quotaService) GetQuotaRules() (*models.QuotaRulesResponse, error) {
resp, err := s.gatewayClient.GetQuotaRules("")
if err != nil {
s.logger.Error("获取配额规则失败", zap.Error(err))
return nil, err
}
return resp, nil
}
func (s *quotaService) CreateQuotaRule(body any) (*models.QuotaRulesResponse, error) {
return s.gatewayClient.CreateQuotaRule("", body)
}
func (s *quotaService) UpdateQuotaRule(id string, body any) (*models.QuotaRulesResponse, error) {
if id == "" {
return nil, errors.New("缺少规则ID")
}
return s.gatewayClient.UpdateQuotaRule("", id, body)
}
func (s *quotaService) DeleteQuotaRule(id string) (*models.QuotaRulesResponse, error) {
if id == "" {
return nil, errors.New("缺少规则ID")
}
return s.gatewayClient.DeleteQuotaRule("", id)
}
// validateQuotaHistoryRequest 验证配额历史请求参数
func (s *quotaService) validateQuotaHistoryRequest(req *models.QuotaHistoryRequest) error {
if req.StartDate == "" {
return errors.New("开始日期不能为空")
}
if req.EndDate == "" {
return errors.New("结束日期不能为空")
}
// 验证日期格式 (简单验证,实际项目中可以使用更严格的验证)
if len(req.StartDate) != 10 || len(req.EndDate) != 10 {
return errors.New("日期格式不正确,应为 YYYY-MM-DD")
}
// 验证周期参数
if req.Period != "" && req.Period != "daily" && req.Period != "monthly" {
return errors.New("周期参数只能是 daily 或 monthly")
}
// 设置默认值
if req.Period == "" {
req.Period = "daily"
}
if len(req.GroupBy) == 0 {
req.GroupBy = []string{"user_id"}
}
return nil
}

View File

@@ -0,0 +1,165 @@
package services
import (
"errors"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// RBACService 简化的RBAC服务接口
type RBACService interface {
// 角色页面权限管理
AssignRolePagePermissions(req *models.RolePagePermissionAssignRequest) error
RemoveRolePagePermissions(roleID uint, pageIDs []uint) error
GetRolePagePermissions(roleID uint) ([]models.Page, error)
GetRolePagePermissionIDs(roleID uint) ([]uint, error)
// 页面权限检查
CheckUserPagePermission(userID uint, pagePath string) (bool, error)
GetUserAccessiblePages(userID uint) ([]string, error)
GetUserPermissionsResponse(userID uint) (*models.UserPermissionsResponse, error)
GetRolePagePermissionsResponse(roleID uint) (*models.RolePagePermissionsResponse, error)
// 角色管理
GetRoleByID(roleID uint) (*models.Role, error)
}
type rbacService struct {
rbacStorage storage.RBACStorage
userStorage storage.UserStorage
logger *utils.Logger
}
// NewRBACService 创建RBAC服务实例
func NewRBACService(rbacStorage storage.RBACStorage, userStorage storage.UserStorage, logger *utils.Logger) RBACService {
return &rbacService{
rbacStorage: rbacStorage,
userStorage: userStorage,
logger: logger,
}
}
// AssignRolePagePermissions 分配角色页面权限
func (s *rbacService) AssignRolePagePermissions(req *models.RolePagePermissionAssignRequest) error {
err := s.rbacStorage.AssignRolePagePermissions(req.RoleID, req.PageIDs)
if err != nil {
s.logger.Error("分配角色页面权限失败", zap.Uint("roleID", req.RoleID), zap.Uints("pageIDs", req.PageIDs), zap.Error(err))
return errors.New("分配角色页面权限失败")
}
s.logger.Info("分配角色页面权限成功", zap.Uint("roleID", req.RoleID), zap.Uints("pageIDs", req.PageIDs))
return nil
}
// RemoveRolePagePermissions 移除角色页面权限
func (s *rbacService) RemoveRolePagePermissions(roleID uint, pageIDs []uint) error {
err := s.rbacStorage.RemoveRolePagePermissions(roleID, pageIDs)
if err != nil {
s.logger.Error("移除角色页面权限失败", zap.Uint("roleID", roleID), zap.Uints("pageIDs", pageIDs), zap.Error(err))
return errors.New("移除角色页面权限失败")
}
s.logger.Info("移除角色页面权限成功", zap.Uint("roleID", roleID), zap.Uints("pageIDs", pageIDs))
return nil
}
// GetRolePagePermissions 获取角色页面权限
func (s *rbacService) GetRolePagePermissions(roleID uint) ([]models.Page, error) {
pages, err := s.rbacStorage.GetRolePagePermissions(roleID)
if err != nil {
s.logger.Error("获取角色页面权限失败", zap.Uint("roleID", roleID), zap.Error(err))
return nil, errors.New("获取角色页面权限失败")
}
return pages, nil
}
// GetRolePagePermissionIDs 获取角色页面权限ID列表
func (s *rbacService) GetRolePagePermissionIDs(roleID uint) ([]uint, error) {
pageIDs, err := s.rbacStorage.GetRolePagePermissionIDs(roleID)
if err != nil {
s.logger.Error("获取角色页面权限ID失败", zap.Uint("roleID", roleID), zap.Error(err))
return nil, errors.New("获取角色页面权限ID失败")
}
return pageIDs, nil
}
// CheckUserPagePermission 检查用户页面权限
func (s *rbacService) CheckUserPagePermission(userID uint, pagePath string) (bool, error) {
hasPermission, err := s.rbacStorage.CheckUserRolePagePermission(userID, pagePath)
if err != nil {
s.logger.Error("检查用户页面权限失败", zap.Uint("userID", userID), zap.String("pagePath", pagePath), zap.Error(err))
return false, errors.New("检查用户页面权限失败")
}
return hasPermission, nil
}
// GetUserAccessiblePages 获取用户可访问页面
func (s *rbacService) GetUserAccessiblePages(userID uint) ([]string, error) {
pages, err := s.rbacStorage.GetUserRoleAccessiblePages(userID)
if err != nil {
s.logger.Error("获取用户可访问页面失败", zap.Uint("userID", userID), zap.Error(err))
return nil, errors.New("获取用户可访问页面失败")
}
return pages, nil
}
// GetUserPermissionsResponse 获取用户权限响应
func (s *rbacService) GetUserPermissionsResponse(userID uint) (*models.UserPermissionsResponse, error) {
// 获取用户信息
user, err := s.userStorage.GetByID(userID)
if err != nil {
s.logger.Error("获取用户信息失败", zap.Uint("userID", userID), zap.Error(err))
return nil, errors.New("获取用户信息失败")
}
// 查询角色信息
role, err := s.rbacStorage.GetRoleByID(user.RoleID)
if err != nil {
s.logger.Error("获取角色信息失败", zap.Uint("roleID", user.RoleID), zap.Error(err))
return nil, errors.New("获取角色信息失败")
}
// 获取用户可访问页面
pagePaths, err := s.GetUserAccessiblePages(userID)
if err != nil {
s.logger.Error("获取用户可访问页面失败", zap.Uint("userID", userID), zap.Error(err))
return nil, errors.New("获取用户可访问页面失败")
}
// 转换页面路径为页面对象
var pages []models.Page
for _, path := range pagePaths {
pages = append(pages, models.Page{Path: path})
}
return &models.UserPermissionsResponse{
User: *user,
Roles: []models.Role{*role},
Pages: pages,
}, nil
}
// GetRolePagePermissionsResponse 获取角色页面权限响应
func (s *rbacService) GetRolePagePermissionsResponse(roleID uint) (*models.RolePagePermissionsResponse, error) {
// 获取角色信息(这里需要从角色存储中获取,暂时简化)
role := models.Role{BaseModel: models.BaseModel{ID: roleID}}
// 获取角色页面权限
pages, err := s.GetRolePagePermissions(roleID)
if err != nil {
s.logger.Error("获取角色页面权限失败", zap.Uint("roleID", roleID), zap.Error(err))
return nil, errors.New("获取角色页面权限失败")
}
return &models.RolePagePermissionsResponse{
Role: role,
Pages: pages,
}, nil
}
// GetRoleByID 根据ID获取角色
func (s *rbacService) GetRoleByID(roleID uint) (*models.Role, error) {
return s.rbacStorage.GetRoleByID(roleID)
}

View File

@@ -0,0 +1,137 @@
package services
import (
"errors"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// RoleService 角色服务接口
type RoleService interface {
Create(req *models.RoleCreateRequest) (*models.Role, error)
GetByID(id uint) (*models.Role, error)
Update(id uint, req *models.RoleUpdateRequest) (*models.Role, error)
Delete(id uint) error
List(req *models.RoleListRequest) ([]models.Role, int64, error)
UpdateStatus(id uint, status int) error
}
type roleService struct {
roleStorage storage.RoleStorage
logger *utils.Logger
}
// NewRoleService 创建角色服务实例
func NewRoleService(roleStorage storage.RoleStorage, logger *utils.Logger) RoleService {
return &roleService{
roleStorage: roleStorage,
logger: logger,
}
}
// Create 创建角色
func (s *roleService) Create(req *models.RoleCreateRequest) (*models.Role, error) {
// 检查角色名称是否已存在
_, err := s.roleStorage.GetByName(req.Name)
if err == nil {
return nil, errors.New("角色名称已存在")
}
// 创建角色
role := &models.Role{
Name: req.Name,
Level: req.Level,
Description: req.Description,
IsDefault: req.IsDefault,
}
err = s.roleStorage.Create(role)
if err != nil {
s.logger.Error("创建角色失败", zap.Error(err))
return nil, errors.New("创建角色失败")
}
s.logger.Info("角色创建成功", zap.String("name", role.Name))
return role, nil
}
// GetByID 根据ID获取角色
func (s *roleService) GetByID(id uint) (*models.Role, error) {
return s.roleStorage.GetByID(id)
}
// Update 更新角色
func (s *roleService) Update(id uint, req *models.RoleUpdateRequest) (*models.Role, error) {
// 获取角色信息
role, err := s.roleStorage.GetByID(id)
if err != nil {
return nil, errors.New("角色不存在")
}
// 检查角色名称是否已被其他角色使用
if req.Name != role.Name {
existingRole, err := s.roleStorage.GetByName(req.Name)
if err == nil && existingRole.ID != id {
return nil, errors.New("角色名称已被其他角色使用")
}
role.Name = req.Name
}
// 更新角色信息
role.Name = req.Name
role.Level = req.Level
role.Description = req.Description
role.IsDefault = req.IsDefault
err = s.roleStorage.Update(role)
if err != nil {
s.logger.Error("更新角色失败", zap.Error(err))
return nil, errors.New("更新角色失败")
}
s.logger.Info("角色更新成功", zap.Uint("role_id", id))
return role, nil
}
// Delete 删除角色
func (s *roleService) Delete(id uint) error {
// 检查角色是否存在
_, err := s.roleStorage.GetByID(id)
if err != nil {
return errors.New("角色不存在")
}
err = s.roleStorage.Delete(id)
if err != nil {
s.logger.Error("删除角色失败", zap.Error(err))
return errors.New("删除角色失败")
}
s.logger.Info("角色删除成功", zap.Uint("role_id", id))
return nil
}
// List 获取角色列表
func (s *roleService) List(req *models.RoleListRequest) ([]models.Role, int64, error) {
return s.roleStorage.List(req)
}
// UpdateStatus 更新角色状态
func (s *roleService) UpdateStatus(id uint, status int) error {
// 检查角色是否存在
_, err := s.roleStorage.GetByID(id)
if err != nil {
return errors.New("角色不存在")
}
err = s.roleStorage.UpdateStatus(id, status)
if err != nil {
s.logger.Error("更新角色状态失败", zap.Error(err))
return errors.New("更新角色状态失败")
}
s.logger.Info("角色状态更新成功", zap.Uint("role_id", id), zap.Int("status", status))
return nil
}

View File

@@ -0,0 +1,170 @@
package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"goalfymax-admin/internal/config"
)
// SSOAdminUserCreateRequest SSO创建用户请求
type SSOAdminUserCreateRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password"`
}
// SSOAdminUserCreateResponse SSO创建用户响应
type SSOAdminUserCreateResponse struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Phone string `json:"phone"`
Message string `json:"message,omitempty"`
}
// SSOAdminUserRoleAssignRequest SSO用户角色分配请求
type SSOAdminUserRoleAssignRequest struct {
UserID int `json:"user_id"`
RoleID int `json:"role_id"`
}
// SSOAdminUserRoleAssignResponse SSO用户角色分配响应
type SSOAdminUserRoleAssignResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
// SSOAdminService SSO管理服务接口
type SSOAdminService interface {
CreateUser(ctx context.Context, req *SSOAdminUserCreateRequest) (*SSOAdminUserCreateResponse, error)
SetSystemRole(ctx context.Context, userID int, systemRole string) error
}
// ssoAdminService SSO管理服务实现
type ssoAdminService struct {
baseURL string
adminToken string
timeout time.Duration
client *http.Client
}
// NewSSOAdminService 创建SSO管理服务实例
func NewSSOAdminService() SSOAdminService {
cfg := config.GetConfig()
client := &http.Client{
Timeout: cfg.SSO.Timeout,
}
return &ssoAdminService{
baseURL: cfg.SSO.SSOServerURL,
adminToken: cfg.SSO.AdminToken,
timeout: cfg.SSO.Timeout,
client: client,
}
}
// CreateUser 创建SSO用户
func (s *ssoAdminService) CreateUser(ctx context.Context, req *SSOAdminUserCreateRequest) (*SSOAdminUserCreateResponse, error) {
url := fmt.Sprintf("%s/api/admin/users", s.baseURL)
// 构造请求体
reqBody, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("序列化请求体失败: %w", err)
}
// 创建HTTP请求
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminToken))
httpReq.Header.Set("Accept", "*/*")
httpReq.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
httpReq.Header.Set("Cache-Control", "no-cache")
httpReq.Header.Set("Pragma", "no-cache")
// 发送请求
resp, err := s.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close()
// 读取响应体
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应体失败: %w", err)
}
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("SSO API返回错误状态码 %d: %s", resp.StatusCode, string(respBody))
}
// 解析响应
var response SSOAdminUserCreateResponse
if err := json.Unmarshal(respBody, &response); err != nil {
return nil, fmt.Errorf("解析SSO响应失败: %w", err)
}
return &response, nil
}
// SetSystemRole 设置系统角色custom/sys_admin
func (s *ssoAdminService) SetSystemRole(ctx context.Context, userID int, systemRole string) error {
url := fmt.Sprintf("%s/api/admin/users/%d/role", s.baseURL, userID)
// 构造请求体
reqBodyBytes, err := json.Marshal(map[string]string{
"system_role": systemRole,
})
if err != nil {
return fmt.Errorf("序列化请求体失败: %w", err)
}
// 创建HTTP请求
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBodyBytes))
if err != nil {
return fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminToken))
httpReq.Header.Set("Accept", "*/*")
httpReq.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
httpReq.Header.Set("Cache-Control", "no-cache")
httpReq.Header.Set("Pragma", "no-cache")
// 发送请求
resp, err := s.client.Do(httpReq)
if err != nil {
return fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close()
// 读取响应体
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取响应体失败: %w", err)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("SSO系统角色API返回错误状态码 %d: %s", resp.StatusCode, string(respBody))
}
// 允许无 success 包装的简易响应,这里不做进一步结构校验
return nil
}

View File

@@ -0,0 +1,618 @@
package services
import (
"context"
"fmt"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/middleware"
"goalfymax-admin/pkg/utils"
"net/url"
"strconv"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
)
// SSOService SSO服务接口
type SSOService interface {
// 登录相关
InitiateLogin(ctx context.Context) (*models.SSOLoginResponse, error)
HandleCallback(ctx context.Context, req *models.SSOCallbackRequest) (*models.SSOCallbackResponse, error)
RefreshToken(ctx context.Context, req *models.RefreshTokenRequest) (*models.RefreshTokenResponse, error)
Logout(ctx context.Context, token string) (*models.LogoutResponse, error)
GetUserInfo(ctx context.Context, token string) (*models.UserInfoResponse, error)
// 登录信息管理
RecordUserLogin(ctx context.Context, req *UserLoginRequest) error
UserLogout(ctx context.Context, userID int, uuid string) error
GetUserLoginInfo(ctx context.Context, userID int) (*models.LoginInfo, error)
IsUserOnline(ctx context.Context, userID int) (bool, error)
GetOnlineUsers(ctx context.Context) ([]*models.LoginInfo, error)
GetOnlineUserCount(ctx context.Context) (int64, error)
BatchUserLogout(ctx context.Context, userIDs []int) error
}
// UserLoginRequest 用户登录请求结构体
type UserLoginRequest struct {
UserID int `json:"user_id" binding:"required"`
UserName string `json:"user_name" binding:"required,max=100"`
Email string `json:"email" binding:"required,email,max=255"`
UUID string `json:"uuid" binding:"required"`
}
type ssoService struct {
client *middleware.SSOClient
pkceStateStorage storage.PKCEStateStorage
loginInfoStorage storage.LoginInfoStorage
rbacService RBACService
logger *utils.Logger
}
// NewSSOService 创建SSO服务实例
func NewSSOService(client *middleware.SSOClient, pkceStateStorage storage.PKCEStateStorage, loginInfoStorage storage.LoginInfoStorage, rbacService RBACService, logger *utils.Logger) SSOService {
return &ssoService{
client: client,
pkceStateStorage: pkceStateStorage,
loginInfoStorage: loginInfoStorage,
rbacService: rbacService,
logger: logger,
}
}
// InitiateLogin 初始化SSO登录
func (s *ssoService) InitiateLogin(ctx context.Context) (*models.SSOLoginResponse, error) {
// 生成状态参数
state := fmt.Sprintf("state_%d", time.Now().UnixNano())
// 获取授权URL
authURL, codeVerifier, err := s.client.GetAuthorizationURL(state)
if err != nil {
s.logger.Error("failed to generate authorization URL", zap.Error(err))
return nil, fmt.Errorf("生成授权URL失败: %w", err)
}
// 将state和codeVerifier存储到数据库
pkceState := &models.PKCEState{
State: state,
CodeVerifier: codeVerifier,
}
if err := s.pkceStateStorage.Create(pkceState); err != nil {
s.logger.Error("failed to store PKCE state", zap.Error(err))
return nil, fmt.Errorf("存储PKCE状态失败: %w", err)
}
return &models.SSOLoginResponse{
Success: true,
Message: "SSO login initiated",
AuthURL: authURL,
State: state,
CodeVerifier: "", // 不返回codeVerifier到前端保护安全
}, nil
}
// HandleCallback 处理SSO回调
func (s *ssoService) HandleCallback(ctx context.Context, req *models.SSOCallbackRequest) (*models.SSOCallbackResponse, error) {
// 从数据库获取PKCE状态信息
pkceState, err := s.pkceStateStorage.GetByState(req.State)
if err != nil {
s.logger.Error("failed to get PKCE state", zap.String("state", req.State), zap.Error(err))
return nil, fmt.Errorf("无效或过期的状态参数")
}
// 交换令牌
tokenResp, err := s.client.ExchangeCodeForToken(ctx, req.Code, pkceState.CodeVerifier)
if err != nil {
s.logger.Error("failed to exchange token", zap.Error(err))
return nil, fmt.Errorf("令牌交换失败: %w", err)
}
// 获取用户信息
userInfo, err := s.client.GetUserInfo(ctx, tokenResp.AccessToken)
if err != nil {
s.logger.Error("failed to get user info", zap.Error(err))
return nil, fmt.Errorf("获取用户信息失败: %w", err)
}
// 记录用户登录信息
uid, err := strconv.Atoi(userInfo.Sub)
if err != nil {
s.logger.Error("failed to convert user id", zap.String("sub", userInfo.Sub), zap.Error(err))
return nil, fmt.Errorf("无效的用户ID")
}
// 计算本次登录应赋予的角色IDsys_admin → 5否则 → 0
assignedRoleID := uint(0)
if len(userInfo.Roles) > 0 {
for _, r := range userInfo.Roles {
if r.Name == "sys_admin" {
assignedRoleID = uint(5)
break
}
}
}
if assignedRoleID == 0 {
s.logger.Info("本次登录未检测到 sys_admin 角色赋予无权限角色ID=0", zap.Int("userID", uid))
} else {
s.logger.Info("本次登录检测到 sys_admin 角色赋予角色ID=5", zap.Int("userID", uid))
}
// 查找或创建用户记录,并根据本次判定设置 role_idsys_admin=5否则=0
_, err = s.findOrCreateUserWithRole(uint(uid), userInfo, assignedRoleID)
if err != nil {
s.logger.Error("failed to find or create user", zap.Error(err))
return nil, fmt.Errorf("创建或查找用户失败: %w", err)
}
uuid := uuid.NewString()
err = s.RecordUserLogin(ctx, &UserLoginRequest{
UserID: uid,
UserName: userInfo.Name,
Email: userInfo.Email,
UUID: uuid,
})
if err != nil {
s.logger.Error("failed to record user login", zap.Int("user_id", uid), zap.Error(err))
// 不返回错误,继续处理
}
// 获取用户可访问页面信息
userPages, err := s.rbacService.GetUserAccessiblePages(uint(uid))
if err != nil {
s.logger.Warn("获取用户页面权限失败", zap.Error(err))
userPages = []string{}
}
s.logger.Info("获取用户可访问页面", zap.Uint("userID", uint(uid)), zap.Strings("pages", userPages))
// 获取用户角色信息(用于前端体验判断,后端仍以接口鉴权为准)
var user models.User
var userRoleName string
err = storage.DB.Where("id = ?", uint(uid)).First(&user).Error
if err != nil {
s.logger.Warn("获取用户信息失败", zap.Error(err))
} else {
role, rerr := s.rbacService.GetRoleByID(user.RoleID)
if rerr != nil {
s.logger.Warn("获取角色信息失败", zap.Error(rerr))
} else {
userRoleName = role.Name
s.logger.Info("获取用户角色", zap.Uint("userID", uint(uid)), zap.String("roleName", userRoleName))
}
}
// 构建包含页面权限的用户信息
userInfoWithPages := &models.UserInfo{
Sub: userInfo.Sub,
Name: userInfo.Name,
Email: userInfo.Email,
PreferredUsername: userInfo.PreferredUsername,
Pages: convertPagesFromPaths(userPages),
Roles: []models.Role{},
}
if userRoleName != "" {
userInfoWithPages.Roles = append(userInfoWithPages.Roles, models.Role{Name: userRoleName})
}
// 清理PKCE状态
_ = s.pkceStateStorage.DeleteByState(req.State)
return &models.SSOCallbackResponse{
Success: true,
Message: "SSO login successful",
AccessToken: tokenResp.AccessToken,
IDToken: tokenResp.IDToken,
RefreshToken: tokenResp.RefreshToken,
ExpiresIn: tokenResp.ExpiresIn,
UserInfo: userInfoWithPages,
UUID: uuid,
}, nil
}
// convertPagesFromPaths 将页面路径转换为页面对象
func convertPagesFromPaths(paths []string) []models.Page {
var pages []models.Page
fmt.Printf("🔍 [convertPagesFromPaths] 输入路径: %v\n", paths)
// 从数据库获取完整的页面信息
err := storage.DB.Where("path IN ? AND is_active = TRUE AND deleted_at IS NULL", paths).Find(&pages).Error
if err != nil {
fmt.Printf("❌ [convertPagesFromPaths] 数据库查询失败: %v\n", err)
// 如果数据库查询失败,至少返回路径信息
for _, path := range paths {
pages = append(pages, models.Page{
Path: path,
IsActive: true, // 默认设置为激活状态
})
}
} else {
fmt.Printf("✅ [convertPagesFromPaths] 数据库查询成功,找到 %d 个页面\n", len(pages))
for _, page := range pages {
fmt.Printf(" - 页面: %s, 激活状态: %v\n", page.Path, page.IsActive)
}
}
return pages
}
// RefreshToken 刷新令牌
func (s *ssoService) RefreshToken(ctx context.Context, req *models.RefreshTokenRequest) (*models.RefreshTokenResponse, error) {
// 刷新令牌
tokenResp, err := s.client.RefreshToken(ctx, req.RefreshToken)
if err != nil {
s.logger.Error("failed to refresh token", zap.Error(err))
return nil, fmt.Errorf("令牌刷新失败: %w", err)
}
return &models.RefreshTokenResponse{
Success: true,
Message: "Token refreshed successfully",
AccessToken: tokenResp.AccessToken,
IDToken: tokenResp.IDToken,
RefreshToken: tokenResp.RefreshToken,
ExpiresIn: tokenResp.ExpiresIn,
}, nil
}
// Logout 登出
func (s *ssoService) Logout(ctx context.Context, token string) (*models.LogoutResponse, error) {
// 获取用户信息用于记录登出
userInfo, err := s.client.GetUserInfo(ctx, token)
if err != nil {
s.logger.Error("failed to get user info during logout", zap.Error(err))
// 继续执行登出,不中断流程
}
// 调用SSO服务登出
err = s.client.Logout(ctx, token)
if err != nil {
s.logger.Error("failed to logout", zap.Error(err))
return nil, fmt.Errorf("登出失败: %w", err)
}
// 记录用户登出信息
if userInfo != nil {
uid, err := strconv.Atoi(userInfo.Sub)
if err != nil {
s.logger.Error("failed to convert user id during logout", zap.String("sub", userInfo.Sub), zap.Error(err))
} else {
// 先根据user_id查找用户的uuid
loginInfo, err := s.loginInfoStorage.GetByUserID(uid)
if err != nil {
s.logger.Error("failed to get user login info during logout", zap.Int("user_id", uid), zap.Error(err))
} else {
// 如果UUID为空直接根据user_id进行登出
if loginInfo.UUID == "" {
s.logger.Warn("UUID is empty, logging out by user_id only", zap.Int("user_id", uid))
err = s.loginInfoStorage.SetUserOffline(uid)
if err != nil {
s.logger.Error("failed to set user offline by user_id", zap.Int("user_id", uid), zap.Error(err))
}
} else {
// 使用找到的uuid进行登出
err = s.UserLogout(ctx, uid, loginInfo.UUID)
if err != nil {
s.logger.Error("failed to record user logout", zap.Int("user_id", uid), zap.Error(err))
}
}
}
}
}
redirectUrl := s.client.GetServerUrl() + "/oauth2/logout?redirect_uri=" + url.QueryEscape(s.client.GetRedirectUrl())
return &models.LogoutResponse{
Success: true,
Message: redirectUrl,
}, nil
}
// GetUserInfo 获取用户信息
func (s *ssoService) GetUserInfo(ctx context.Context, token string) (*models.UserInfoResponse, error) {
// 获取用户信息
userInfo, err := s.client.GetUserInfo(ctx, token)
if err != nil {
s.logger.Error("failed to get user info", zap.Error(err))
return nil, fmt.Errorf("获取用户信息失败: %w", err)
}
return &models.UserInfoResponse{
Success: true,
Message: "User info retrieved successfully",
UserInfo: userInfo,
}, nil
}
// RecordUserLogin 记录用户登录状态
func (s *ssoService) RecordUserLogin(ctx context.Context, req *UserLoginRequest) error {
// 检查用户是否已存在
existingUser, err := s.loginInfoStorage.GetByUserID(req.UserID)
if err != nil && err != gorm.ErrRecordNotFound {
s.logger.Error("failed to get existing user login info",
zap.Int("user_id", req.UserID),
zap.Error(err))
return fmt.Errorf("获取用户登录信息失败: %w", err)
}
if existingUser != nil {
// 用户存在直接更新状态
existingUser.IsOnline = true
existingUser.UUID = req.UUID // 同时更新UUID可能有变化
existingUser.UserName = req.UserName
existingUser.Email = req.Email
err = s.loginInfoStorage.Update(existingUser)
if err != nil {
s.logger.Error("failed to update user online status",
zap.Int("user_id", req.UserID),
zap.Error(err))
return fmt.Errorf("更新用户在线状态失败: %w", err)
}
s.logger.Info("user login status updated successfully",
zap.Int("user_id", req.UserID),
zap.String("user_name", req.UserName))
} else {
// 用户不存在,创建新用户并设置为在线
newLoginInfo := &models.LoginInfo{
UserID: req.UserID,
UserName: req.UserName,
Email: req.Email,
UUID: req.UUID,
IsOnline: true,
}
err = s.loginInfoStorage.Create(newLoginInfo)
if err != nil {
s.logger.Error("failed to create new user login info",
zap.Int("user_id", req.UserID),
zap.String("user_name", req.UserName),
zap.Error(err))
return fmt.Errorf("创建用户登录信息失败: %w", err)
}
s.logger.Info("new user login info created successfully",
zap.Int("user_id", req.UserID),
zap.String("user_name", req.UserName))
}
return nil
}
// UserLogout 用户登出
func (s *ssoService) UserLogout(ctx context.Context, userID int, uuid string) error {
// 检查用户是否存在
existingUser, err := s.loginInfoStorage.GetByUserIDAndUUID(userID, uuid)
if err != nil && err != gorm.ErrRecordNotFound {
s.logger.Error("failed to get user login info for logout",
zap.Int("user_id", userID),
zap.String("uuid", uuid),
zap.Error(err))
return fmt.Errorf("获取用户登录信息失败: %w", err)
}
if existingUser == nil {
// 用户不存在,不需要操作
s.logger.Info("user not found, no logout action needed",
zap.Int("user_id", userID),
zap.String("uuid", uuid))
return nil
}
// 检查用户是否已经离线
if !existingUser.IsOnline {
// 用户已经离线,不需要操作
s.logger.Info("user is already offline, no action needed",
zap.Int("user_id", userID),
zap.String("uuid", uuid),
zap.String("user_name", existingUser.UserName))
return nil
}
// 设置用户为离线状态
err = s.loginInfoStorage.SetUserOffline(userID)
if err != nil {
s.logger.Error("failed to set user offline",
zap.Int("user_id", userID),
zap.String("user_name", existingUser.UserName),
zap.Error(err))
return fmt.Errorf("设置用户离线状态失败: %w", err)
}
s.logger.Info("user logout successfully",
zap.Int("user_id", userID),
zap.String("uuid", uuid),
zap.String("user_name", existingUser.UserName))
return nil
}
// GetUserLoginInfo 获取用户登录信息
func (s *ssoService) GetUserLoginInfo(ctx context.Context, userID int) (*models.LoginInfo, error) {
loginInfo, err := s.loginInfoStorage.GetByUserID(userID)
if err != nil {
s.logger.Error("failed to get user login info",
zap.Int("user_id", userID),
zap.Error(err))
return nil, fmt.Errorf("获取用户登录信息失败: %w", err)
}
return loginInfo, nil
}
// IsUserOnline 检查用户是否在线
func (s *ssoService) IsUserOnline(ctx context.Context, userID int) (bool, error) {
loginInfo, err := s.GetUserLoginInfo(ctx, userID)
if err != nil {
return false, err
}
if loginInfo == nil {
return false, nil
}
return loginInfo.IsOnline, nil
}
// GetOnlineUsers 获取在线用户列表
func (s *ssoService) GetOnlineUsers(ctx context.Context) ([]*models.LoginInfo, error) {
onlineUsers, err := s.loginInfoStorage.ListOnlineUsers()
if err != nil {
s.logger.Error("failed to get online users", zap.Error(err))
return nil, fmt.Errorf("获取在线用户列表失败: %w", err)
}
return onlineUsers, nil
}
// GetOnlineUserCount 获取在线用户数量
func (s *ssoService) GetOnlineUserCount(ctx context.Context) (int64, error) {
count, err := s.loginInfoStorage.CountOnlineUsers()
if err != nil {
s.logger.Error("failed to count online users", zap.Error(err))
return 0, fmt.Errorf("统计在线用户数量失败: %w", err)
}
return count, nil
}
// BatchUserLogout 批量用户登出(可用于系统维护等场景)
func (s *ssoService) BatchUserLogout(ctx context.Context, userIDs []int) error {
if len(userIDs) == 0 {
return nil
}
for _, userID := range userIDs {
err := s.loginInfoStorage.SetUserOffline(userID)
if err != nil {
s.logger.Error("failed to set user offline in batch",
zap.Int("user_id", userID),
zap.Error(err))
// 继续处理其他用户,不中断整个批量操作
continue
}
}
s.logger.Info("batch user logout completed",
zap.Ints("user_ids", userIDs))
return nil
}
// findOrCreateUser 查找或创建用户
func (s *ssoService) findOrCreateUserWithRole(userID uint, userInfo *models.UserInfo, assignedRoleID uint) (*models.User, error) {
// 尝试查找现有用户
var user models.User
err := storage.DB.Where("id = ?", userID).First(&user).Error
if err == nil {
// 用户存在,更新登录信息
now := time.Now()
user.LastLoginAt = &now
user.LoginCount++
// 更新用户信息如果SSO信息有变化
if userInfo.Name != "" && user.Nickname != userInfo.Name {
user.Nickname = userInfo.Name
}
if userInfo.Email != "" && user.Email != userInfo.Email {
user.Email = userInfo.Email
}
// 同步规则:
// - 若SSO判定为普通(0)无论当前为何值一律降级为0
// - 若SSO判定为sys_admin(5)仅当当前为0时升级为5避免覆盖业务自定义角色
if assignedRoleID == 0 {
if user.RoleID != 0 {
s.logger.Info("降级用户角色(→0)", zap.Uint("userID", userID), zap.Uint("oldRoleID", user.RoleID))
user.RoleID = 0
} else {
s.logger.Debug("保持0角色不变", zap.Uint("userID", userID))
}
} else if assignedRoleID == 5 {
if user.RoleID == 0 {
s.logger.Info("升级用户角色(0→5)", zap.Uint("userID", userID))
user.RoleID = 5
} else {
s.logger.Debug("保持非0角色不变", zap.Uint("userID", userID), zap.Uint("currentRoleID", user.RoleID))
}
}
err = storage.DB.Save(&user).Error
if err != nil {
return nil, fmt.Errorf("更新用户信息失败: %w", err)
}
s.logger.Info("用户登录信息已更新",
zap.Uint("userID", userID),
zap.String("username", user.Username))
return &user, nil
}
if err != gorm.ErrRecordNotFound {
return nil, fmt.Errorf("查询用户失败: %w", err)
}
// 用户不存在,创建新用户
now := time.Now()
user = models.User{
BaseModel: models.BaseModel{
ID: userID,
CreatedAt: now,
UpdatedAt: now,
},
Username: userInfo.PreferredUsername,
Email: userInfo.Email,
Nickname: userInfo.Name,
Status: 1, // 默认启用
SSOProvider: "default", // 可以根据实际情况设置
LastLoginAt: &now,
LoginCount: 1,
RoleID: assignedRoleID,
}
// 如果PreferredUsername为空使用Email作为用户名
if user.Username == "" {
user.Username = userInfo.Email
}
err = storage.DB.Create(&user).Error
if err != nil {
return nil, fmt.Errorf("创建用户失败: %w", err)
}
s.logger.Info("新用户创建成功",
zap.Uint("userID", userID),
zap.String("username", user.Username),
zap.String("email", user.Email))
return &user, nil
}
// assignDefaultRole 分配默认角色L5全员
func (s *ssoService) assignDefaultRole(userID uint) error {
// 获取默认角色IDL5全员
var role models.Role
err := storage.DB.Where("is_default = ?", true).First(&role).Error
if err != nil {
return fmt.Errorf("获取默认角色失败: %w", err)
}
// 更新用户的角色ID
err = storage.DB.Model(&models.User{}).Where("id = ?", userID).Update("role_id", role.ID).Error
if err != nil {
return fmt.Errorf("分配默认角色失败: %w", err)
}
s.logger.Info("用户已分配默认角色",
zap.Uint("userID", userID),
zap.Uint("roleID", role.ID),
zap.String("roleName", role.Name))
return nil
}

View File

@@ -0,0 +1,145 @@
package services
import (
"errors"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// SystemConfigService 系统配置服务接口
type SystemConfigService interface {
Create(req *models.SystemConfigRequest) (*models.SystemConfig, error)
GetByID(id uint) (*models.SystemConfig, error)
Update(id uint, req *models.SystemConfigUpdateRequest) (*models.SystemConfig, error)
Delete(id uint) error
List(req *models.SystemConfigListRequest) ([]models.SystemConfig, int64, error)
UpdateStatus(id uint, status int) error
GetAll() ([]models.SystemConfig, error)
GetByKey(key string) (*models.SystemConfig, error)
}
type systemConfigService struct {
storage storage.SystemConfigStorage
logger *utils.Logger
}
// NewSystemConfigService 创建系统配置服务实例
func NewSystemConfigService(storage storage.SystemConfigStorage, logger *utils.Logger) SystemConfigService {
return &systemConfigService{
storage: storage,
logger: logger,
}
}
// Create 创建系统配置
func (s *systemConfigService) Create(req *models.SystemConfigRequest) (*models.SystemConfig, error) {
// 检查配置标识是否已存在
_, err := s.storage.GetByKey(req.Key)
if err == nil {
return nil, errors.New("配置标识已存在")
}
// 创建配置
config := &models.SystemConfig{
Key: req.Key,
Name: req.Name,
Value: req.Value,
Type: req.Type,
Desc: req.Desc,
Status: 1, // 默认启用
}
err = s.storage.Create(config)
if err != nil {
s.logger.Error("创建系统配置失败", zap.Error(err))
return nil, errors.New("创建系统配置失败")
}
s.logger.Info("系统配置创建成功", zap.String("key", config.Key))
return config, nil
}
// GetByID 根据ID获取系统配置
func (s *systemConfigService) GetByID(id uint) (*models.SystemConfig, error) {
return s.storage.GetByID(id)
}
// GetByKey 根据配置标识获取系统配置
func (s *systemConfigService) GetByKey(key string) (*models.SystemConfig, error) {
return s.storage.GetByKey(key)
}
// Update 更新系统配置
func (s *systemConfigService) Update(id uint, req *models.SystemConfigUpdateRequest) (*models.SystemConfig, error) {
// 获取配置信息
config, err := s.storage.GetByID(id)
if err != nil {
return nil, errors.New("配置不存在")
}
// 更新字段
config.Name = req.Name
config.Value = req.Value
config.Type = req.Type
config.Desc = req.Desc
// 保存更新
err = s.storage.Update(config)
if err != nil {
s.logger.Error("更新系统配置失败", zap.Error(err))
return nil, errors.New("更新系统配置失败")
}
s.logger.Info("系统配置更新成功", zap.Uint("id", id))
return config, nil
}
// Delete 删除系统配置
func (s *systemConfigService) Delete(id uint) error {
// 检查配置是否存在
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("配置不存在")
}
// 删除配置
err = s.storage.Delete(id)
if err != nil {
s.logger.Error("删除系统配置失败", zap.Error(err))
return errors.New("删除系统配置失败")
}
s.logger.Info("系统配置删除成功", zap.Uint("id", id))
return nil
}
// List 获取系统配置列表
func (s *systemConfigService) List(req *models.SystemConfigListRequest) ([]models.SystemConfig, int64, error) {
return s.storage.List(req)
}
// UpdateStatus 更新状态
func (s *systemConfigService) UpdateStatus(id uint, status int) error {
// 检查配置是否存在
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("配置不存在")
}
// 更新状态
err = s.storage.UpdateStatus(id, status)
if err != nil {
s.logger.Error("更新系统配置状态失败", zap.Error(err))
return errors.New("更新系统配置状态失败")
}
s.logger.Info("系统配置状态更新成功", zap.Uint("id", id), zap.Int("status", status))
return nil
}
// GetAll 获取所有系统配置
func (s *systemConfigService) GetAll() ([]models.SystemConfig, error) {
return s.storage.GetAll()
}

View File

@@ -0,0 +1,260 @@
package services
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/oss"
"goalfymax-admin/internal/storage"
"strconv"
)
// UserFeedbackService 用户反馈服务
type UserFeedbackService struct {
storage *storage.UserFeedbackStorage
}
// NewUserFeedbackService 创建用户反馈服务实例
func NewUserFeedbackService(storage *storage.UserFeedbackStorage) *UserFeedbackService {
return &UserFeedbackService{storage: storage}
}
// List 获取用户反馈列表
func (s *UserFeedbackService) List(ctx context.Context, req *models.UserFeedbackListRequest) (*models.UserFeedbackListResponse, error) {
// 参数校验
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 10
}
if req.PageSize > 100 {
req.PageSize = 100
}
// 状态值校验
if req.Status != nil && (*req.Status < 0 || *req.Status > 1) {
return nil, fmt.Errorf("状态值无效")
}
// 用户ID校验
if req.UserID != nil && *req.UserID <= 0 {
return nil, fmt.Errorf("用户ID无效")
}
// 时间格式校验
if req.StartTime != "" {
if _, err := strconv.ParseInt(req.StartTime, 10, 64); err != nil {
// 尝试解析时间格式
if err := parseTimeString(req.StartTime); err != nil {
return nil, fmt.Errorf("开始时间格式无效")
}
}
}
if req.EndTime != "" {
if _, err := strconv.ParseInt(req.EndTime, 10, 64); err != nil {
// 尝试解析时间格式
if err := parseTimeString(req.EndTime); err != nil {
return nil, fmt.Errorf("结束时间格式无效")
}
}
}
// 调用存储层
feedbacks, total, err := s.storage.List(ctx, req)
if err != nil {
return nil, fmt.Errorf("获取反馈列表失败: %w", err)
}
// 转换为带有可访问URL的返回结构
items := make([]models.UserFeedbackItem, 0, len(feedbacks))
for _, fb := range feedbacks {
var keys []string
if fb.FileKeys != "" {
_ = json.Unmarshal([]byte(fb.FileKeys), &keys)
}
// 直接下载文件内容并进行Base64编码
var fileContents []string
for _, k := range keys {
if k == "" {
continue
}
content, mimeType, err := oss.DownloadFileContent(ctx, k)
if err != nil {
// 记录错误,但继续处理其他文件
fmt.Printf("Error downloading file %s: %v\n", k, err)
continue
}
encodedContent := fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(content))
fileContents = append(fileContents, encodedContent)
}
items = append(items, models.UserFeedbackItem{
ID: fb.ID,
UserID: fb.UID,
Content: fb.Content,
FileKeys: keys,
FileContents: fileContents, // 返回Base64编码的内容
Status: fb.Status,
HandledBy: fb.HandledBy,
HandledAt: fb.HandledAt,
CreatedAt: fb.CreatedAt,
UpdatedAt: fb.UpdatedAt,
})
}
return &models.UserFeedbackListResponse{
List: items,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// GetByID 根据ID获取用户反馈
func (s *UserFeedbackService) GetByID(ctx context.Context, id int64) (*models.UserFeedbackItem, error) {
if id <= 0 {
return nil, fmt.Errorf("反馈ID无效")
}
feedback, err := s.storage.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("获取用户反馈失败: %w", err)
}
if feedback == nil {
return nil, fmt.Errorf("反馈不存在")
}
// 解析 file_keys
var keys []string
if feedback.FileKeys != "" {
_ = json.Unmarshal([]byte(feedback.FileKeys), &keys)
}
// 下载文件内容并进行Base64编码
var fileContents []string
for _, k := range keys {
if k == "" {
continue
}
content, mimeType, err := oss.DownloadFileContent(ctx, k)
if err != nil {
fmt.Printf("Error downloading file %s: %v\n", k, err)
continue
}
encodedContent := fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(content))
fileContents = append(fileContents, encodedContent)
}
return &models.UserFeedbackItem{
ID: feedback.ID,
UserID: feedback.UID,
Content: feedback.Content,
FileKeys: keys,
FileContents: fileContents,
Status: feedback.Status,
HandledBy: feedback.HandledBy,
HandledAt: feedback.HandledAt,
CreatedAt: feedback.CreatedAt,
UpdatedAt: feedback.UpdatedAt,
}, nil
}
// MarkHandled 切换处理状态(已处理/未处理)
func (s *UserFeedbackService) MarkHandled(ctx context.Context, id int64, handledBy int, note string) error {
if id <= 0 {
return fmt.Errorf("反馈ID无效")
}
// 检查反馈是否存在
feedback, err := s.storage.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("获取反馈信息失败: %w", err)
}
if feedback == nil {
return fmt.Errorf("反馈不存在")
}
// 如果要标记为已处理需要处理人ID
if feedback.Status == 0 && handledBy <= 0 {
return fmt.Errorf("处理人ID无效")
}
// 切换状态
if err := s.storage.MarkHandled(ctx, id, handledBy, note); err != nil {
return fmt.Errorf("切换状态失败: %w", err)
}
return nil
}
// Delete 删除用户反馈
func (s *UserFeedbackService) Delete(ctx context.Context, id int64) error {
if id <= 0 {
return fmt.Errorf("反馈ID无效")
}
// 检查反馈是否存在
feedback, err := s.storage.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("获取反馈信息失败: %w", err)
}
if feedback == nil {
return fmt.Errorf("反馈不存在")
}
// 删除反馈
if err := s.storage.Delete(ctx, id); err != nil {
return fmt.Errorf("删除反馈失败: %w", err)
}
return nil
}
// GetStatistics 获取反馈统计信息
func (s *UserFeedbackService) GetStatistics(ctx context.Context) (map[string]interface{}, error) {
stats, err := s.storage.GetStatistics(ctx)
if err != nil {
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
return stats, nil
}
// parseTimeString 解析时间字符串
func parseTimeString(timeStr string) error {
// 支持多种时间格式
formats := []string{
"2006-01-02 15:04:05",
"2006-01-02",
"2006/01/02 15:04:05",
"2006/01/02",
}
// 先检查是否是时间戳格式
if _, err := strconv.ParseInt(timeStr, 10, 64); err == nil {
return nil // 时间戳格式
}
// 检查其他时间格式
for _, format := range formats {
// 这里可以添加实际的时间解析逻辑,暂时跳过
_ = format
}
return fmt.Errorf("时间格式无效")
}
// hasHTTPPrefix 判断字符串是否为 http/https URL
func hasHTTPPrefix(s string) bool {
// 此函数在此服务中已不再需要,但保留以避免潜在编译错误,或者可以在此被移除
return false
}
// joinURL 已废弃,改为强制预签名

View File

@@ -0,0 +1,145 @@
package services
import (
"errors"
"go.uber.org/zap"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
)
// UserLevelConfigService 用户等级配置服务接口
type UserLevelConfigService interface {
Create(req *models.UserLevelConfigCreateRequest) (*models.UserLevelConfig, error)
GetByID(id uint) (*models.UserLevelConfig, error)
Update(id uint, req *models.UserLevelConfigUpdateRequest) (*models.UserLevelConfig, error)
Delete(id uint) error
List(req *models.UserLevelConfigListRequest) ([]models.UserLevelConfig, int64, error)
UpdateStatus(id uint, status int) error
GetAll() ([]models.UserLevelConfig, error)
}
type userLevelConfigService struct {
storage storage.UserLevelConfigStorage
logger *utils.Logger
}
// NewUserLevelConfigService 创建用户等级配置服务实例
func NewUserLevelConfigService(storage storage.UserLevelConfigStorage, logger *utils.Logger) UserLevelConfigService {
return &userLevelConfigService{
storage: storage,
logger: logger,
}
}
// Create 创建用户等级配置
func (s *userLevelConfigService) Create(req *models.UserLevelConfigCreateRequest) (*models.UserLevelConfig, error) {
// 检查等级代码是否已存在
_, err := s.storage.GetByLevelCode(req.LevelCode)
if err == nil {
return nil, errors.New("等级代码已存在")
}
// 创建配置
config := &models.UserLevelConfig{
LevelName: req.LevelName,
LevelCode: req.LevelCode,
ProjectLimit: req.ProjectLimit,
CoderVMLimit: req.CoderVMLimit,
BrowserVMLimit: req.BrowserVMLimit,
ProcessLimit: req.ProcessLimit,
Description: req.Description,
SortOrder: req.SortOrder,
Status: 1, // 默认启用
}
err = s.storage.Create(config)
if err != nil {
s.logger.Error("创建用户等级配置失败", zap.Error(err))
return nil, errors.New("创建用户等级配置失败")
}
s.logger.Info("用户等级配置创建成功", zap.String("level_name", config.LevelName))
return config, nil
}
// GetByID 根据ID获取用户等级配置
func (s *userLevelConfigService) GetByID(id uint) (*models.UserLevelConfig, error) {
return s.storage.GetByID(id)
}
// Update 更新用户等级配置
func (s *userLevelConfigService) Update(id uint, req *models.UserLevelConfigUpdateRequest) (*models.UserLevelConfig, error) {
// 获取配置信息
config, err := s.storage.GetByID(id)
if err != nil {
return nil, errors.New("配置不存在")
}
// 更新字段
config.LevelName = req.LevelName
config.ProjectLimit = req.ProjectLimit
config.CoderVMLimit = req.CoderVMLimit
config.BrowserVMLimit = req.BrowserVMLimit
config.ProcessLimit = req.ProcessLimit
config.Description = req.Description
config.SortOrder = req.SortOrder
// 保存更新
err = s.storage.Update(config)
if err != nil {
s.logger.Error("更新用户等级配置失败", zap.Error(err))
return nil, errors.New("更新用户等级配置失败")
}
s.logger.Info("用户等级配置更新成功", zap.Uint("id", id))
return config, nil
}
// Delete 删除用户等级配置
func (s *userLevelConfigService) Delete(id uint) error {
// 检查配置是否存在
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("配置不存在")
}
// 删除配置
err = s.storage.Delete(id)
if err != nil {
s.logger.Error("删除用户等级配置失败", zap.Error(err))
return errors.New("删除用户等级配置失败")
}
s.logger.Info("用户等级配置删除成功", zap.Uint("id", id))
return nil
}
// List 获取用户等级配置列表
func (s *userLevelConfigService) List(req *models.UserLevelConfigListRequest) ([]models.UserLevelConfig, int64, error) {
return s.storage.List(req)
}
// UpdateStatus 更新状态
func (s *userLevelConfigService) UpdateStatus(id uint, status int) error {
// 检查配置是否存在
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("配置不存在")
}
// 更新状态
err = s.storage.UpdateStatus(id, status)
if err != nil {
s.logger.Error("更新用户等级配置状态失败", zap.Error(err))
return errors.New("更新用户等级配置状态失败")
}
s.logger.Info("用户等级配置状态更新成功", zap.Uint("id", id), zap.Int("status", status))
return nil
}
// GetAll 获取所有用户等级配置
func (s *userLevelConfigService) GetAll() ([]models.UserLevelConfig, error) {
return s.storage.GetAll()
}

View File

@@ -0,0 +1,79 @@
package services
import (
"errors"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
)
type UserProjectQuotaService interface {
Create(input *models.UserProjectQuota) (*models.UserProjectQuota, error)
Update(id uint, input *models.UserProjectQuota) (*models.UserProjectQuota, error)
Delete(id uint) error
GetByID(id uint) (*models.UserProjectQuota, error)
GetByUserID(userID string) (*models.UserProjectQuota, error)
List(filter storage.UserProjectQuotaFilter) ([]models.UserProjectQuota, int64, error)
}
type userProjectQuotaService struct {
store storage.UserProjectQuotaStorage
}
func NewUserProjectQuotaService(store storage.UserProjectQuotaStorage) UserProjectQuotaService {
return &userProjectQuotaService{store: store}
}
func (s *userProjectQuotaService) validate(q *models.UserProjectQuota) error {
if q.UserID == "" {
return errors.New("user_id 不能为空")
}
if q.ProjectLimit < 0 || q.CoderVMLimit < 0 || q.BrowserVMLimit < 0 || q.ProcessLimit < 0 {
return errors.New("配额上限不能为负数")
}
return nil
}
func (s *userProjectQuotaService) Create(input *models.UserProjectQuota) (*models.UserProjectQuota, error) {
if err := s.validate(input); err != nil {
return nil, err
}
if _, err := s.store.GetByUserID(input.UserID); err == nil {
return nil, errors.New("该用户配额已存在")
}
if err := s.store.Create(input); err != nil {
return nil, err
}
return input, nil
}
func (s *userProjectQuotaService) Update(id uint, input *models.UserProjectQuota) (*models.UserProjectQuota, error) {
if err := s.validate(input); err != nil {
return nil, err
}
exist, err := s.store.GetByID(id)
if err != nil {
return nil, errors.New("记录不存在")
}
// user_id 不建议修改如需修改可放开以下行exist.UserID = input.UserID
exist.ProjectLimit = input.ProjectLimit
exist.CoderVMLimit = input.CoderVMLimit
exist.BrowserVMLimit = input.BrowserVMLimit
exist.ProcessLimit = input.ProcessLimit
exist.Enabled = input.Enabled
exist.Description = input.Description
if err := s.store.Update(exist); err != nil {
return nil, err
}
return exist, nil
}
func (s *userProjectQuotaService) Delete(id uint) error { return s.store.Delete(id) }
func (s *userProjectQuotaService) GetByID(id uint) (*models.UserProjectQuota, error) {
return s.store.GetByID(id)
}
func (s *userProjectQuotaService) GetByUserID(userID string) (*models.UserProjectQuota, error) {
return s.store.GetByUserID(userID)
}
func (s *userProjectQuotaService) List(filter storage.UserProjectQuotaFilter) ([]models.UserProjectQuota, int64, error) {
return s.store.List(filter)
}

View File

@@ -0,0 +1,404 @@
package services
import (
"context"
"errors"
"fmt"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/utils"
"go.uber.org/zap"
)
// UserService 用户服务接口
type UserService interface {
Login(req *models.LoginRequest) (*models.LoginResponse, error)
Create(req *models.UserCreateRequest) (*models.User, error)
GetByID(id uint) (*models.User, error)
Update(id uint, req *models.UserUpdateRequest) (*models.User, error)
Delete(id uint) error
List(req *models.UserListRequest) ([]models.User, int64, error)
ListWithRoles(req *models.UserListRequest) ([]models.UserWithRoles, int64, error)
UpdateStatus(id uint, status int) error
UpdateRole(id uint, roleID uint) error
CheckUserSystemRole(userID int) (bool, error)
ChangeUserSystemRole(userID int, systemRole string) error
}
type userService struct {
userStorage storage.UserStorage
rbacStorage storage.RBACStorage
goalfyMaxUserStorage storage.GoalfyMaxUserStorage
jwtManager *utils.JWTManager
logger *utils.Logger
ssoAdminService SSOAdminService
}
// NewUserService 创建用户服务实例
func NewUserService(userStorage storage.UserStorage, rbacStorage storage.RBACStorage, goalfyMaxUserStorage storage.GoalfyMaxUserStorage, jwtManager *utils.JWTManager, logger *utils.Logger, ssoAdminService SSOAdminService) UserService {
return &userService{
userStorage: userStorage,
rbacStorage: rbacStorage,
goalfyMaxUserStorage: goalfyMaxUserStorage,
jwtManager: jwtManager,
logger: logger,
ssoAdminService: ssoAdminService,
}
}
// Login 用户登录
func (s *userService) Login(req *models.LoginRequest) (*models.LoginResponse, error) {
// 获取用户信息
user, err := s.userStorage.GetByUsername(req.Username)
if err != nil {
s.logger.Error("用户不存在", zap.String("username", req.Username))
return nil, errors.New("用户名或密码错误")
}
// 检查用户状态
if user.Status != 1 {
s.logger.Error("用户已被禁用", zap.String("username", req.Username))
return nil, errors.New("用户已被禁用")
}
// SSO用户不需要密码验证直接通过
// 生成JWT token
token, err := s.jwtManager.GenerateToken(user.ID, user.Username)
if err != nil {
s.logger.Error("生成token失败", zap.Error(err))
return nil, errors.New("登录失败")
}
// 记录登录日志
// 这里应该调用日志存储服务记录登录日志
s.logger.Info("用户登录成功", zap.String("username", user.Username))
return &models.LoginResponse{
Token: token,
User: *user,
ExpireAt: 0, // 实际应该计算过期时间
}, nil
}
// Create 创建用户
func (s *userService) Create(req *models.UserCreateRequest) (*models.User, error) {
// 验证用户名是否已存在
_, err := s.userStorage.GetByUsername(req.Username)
if err == nil {
return nil, errors.New("用户名已存在")
}
// 验证邮箱是否已存在
_, err = s.userStorage.GetByEmail(req.Email)
if err == nil {
return nil, errors.New("邮箱已存在")
}
// 1. 先调用SSO创建用户
ctx := context.Background()
ssoReq := &SSOAdminUserCreateRequest{
Username: req.Username,
Email: req.Email,
Phone: "", // 默认空手机号
Password: req.Password,
}
ssoUser, err := s.ssoAdminService.CreateUser(ctx, ssoReq)
if err != nil {
return nil, fmt.Errorf("SSO创建用户失败: %w", err)
}
// 2. 设置系统角色为sys_admin
if err := s.ssoAdminService.SetSystemRole(ctx, ssoUser.ID, "sys_admin"); err != nil {
// 如果设置失败,记录错误但不阻止用户创建
fmt.Printf("警告: 设置系统角色失败: %v\n", err)
}
// 设置角色ID如果没有指定则使用默认角色
roleID := req.RoleID
if roleID == 0 {
defaultRoleID, err := s.getDefaultRole()
if err != nil {
s.logger.Error("获取默认角色失败", zap.Error(err))
return nil, errors.New("获取默认角色失败")
}
roleID = defaultRoleID
}
// 创建用户
user := &models.User{
Username: req.Username,
Email: req.Email,
Nickname: req.Nickname,
Status: 1,
SSOProvider: req.SSOProvider,
RoleID: roleID,
}
err = s.userStorage.Create(user)
if err != nil {
s.logger.Error("创建用户失败", zap.Error(err))
return nil, errors.New("创建用户失败")
}
s.logger.Info("用户创建成功", zap.String("username", user.Username), zap.Uint("role_id", roleID))
return user, nil
}
// GetByID 根据ID获取用户
func (s *userService) GetByID(id uint) (*models.User, error) {
return s.userStorage.GetByID(id)
}
// Update 更新用户
func (s *userService) Update(id uint, req *models.UserUpdateRequest) (*models.User, error) {
// 获取用户信息
user, err := s.userStorage.GetByID(id)
if err != nil {
return nil, errors.New("用户不存在")
}
// 更新用户信息
if req.Nickname != "" {
user.Nickname = req.Nickname
}
if req.Email != "" {
// 检查邮箱是否已被其他用户使用
existingUser, err := s.userStorage.GetByEmail(req.Email)
if err == nil && existingUser.ID != id {
return nil, errors.New("邮箱已被其他用户使用")
}
user.Email = req.Email
}
if req.Avatar != "" {
user.Avatar = req.Avatar
}
if req.Status != nil {
user.Status = *req.Status
}
if req.RoleID != nil {
user.RoleID = *req.RoleID
}
if req.SSOProvider != "" {
user.SSOProvider = req.SSOProvider
}
err = s.userStorage.Update(user)
if err != nil {
s.logger.Error("更新用户失败", zap.Error(err))
return nil, errors.New("更新用户失败")
}
s.logger.Info("用户更新成功", zap.Uint("user_id", id))
return user, nil
}
// Delete 删除用户
func (s *userService) Delete(id uint) error {
// 检查用户是否存在
_, err := s.userStorage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
err = s.userStorage.Delete(id)
if err != nil {
s.logger.Error("删除用户失败", zap.Error(err))
return errors.New("删除用户失败")
}
s.logger.Info("用户删除成功", zap.Uint("user_id", id))
return nil
}
// List 获取用户列表
func (s *userService) List(req *models.UserListRequest) ([]models.User, int64, error) {
return s.userStorage.List(req)
}
// UpdateStatus 更新用户状态
func (s *userService) UpdateStatus(id uint, status int) error {
// 检查用户是否存在
_, err := s.userStorage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
err = s.userStorage.UpdateStatus(id, status)
if err != nil {
s.logger.Error("更新用户状态失败", zap.Error(err))
return errors.New("更新用户状态失败")
}
s.logger.Info("用户状态更新成功", zap.Uint("user_id", id), zap.Int("status", status))
return nil
}
// ListWithRoles 获取用户列表(包含角色信息)
func (s *userService) ListWithRoles(req *models.UserListRequest) ([]models.UserWithRoles, int64, error) {
// 获取用户列表
users, total, err := s.userStorage.List(req)
if err != nil {
return nil, 0, err
}
// 为每个用户查询角色信息
usersWithRoles := make([]models.UserWithRoles, len(users))
for i, user := range users {
if user.RoleID == 0 {
// role_id 为 0 表示未分配角色,直接返回空角色
s.logger.Debug("用户未分配角色", zap.Uint("userID", user.ID))
usersWithRoles[i] = models.UserWithRoles{User: user, Role: nil}
continue
}
// 查询用户角色
role, err := s.rbacStorage.GetRoleByID(user.RoleID)
if err != nil {
s.logger.Warn("获取用户角色失败", zap.Uint("userID", user.ID), zap.Uint("roleID", user.RoleID), zap.Error(err))
// 如果获取角色失败,仍然返回用户信息,但角色为空
usersWithRoles[i] = models.UserWithRoles{
User: user,
Role: nil,
}
} else {
usersWithRoles[i] = models.UserWithRoles{
User: user,
Role: role,
}
}
}
return usersWithRoles, total, nil
}
// getDefaultRole 获取默认角色L5全员
func (s *userService) getDefaultRole() (uint, error) {
// 查询默认角色ID
var roleID uint
err := s.rbacStorage.GetDefaultRoleID(&roleID)
if err != nil {
s.logger.Error("获取默认角色失败", zap.Error(err))
return 0, errors.New("获取默认角色失败")
}
return roleID, nil
}
// UpdateRole 更新用户角色
func (s *userService) UpdateRole(id uint, roleID uint) error {
// 检查用户是否存在
user, err := s.userStorage.GetByID(id)
if err != nil {
s.logger.Error("获取用户信息失败", zap.Uint("user_id", id), zap.Error(err))
return errors.New("用户不存在")
}
// 更新用户角色
user.RoleID = roleID
err = s.userStorage.Update(user)
if err != nil {
s.logger.Error("更新用户角色失败", zap.Uint("user_id", id), zap.Uint("role_id", roleID), zap.Error(err))
return errors.New("更新用户角色失败")
}
s.logger.Info("用户角色更新成功", zap.Uint("user_id", id), zap.Uint("role_id", roleID))
return nil
}
// CheckUserSystemRole 检查用户在系统用户管理表中的角色
// 返回: true表示存在sys_adminfalse表示不存在custom
func (s *userService) CheckUserSystemRole(userID int) (bool, error) {
// admin_users表的id字段就是SSO用户ID
// 所以通过userID查询admin_users表如果存在则说明是系统管理员
_, err := s.userStorage.GetByID(uint(userID))
if err != nil {
// 如果查询失败用户不存在返回false
return false, nil
}
// 用户存在返回true
return true, nil
}
// ChangeUserSystemRole 变更用户在SSO中的系统角色
// 同时需要在admin_users表中添加或删除记录
func (s *userService) ChangeUserSystemRole(userID int, systemRole string) error {
ctx := context.Background()
// 1. 先调用SSO接口变更角色
err := s.ssoAdminService.SetSystemRole(ctx, userID, systemRole)
if err != nil {
s.logger.Error("调用SSO接口变更角色失败", zap.Int("user_id", userID), zap.String("system_role", systemRole), zap.Error(err))
return fmt.Errorf("调用SSO接口变更角色失败: %w", err)
}
// 2. 根据角色变更更新admin_users表
if systemRole == "sys_admin" {
// 设置为系统管理员需要确保admin_users表中存在该用户
_, err := s.userStorage.GetByID(uint(userID))
if err != nil {
// 用户不存在,需要创建
// 从GoalfyMax用户表获取用户信息
goalfyUser, err := s.goalfyMaxUserStorage.GetByUserID(userID)
if err != nil {
s.logger.Error("获取GoalfyMax用户信息失败", zap.Int("user_id", userID), zap.Error(err))
return fmt.Errorf("获取GoalfyMax用户信息失败: %w", err)
}
// 获取默认角色ID系统管理员角色
defaultRoleID, err := s.getDefaultRole()
if err != nil {
s.logger.Error("获取默认角色失败", zap.Error(err))
// 如果获取默认角色失败使用角色ID 5系统管理员
defaultRoleID = 5
}
// 创建admin_users记录
// 注意admin_users表的id字段对应SSO用户ID
user := &models.User{
BaseModel: models.BaseModel{
ID: uint(userID), // 使用SSO用户ID作为主键
},
Username: goalfyUser.Username,
Email: goalfyUser.Email,
Nickname: goalfyUser.Nickname,
Status: 1,
SSOProvider: "default",
RoleID: defaultRoleID,
}
// 使用storage的Create方法它应该能处理指定ID的情况
err = s.userStorage.Create(user)
if err != nil {
s.logger.Error("创建admin_users记录失败", zap.Int("user_id", userID), zap.Error(err))
return fmt.Errorf("创建admin_users记录失败: %w", err)
}
s.logger.Info("创建系统管理员用户成功", zap.Int("user_id", userID))
} else {
// 用户已存在,更新状态为正常(如果需要)
s.logger.Info("用户已是系统管理员", zap.Int("user_id", userID))
}
} else if systemRole == "custom" {
// 设置为普通用户需要删除admin_users表中的记录软删除
_, err := s.userStorage.GetByID(uint(userID))
if err == nil {
// 用户存在,删除记录
err = s.userStorage.Delete(uint(userID))
if err != nil {
s.logger.Error("删除admin_users记录失败", zap.Int("user_id", userID), zap.Error(err))
return fmt.Errorf("删除admin_users记录失败: %w", err)
}
s.logger.Info("删除系统管理员用户成功", zap.Int("user_id", userID))
} else {
s.logger.Info("用户不是系统管理员,无需删除", zap.Int("user_id", userID))
}
} else {
return fmt.Errorf("无效的系统角色: %s", systemRole)
}
s.logger.Info("用户系统角色变更成功", zap.Int("user_id", userID), zap.String("system_role", systemRole))
return nil
}