Files
goalfylearning-admin/internal/services/invite_code_application_service.go

311 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}