refactor: admin 模块按 handler/model/service 分层

This commit is contained in:
root
2026-03-09 21:13:49 +08:00
parent 7591a443d9
commit f45940cbc5
11 changed files with 101 additions and 38 deletions
+146
View File
@@ -0,0 +1,146 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
"time"
adminmodel "wx_service/internal/admin/model"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
func (s *Service) EnsureDefaultAdmin(ctx context.Context) error {
var count int64
if err := s.db.WithContext(ctx).Model(&adminmodel.Admin{}).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
username := s.defaultUsername
if username == "" {
username = "admin"
}
password := strings.TrimSpace(s.defaultPassword)
if password == "" {
password = "admin123"
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
record := &adminmodel.Admin{
Username: username,
Password: string(hashedPassword),
Role: "super_admin",
}
if err := s.db.WithContext(ctx).Create(record).Error; err != nil {
if isDuplicateError(err) {
return nil
}
return err
}
return nil
}
type LoginResult struct {
Token string `json:"token"`
Admin *adminmodel.Admin `json:"admin"`
}
func (s *Service) Login(ctx context.Context, username, password string) (*LoginResult, error) {
username = strings.TrimSpace(username)
if username == "" || password == "" {
return nil, ErrInvalidInput
}
var admin adminmodel.Admin
if err := s.db.WithContext(ctx).Where("username = ?", username).First(&admin).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrInvalidCredentials
}
return nil, err
}
if err := bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(password)); err != nil {
return nil, ErrInvalidCredentials
}
token, err := s.generateToken(&admin)
if err != nil {
return nil, err
}
now := time.Now()
if err := s.db.WithContext(ctx).Model(&admin).Update("last_login_at", now).Error; err != nil {
return nil, err
}
admin.LastLoginAt = &now
admin.Password = ""
return &LoginResult{
Token: token,
Admin: &admin,
}, nil
}
func (s *Service) ParseToken(token string) (*Claims, error) {
if len(s.jwtSecret) == 0 {
return nil, ErrMissingJWTSecret
}
claims := &Claims{}
parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, ErrInvalidToken
}
return s.jwtSecret, nil
})
if err != nil || parsed == nil || !parsed.Valid {
return nil, ErrInvalidToken
}
if claims.AdminID == 0 {
return nil, ErrInvalidToken
}
return claims, nil
}
func (s *Service) generateToken(admin *adminmodel.Admin) (string, error) {
if len(s.jwtSecret) == 0 {
return "", ErrMissingJWTSecret
}
now := time.Now()
claims := &Claims{
AdminID: admin.ID,
Username: admin.Username,
Role: admin.Role,
RegisteredClaims: jwt.RegisteredClaims{
Subject: fmt.Sprintf("%d", admin.ID),
ExpiresAt: jwt.NewNumericDate(now.Add(s.tokenTTL)),
IssuedAt: jwt.NewNumericDate(now),
},
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return t.SignedString(s.jwtSecret)
}
func (s *Service) GetProfile(ctx context.Context, adminID uint) (*adminmodel.Admin, error) {
var admin adminmodel.Admin
if err := s.db.WithContext(ctx).
Select("id", "username", "role", "last_login_at", "created_at", "updated_at").
Where("id = ?", adminID).
First(&admin).Error; err != nil {
return nil, err
}
return &admin, nil
}
@@ -0,0 +1,265 @@
package service
import (
"context"
"errors"
"strings"
"time"
expirymodel "wx_service/internal/expiry"
membershipmodel "wx_service/internal/membership/model"
"wx_service/internal/model"
rmmodel "wx_service/internal/remove_watermark/model"
"gorm.io/gorm"
)
type ListMiniProgramsQuery struct {
Page int
PageSize int
Keyword string
}
type MiniProgramItem struct {
ID uint `json:"id"`
Name string `json:"name"`
AppID string `json:"app_id"`
Description string `json:"description"`
UserCount int64 `json:"user_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
AppSecretSet bool `json:"app_secret_set"`
}
type ListMiniProgramsResult struct {
List []MiniProgramItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func (s *Service) ListMiniPrograms(ctx context.Context, query ListMiniProgramsQuery) (*ListMiniProgramsResult, error) {
if query.Page < 1 {
query.Page = 1
}
if query.PageSize < 1 {
query.PageSize = 20
}
if query.PageSize > 100 {
query.PageSize = 100
}
query.Keyword = strings.TrimSpace(query.Keyword)
dbQuery := s.db.WithContext(ctx).Model(&model.MiniProgram{})
if query.Keyword != "" {
keywordLike := "%" + query.Keyword + "%"
dbQuery = dbQuery.Where("name LIKE ? OR app_id LIKE ?", keywordLike, keywordLike)
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
var miniPrograms []model.MiniProgram
if total > 0 {
if err := dbQuery.Order("id DESC").
Limit(query.PageSize).
Offset((query.Page - 1) * query.PageSize).
Find(&miniPrograms).Error; err != nil {
return nil, err
}
}
ids := make([]uint, 0, len(miniPrograms))
for _, item := range miniPrograms {
ids = append(ids, item.ID)
}
userCountMap, err := s.groupCountByMiniProgramID(ctx, &model.User{}, ids, "")
if err != nil {
return nil, err
}
result := make([]MiniProgramItem, 0, len(miniPrograms))
for _, item := range miniPrograms {
result = append(result, MiniProgramItem{
ID: item.ID,
Name: item.Name,
AppID: item.AppID,
Description: item.Description,
UserCount: userCountMap[item.ID],
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
AppSecretSet: item.AppSecret != "",
})
}
return &ListMiniProgramsResult{
List: result,
Total: total,
Page: query.Page,
PageSize: query.PageSize,
}, nil
}
func (s *Service) GetMiniProgram(ctx context.Context, id uint) (*MiniProgramItem, error) {
var miniProgram model.MiniProgram
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&miniProgram).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrMiniProgramNotFound
}
return nil, err
}
var userCount int64
if err := s.db.WithContext(ctx).Model(&model.User{}).Where("mini_program_id = ?", id).Count(&userCount).Error; err != nil {
return nil, err
}
return &MiniProgramItem{
ID: miniProgram.ID,
Name: miniProgram.Name,
AppID: miniProgram.AppID,
Description: miniProgram.Description,
UserCount: userCount,
CreatedAt: miniProgram.CreatedAt,
UpdatedAt: miniProgram.UpdatedAt,
AppSecretSet: miniProgram.AppSecret != "",
}, nil
}
type CreateMiniProgramInput struct {
Name string
AppID string
AppSecret string
Description string
}
func (s *Service) CreateMiniProgram(ctx context.Context, input CreateMiniProgramInput) (*MiniProgramItem, error) {
input.Name = strings.TrimSpace(input.Name)
input.AppID = strings.TrimSpace(input.AppID)
input.AppSecret = strings.TrimSpace(input.AppSecret)
input.Description = strings.TrimSpace(input.Description)
if input.Name == "" || input.AppID == "" || input.AppSecret == "" {
return nil, ErrInvalidInput
}
item := &model.MiniProgram{
Name: input.Name,
AppID: input.AppID,
AppSecret: input.AppSecret,
Description: input.Description,
}
if err := s.db.WithContext(ctx).Create(item).Error; err != nil {
if isDuplicateError(err) {
return nil, ErrMiniProgramAppIDUsed
}
return nil, err
}
return s.GetMiniProgram(ctx, item.ID)
}
type UpdateMiniProgramInput struct {
Name string
AppID string
AppSecret *string
Description string
}
func (s *Service) UpdateMiniProgram(ctx context.Context, id uint, input UpdateMiniProgramInput) (*MiniProgramItem, error) {
input.Name = strings.TrimSpace(input.Name)
input.AppID = strings.TrimSpace(input.AppID)
input.Description = strings.TrimSpace(input.Description)
if input.Name == "" || input.AppID == "" {
return nil, ErrInvalidInput
}
var item model.MiniProgram
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrMiniProgramNotFound
}
return nil, err
}
updateData := map[string]interface{}{
"name": input.Name,
"app_id": input.AppID,
"description": input.Description,
}
if input.AppSecret != nil {
trimmedSecret := strings.TrimSpace(*input.AppSecret)
if trimmedSecret != "" {
updateData["app_secret"] = trimmedSecret
}
}
if err := s.db.WithContext(ctx).Model(&item).Updates(updateData).Error; err != nil {
if isDuplicateError(err) {
return nil, ErrMiniProgramAppIDUsed
}
return nil, err
}
return s.GetMiniProgram(ctx, id)
}
func (s *Service) DeleteMiniProgram(ctx context.Context, id uint) error {
var item model.MiniProgram
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrMiniProgramNotFound
}
return err
}
var userCount int64
if err := s.db.WithContext(ctx).Model(&model.User{}).Where("mini_program_id = ?", id).Count(&userCount).Error; err != nil {
return err
}
if userCount > 0 {
return ErrMiniProgramHasUsers
}
return s.db.WithContext(ctx).Delete(&item).Error
}
type MiniProgramDetailStats struct {
UserCount int64 `json:"user_count"`
DataCount int64 `json:"data_count"`
}
func (s *Service) GetMiniProgramStats(ctx context.Context, id uint) (*MiniProgramDetailStats, error) {
var exists int64
if err := s.db.WithContext(ctx).Model(&model.MiniProgram{}).Where("id = ?", id).Count(&exists).Error; err != nil {
return nil, err
}
if exists == 0 {
return nil, ErrMiniProgramNotFound
}
userCounts, err := s.groupCountByMiniProgramID(ctx, &model.User{}, []uint{id}, "")
if err != nil {
return nil, err
}
expiryCounts, err := s.groupCountByMiniProgramID(ctx, &expirymodel.ExpiryItem{}, []uint{id}, "")
if err != nil {
return nil, err
}
videoCounts, err := s.groupCountByMiniProgramID(ctx, &rmmodel.VideoParseLog{}, []uint{id}, "")
if err != nil {
return nil, err
}
redeemCounts, err := s.groupCountByMiniProgramID(ctx, &membershipmodel.MembershipRedemption{}, []uint{id}, "")
if err != nil {
return nil, err
}
dataCount := expiryCounts[id] + videoCounts[id] + redeemCounts[id]
return &MiniProgramDetailStats{
UserCount: userCounts[id],
DataCount: dataCount,
}, nil
}
+187
View File
@@ -0,0 +1,187 @@
package service
import (
"context"
"time"
expirymodel "wx_service/internal/expiry"
membershipmodel "wx_service/internal/membership/model"
"wx_service/internal/model"
rmmodel "wx_service/internal/remove_watermark/model"
)
type OverviewStats struct {
TotalMiniPrograms int64 `json:"total_mini_programs"`
TotalUsers int64 `json:"total_users"`
TotalMembers int64 `json:"total_members"`
TodayNewUsers int64 `json:"today_new_users"`
}
func (s *Service) StatsOverview(ctx context.Context) (*OverviewStats, error) {
var result OverviewStats
if err := s.db.WithContext(ctx).Model(&model.MiniProgram{}).Count(&result.TotalMiniPrograms).Error; err != nil {
return nil, err
}
if err := s.db.WithContext(ctx).Model(&model.User{}).Count(&result.TotalUsers).Error; err != nil {
return nil, err
}
now := time.Now()
if err := s.db.WithContext(ctx).Model(&model.UserMembership{}).
Where("status = ? AND ends_at > ?", "active", now).
Count(&result.TotalMembers).Error; err != nil {
return nil, err
}
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
if err := s.db.WithContext(ctx).Model(&model.User{}).
Where("created_at >= ?", todayStart).
Count(&result.TodayNewUsers).Error; err != nil {
return nil, err
}
return &result, nil
}
type MiniProgramStatsItem struct {
MiniProgramID uint `json:"mini_program_id"`
Name string `json:"name"`
UserCount int64 `json:"user_count"`
MemberCount int64 `json:"member_count"`
DataCount int64 `json:"data_count"`
TodayActive int64 `json:"today_active"`
}
func (s *Service) StatsMiniPrograms(ctx context.Context) ([]MiniProgramStatsItem, error) {
var miniPrograms []model.MiniProgram
if err := s.db.WithContext(ctx).
Select("id", "name", "app_id", "description", "created_at", "updated_at").
Order("id ASC").
Find(&miniPrograms).Error; err != nil {
return nil, err
}
if len(miniPrograms) == 0 {
return []MiniProgramStatsItem{}, nil
}
ids := make([]uint, 0, len(miniPrograms))
for _, item := range miniPrograms {
ids = append(ids, item.ID)
}
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
userCounts, err := s.groupCountByMiniProgramID(ctx, &model.User{}, ids, "")
if err != nil {
return nil, err
}
memberCounts, err := s.groupCountByMiniProgramID(ctx, &model.UserMembership{}, ids, "status = ? AND ends_at > ?", "active", now)
if err != nil {
return nil, err
}
todayUserCounts, err := s.groupCountByMiniProgramID(ctx, &model.User{}, ids, "created_at >= ?", todayStart)
if err != nil {
return nil, err
}
expiryCounts, err := s.groupCountByMiniProgramID(ctx, &expirymodel.ExpiryItem{}, ids, "")
if err != nil {
return nil, err
}
videoCounts, err := s.groupCountByMiniProgramID(ctx, &rmmodel.VideoParseLog{}, ids, "")
if err != nil {
return nil, err
}
redeemCounts, err := s.groupCountByMiniProgramID(ctx, &membershipmodel.MembershipRedemption{}, ids, "")
if err != nil {
return nil, err
}
result := make([]MiniProgramStatsItem, 0, len(miniPrograms))
for _, item := range miniPrograms {
dataCount := expiryCounts[item.ID] + videoCounts[item.ID] + redeemCounts[item.ID]
result = append(result, MiniProgramStatsItem{
MiniProgramID: item.ID,
Name: item.Name,
UserCount: userCounts[item.ID],
MemberCount: memberCounts[item.ID],
DataCount: dataCount,
TodayActive: todayUserCounts[item.ID],
})
}
return result, nil
}
type UserGrowthPoint struct {
Date string `json:"date"`
Count int64 `json:"count"`
}
func (s *Service) StatsUserGrowth(ctx context.Context, days int) ([]UserGrowthPoint, error) {
if days <= 0 {
days = 7
}
if days > 90 {
days = 90
}
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
startDate := todayStart.AddDate(0, 0, -(days - 1))
endDate := todayStart.AddDate(0, 0, 1)
type row struct {
Date string `gorm:"column:date"`
Count int64 `gorm:"column:count"`
}
var rows []row
if err := s.db.WithContext(ctx).
Model(&model.User{}).
Select("DATE(created_at) AS date, COUNT(*) AS count").
Where("created_at >= ? AND created_at < ?", startDate, endDate).
Group("DATE(created_at)").
Order("DATE(created_at) ASC").
Scan(&rows).Error; err != nil {
return nil, err
}
countByDate := make(map[string]int64, len(rows))
for _, item := range rows {
countByDate[item.Date] = item.Count
}
result := make([]UserGrowthPoint, 0, days)
for i := 0; i < days; i++ {
date := startDate.AddDate(0, 0, i).Format("2006-01-02")
result = append(result, UserGrowthPoint{
Date: date,
Count: countByDate[date],
})
}
return result, nil
}
func (s *Service) groupCountByMiniProgramID(ctx context.Context, modelObj interface{}, ids []uint, where string, args ...interface{}) (map[uint]int64, error) {
result := make(map[uint]int64)
if len(ids) == 0 {
return result, nil
}
query := s.db.WithContext(ctx).
Model(modelObj).
Select("mini_program_id, COUNT(*) AS count").
Where("mini_program_id IN ?", ids)
if where != "" {
query = query.Where(where, args...)
}
var rows []groupedCountRow
if err := query.Group("mini_program_id").Scan(&rows).Error; err != nil {
return nil, err
}
for _, item := range rows {
result[item.MiniProgramID] = item.Count
}
return result, nil
}
+61
View File
@@ -0,0 +1,61 @@
package service
import (
"errors"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"gorm.io/gorm"
)
var (
ErrInvalidCredentials = errors.New("invalid credentials")
ErrInvalidToken = errors.New("invalid token")
ErrMissingJWTSecret = errors.New("missing jwt secret")
ErrMiniProgramNotFound = errors.New("mini program not found")
ErrMiniProgramHasUsers = errors.New("mini program has users")
ErrMiniProgramAppIDUsed = errors.New("mini program app_id already exists")
ErrInvalidInput = errors.New("invalid input")
)
type Claims struct {
AdminID uint `json:"admin_id"`
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
type Service struct {
db *gorm.DB
jwtSecret []byte
tokenTTL time.Duration
defaultUsername string
defaultPassword string
}
func NewService(db *gorm.DB, jwtSecret string, tokenTTL time.Duration, defaultUsername, defaultPassword string) *Service {
if tokenTTL <= 0 {
tokenTTL = 24 * time.Hour
}
return &Service{
db: db,
jwtSecret: []byte(strings.TrimSpace(jwtSecret)),
tokenTTL: tokenTTL,
defaultUsername: strings.TrimSpace(defaultUsername),
defaultPassword: defaultPassword,
}
}
type groupedCountRow struct {
MiniProgramID uint `gorm:"column:mini_program_id"`
Count int64 `gorm:"column:count"`
}
func isDuplicateError(err error) bool {
if err == nil {
return false
}
errText := strings.ToLower(err.Error())
return strings.Contains(errText, "duplicate") || strings.Contains(errText, "unique")
}