feat: 新增admin用户与会员管理接口
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
adminservice "wx_service/internal/admin/service"
|
||||
"wx_service/internal/model"
|
||||
)
|
||||
|
||||
func (h *Handler) MembershipOverview(c *gin.Context) {
|
||||
data, err := h.svc.MembershipOverview(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load membership overview failed"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Success(data))
|
||||
}
|
||||
|
||||
func (h *Handler) ListMembershipRedeemCodes(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("page", "1")))
|
||||
pageSize, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("page_size", "20")))
|
||||
|
||||
data, err := h.svc.ListMembershipRedeemCodes(c.Request.Context(), adminservice.ListMembershipRedeemCodesQuery{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Keyword: c.Query("keyword"),
|
||||
Status: c.Query("status"),
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load redeem codes failed"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Success(data))
|
||||
}
|
||||
|
||||
type createMembershipRedeemCodesRequest struct {
|
||||
PackageType string `json:"package_type"`
|
||||
DurationDays int `json:"duration_days"`
|
||||
Quantity int `json:"quantity"`
|
||||
MaxUses int `json:"max_uses"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
|
||||
func (h *Handler) CreateMembershipRedeemCodes(c *gin.Context) {
|
||||
var req createMembershipRedeemCodesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
|
||||
return
|
||||
}
|
||||
|
||||
var expiresAt *time.Time
|
||||
if strings.TrimSpace(req.ExpiresAt) != "" {
|
||||
parsed, err := time.ParseInLocation("2006-01-02 15:04:05", req.ExpiresAt, time.Local)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "expires_at format should be YYYY-MM-DD HH:mm:ss"))
|
||||
return
|
||||
}
|
||||
expiresAt = &parsed
|
||||
}
|
||||
|
||||
codes, err := h.svc.CreateMembershipRedeemCodes(c.Request.Context(), adminservice.CreateMembershipRedeemCodesInput{
|
||||
PackageType: req.PackageType,
|
||||
DurationDays: req.DurationDays,
|
||||
Quantity: req.Quantity,
|
||||
MaxUses: req.MaxUses,
|
||||
ExpiresAt: expiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, adminservice.ErrInvalidInput) {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid input"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "create redeem codes failed"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(gin.H{
|
||||
"count": len(codes),
|
||||
"codes": codes,
|
||||
}))
|
||||
}
|
||||
|
||||
type updateMembershipRedeemCodeStatusRequest struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateMembershipRedeemCodeStatus(c *gin.Context) {
|
||||
id, err := parseUintID(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid redeem code id"))
|
||||
return
|
||||
}
|
||||
|
||||
var req updateMembershipRedeemCodeStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
|
||||
return
|
||||
}
|
||||
|
||||
err = h.svc.UpdateMembershipRedeemCodeStatus(c.Request.Context(), id, req.Status)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, adminservice.ErrMembershipRedeemCodeNotFound):
|
||||
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "redeem code not found"))
|
||||
case errors.Is(err, adminservice.ErrInvalidInput):
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "status must be active/disabled"))
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "update redeem code status failed"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(gin.H{"message": "更新成功"}))
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
adminservice "wx_service/internal/admin/service"
|
||||
"wx_service/internal/model"
|
||||
)
|
||||
|
||||
func (h *Handler) ListUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("page", "1")))
|
||||
pageSize, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("page_size", "20")))
|
||||
miniProgramID, _ := strconv.ParseUint(strings.TrimSpace(c.DefaultQuery("mini_program_id", "0")), 10, 64)
|
||||
|
||||
var isMember *bool
|
||||
if raw := strings.TrimSpace(c.Query("is_member")); raw != "" {
|
||||
value := raw == "1" || strings.EqualFold(raw, "true")
|
||||
isMember = &value
|
||||
}
|
||||
|
||||
data, err := h.svc.ListUsers(c.Request.Context(), adminservice.ListUsersQuery{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
MiniProgramID: uint(miniProgramID),
|
||||
Keyword: c.Query("keyword"),
|
||||
IsMember: isMember,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load users failed"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Success(data))
|
||||
}
|
||||
|
||||
func (h *Handler) GetUserDetail(c *gin.Context) {
|
||||
id, err := parseUintID(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid user id"))
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.svc.GetUserDetail(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, adminservice.ErrAdminUserNotFound) {
|
||||
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "user not found"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load user detail failed"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Success(data))
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
membershipmodel "wx_service/internal/membership/model"
|
||||
"wx_service/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MembershipOverview struct {
|
||||
TotalMembers int64 `json:"total_members"`
|
||||
ActiveMembers int64 `json:"active_members"`
|
||||
ExpiringSoon int64 `json:"expiring_soon"`
|
||||
TodayRedeemed int64 `json:"today_redeemed"`
|
||||
}
|
||||
|
||||
type ListMembershipRedeemCodesQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Keyword string
|
||||
Status string
|
||||
}
|
||||
|
||||
type MembershipRedeemCodeItem struct {
|
||||
ID uint `json:"id"`
|
||||
Code string `json:"code"`
|
||||
CodeSuffix string `json:"code_suffix"`
|
||||
PackageType string `json:"package_type"`
|
||||
DurationDays int `json:"duration_days"`
|
||||
MaxUses int `json:"max_uses"`
|
||||
UsedCount int `json:"used_count"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type ListMembershipRedeemCodesResult struct {
|
||||
List []MembershipRedeemCodeItem `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type CreateMembershipRedeemCodesInput struct {
|
||||
PackageType string
|
||||
DurationDays int
|
||||
Quantity int
|
||||
MaxUses int
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
|
||||
func (s *Service) MembershipOverview(ctx context.Context) (*MembershipOverview, error) {
|
||||
result := &MembershipOverview{}
|
||||
now := time.Now()
|
||||
|
||||
if err := s.db.WithContext(ctx).Model(&model.UserMembership{}).Count(&result.TotalMembers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&model.UserMembership{}).
|
||||
Where("status = ?", "active").
|
||||
Where("ends_at > ?", now).
|
||||
Count(&result.ActiveMembers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&model.UserMembership{}).
|
||||
Where("status = ?", "active").
|
||||
Where("ends_at > ?", now).
|
||||
Where("ends_at <= ?", now.AddDate(0, 0, 7)).
|
||||
Count(&result.ExpiringSoon).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(&membershipmodel.MembershipRedemption{}).
|
||||
Where("created_at >= ?", todayStart).
|
||||
Count(&result.TodayRedeemed).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListMembershipRedeemCodes(ctx context.Context, query ListMembershipRedeemCodesQuery) (*ListMembershipRedeemCodesResult, 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(strings.ToUpper(query.Keyword))
|
||||
query.Status = strings.TrimSpace(strings.ToLower(query.Status))
|
||||
|
||||
dbQuery := s.db.WithContext(ctx).Model(&membershipmodel.MembershipRedeemCode{})
|
||||
if query.Keyword != "" {
|
||||
dbQuery = dbQuery.Where("code_suffix LIKE ?", "%"+query.Keyword+"%")
|
||||
}
|
||||
if query.Status != "" {
|
||||
dbQuery = dbQuery.Where("status = ?", query.Status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := dbQuery.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rows []membershipmodel.MembershipRedeemCode
|
||||
if total > 0 {
|
||||
if err := dbQuery.Order("id DESC").
|
||||
Limit(query.PageSize).
|
||||
Offset((query.Page - 1) * query.PageSize).
|
||||
Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]MembershipRedeemCodeItem, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
status := item.Status
|
||||
if item.ExpiresAt != nil && item.ExpiresAt.Before(time.Now()) {
|
||||
status = "expired"
|
||||
}
|
||||
result = append(result, MembershipRedeemCodeItem{
|
||||
ID: item.ID,
|
||||
Code: "******" + item.CodeSuffix,
|
||||
CodeSuffix: item.CodeSuffix,
|
||||
PackageType: item.Plan,
|
||||
DurationDays: item.DurationDays,
|
||||
MaxUses: item.MaxUses,
|
||||
UsedCount: item.UsedUses,
|
||||
Status: status,
|
||||
ExpiresAt: item.ExpiresAt,
|
||||
CreatedAt: item.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &ListMembershipRedeemCodesResult{
|
||||
List: result,
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateMembershipRedeemCodes(ctx context.Context, input CreateMembershipRedeemCodesInput) ([]string, error) {
|
||||
packageType := strings.TrimSpace(input.PackageType)
|
||||
if packageType == "" {
|
||||
packageType = "month"
|
||||
}
|
||||
if input.DurationDays <= 0 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
if input.Quantity <= 0 {
|
||||
input.Quantity = 1
|
||||
}
|
||||
if input.Quantity > 500 {
|
||||
input.Quantity = 500
|
||||
}
|
||||
if input.MaxUses <= 0 {
|
||||
input.MaxUses = 1
|
||||
}
|
||||
|
||||
codes := make([]string, 0, input.Quantity)
|
||||
records := make([]membershipmodel.MembershipRedeemCode, 0, input.Quantity)
|
||||
for len(codes) < input.Quantity {
|
||||
code, err := generateAdminRedeemCode(20)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash := hashAdminRedeemCode(code)
|
||||
suffix := suffixAdminRedeemCode(code, 6)
|
||||
|
||||
records = append(records, membershipmodel.MembershipRedeemCode{
|
||||
CodeHash: hash,
|
||||
CodeSuffix: suffix,
|
||||
Plan: packageType,
|
||||
DurationDays: input.DurationDays,
|
||||
ExpiresAt: input.ExpiresAt,
|
||||
MaxUses: input.MaxUses,
|
||||
UsedUses: 0,
|
||||
Status: "active",
|
||||
})
|
||||
codes = append(codes, code)
|
||||
}
|
||||
|
||||
if err := s.db.WithContext(ctx).Create(&records).Error; err != nil {
|
||||
return nil, fmt.Errorf("save redeem codes: %w", err)
|
||||
}
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateMembershipRedeemCodeStatus(ctx context.Context, id uint, status string) error {
|
||||
status = strings.TrimSpace(strings.ToLower(status))
|
||||
if status != "active" && status != "disabled" {
|
||||
return ErrInvalidInput
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).
|
||||
Model(&membershipmodel.MembershipRedeemCode{}).
|
||||
Where("id = ?", id).
|
||||
Update("status", status)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrMembershipRedeemCodeNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateAdminRedeemCode(length int) (string, error) {
|
||||
if length <= 0 {
|
||||
length = 16
|
||||
}
|
||||
const alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
|
||||
buf := make([]byte, length)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
out := make([]byte, length)
|
||||
for i, b := range buf {
|
||||
out[i] = alphabet[int(b)%len(alphabet)]
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func hashAdminRedeemCode(code string) string {
|
||||
sum := sha256.Sum256([]byte(code))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func suffixAdminRedeemCode(code string, n int) string {
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
if len(code) <= n {
|
||||
return code
|
||||
}
|
||||
return code[len(code)-n:]
|
||||
}
|
||||
|
||||
func (s *Service) EnsureMembershipRedeemCode(id uint) error {
|
||||
var count int64
|
||||
if err := s.db.Model(&membershipmodel.MembershipRedeemCode{}).Where("id = ?", id).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return ErrMembershipRedeemCodeNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isNotFound(err error) bool {
|
||||
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||
}
|
||||
@@ -16,6 +16,8 @@ var (
|
||||
ErrMiniProgramNotFound = errors.New("mini program not found")
|
||||
ErrMiniProgramHasUsers = errors.New("mini program has users")
|
||||
ErrMiniProgramAppIDUsed = errors.New("mini program app_id already exists")
|
||||
ErrAdminUserNotFound = errors.New("admin user not found")
|
||||
ErrMembershipRedeemCodeNotFound = errors.New("membership redeem code not found")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
expirymodel "wx_service/internal/expiry/model"
|
||||
membershipmodel "wx_service/internal/membership/model"
|
||||
"wx_service/internal/model"
|
||||
rmmodel "wx_service/internal/remove_watermark/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ListUsersQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
MiniProgramID uint
|
||||
Keyword string
|
||||
IsMember *bool
|
||||
}
|
||||
|
||||
type UserListItem struct {
|
||||
ID uint `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Phone string `json:"phone"`
|
||||
MiniProgramID uint `json:"mini_program_id"`
|
||||
MiniProgramName string `json:"mini_program_name"`
|
||||
IsMember bool `json:"is_member"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type ListUsersResult struct {
|
||||
List []UserListItem `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type UserMembershipInfo struct {
|
||||
IsMember bool `json:"is_member"`
|
||||
PackageType string `json:"package_type"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
type UserDetail struct {
|
||||
ID uint `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Phone string `json:"phone"`
|
||||
OpenID string `json:"open_id"`
|
||||
Gender int `json:"gender"`
|
||||
MiniProgramID uint `json:"mini_program_id"`
|
||||
MiniProgramName string `json:"mini_program_name"`
|
||||
IsMember bool `json:"is_member"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Membership UserMembershipInfo `json:"membership"`
|
||||
Stats map[string]int64 `json:"stats"`
|
||||
}
|
||||
|
||||
func (s *Service) ListUsers(ctx context.Context, query ListUsersQuery) (*ListUsersResult, 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)
|
||||
|
||||
now := time.Now()
|
||||
dbQuery := s.db.WithContext(ctx).Model(&model.User{})
|
||||
if query.MiniProgramID > 0 {
|
||||
dbQuery = dbQuery.Where("mini_program_id = ?", query.MiniProgramID)
|
||||
}
|
||||
if query.Keyword != "" {
|
||||
likeKeyword := "%" + query.Keyword + "%"
|
||||
dbQuery = dbQuery.Where("nick_name LIKE ? OR phone LIKE ?", likeKeyword, likeKeyword)
|
||||
}
|
||||
if query.IsMember != nil {
|
||||
subQuery := s.db.WithContext(ctx).Table("user_memberships AS um").
|
||||
Select("1").
|
||||
Where("um.user_id = users.id").
|
||||
Where("um.status = ?", "active").
|
||||
Where("um.ends_at > ?", now)
|
||||
if *query.IsMember {
|
||||
dbQuery = dbQuery.Where("EXISTS (?)", subQuery)
|
||||
} else {
|
||||
dbQuery = dbQuery.Where("NOT EXISTS (?)", subQuery)
|
||||
}
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := dbQuery.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users []model.User
|
||||
if total > 0 {
|
||||
if err := dbQuery.Order("id DESC").
|
||||
Limit(query.PageSize).
|
||||
Offset((query.Page - 1) * query.PageSize).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
miniProgramIDs := make([]uint, 0, len(users))
|
||||
userIDs := make([]uint, 0, len(users))
|
||||
for _, user := range users {
|
||||
miniProgramIDs = append(miniProgramIDs, user.MiniProgramID)
|
||||
userIDs = append(userIDs, user.ID)
|
||||
}
|
||||
|
||||
miniProgramNameMap := map[uint]string{}
|
||||
if len(miniProgramIDs) > 0 {
|
||||
var miniPrograms []model.MiniProgram
|
||||
if err := s.db.WithContext(ctx).
|
||||
Select("id", "name").
|
||||
Where("id IN ?", miniProgramIDs).
|
||||
Find(&miniPrograms).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range miniPrograms {
|
||||
miniProgramNameMap[item.ID] = item.Name
|
||||
}
|
||||
}
|
||||
|
||||
memberUserMap := map[uint]bool{}
|
||||
if len(userIDs) > 0 {
|
||||
var memberships []model.UserMembership
|
||||
if err := s.db.WithContext(ctx).
|
||||
Select("user_id").
|
||||
Where("user_id IN ?", userIDs).
|
||||
Where("status = ?", "active").
|
||||
Where("ends_at > ?", now).
|
||||
Find(&memberships).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, membership := range memberships {
|
||||
memberUserMap[membership.UserID] = true
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]UserListItem, 0, len(users))
|
||||
for _, user := range users {
|
||||
result = append(result, UserListItem{
|
||||
ID: user.ID,
|
||||
Nickname: user.NickName,
|
||||
AvatarURL: user.AvatarURL,
|
||||
Phone: user.Phone,
|
||||
MiniProgramID: user.MiniProgramID,
|
||||
MiniProgramName: miniProgramNameMap[user.MiniProgramID],
|
||||
IsMember: memberUserMap[user.ID],
|
||||
CreatedAt: user.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &ListUsersResult{
|
||||
List: result,
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetUserDetail(ctx context.Context, userID uint) (*UserDetail, error) {
|
||||
var user model.User
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrAdminUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var miniProgram model.MiniProgram
|
||||
_ = s.db.WithContext(ctx).Select("id", "name").Where("id = ?", user.MiniProgramID).First(&miniProgram).Error
|
||||
|
||||
now := time.Now()
|
||||
membershipInfo := UserMembershipInfo{IsMember: false}
|
||||
var membership model.UserMembership
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("user_id = ?", user.ID).
|
||||
Where("status = ?", "active").
|
||||
Where("ends_at > ?", now).
|
||||
Order("ends_at DESC").
|
||||
First(&membership).Error; err == nil {
|
||||
expiresAt := membership.EndsAt
|
||||
membershipInfo = UserMembershipInfo{
|
||||
IsMember: true,
|
||||
PackageType: membership.Plan,
|
||||
ExpiresAt: &expiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
stats := map[string]int64{}
|
||||
var expiryCount int64
|
||||
_ = s.db.WithContext(ctx).Model(&expirymodel.ExpiryItem{}).Where("user_id = ?", user.ID).Count(&expiryCount).Error
|
||||
stats["expiry_items"] = expiryCount
|
||||
|
||||
var watermarkCount int64
|
||||
_ = s.db.WithContext(ctx).Model(&rmmodel.VideoParseLog{}).Where("user_id = ?", user.ID).Count(&watermarkCount).Error
|
||||
stats["watermark_tasks"] = watermarkCount
|
||||
|
||||
var redemptionCount int64
|
||||
_ = s.db.WithContext(ctx).Model(&membershipmodel.MembershipRedemption{}).Where("user_id = ?", user.ID).Count(&redemptionCount).Error
|
||||
stats["membership_redemptions"] = redemptionCount
|
||||
|
||||
return &UserDetail{
|
||||
ID: user.ID,
|
||||
Nickname: user.NickName,
|
||||
AvatarURL: user.AvatarURL,
|
||||
Phone: user.Phone,
|
||||
OpenID: user.OpenID,
|
||||
Gender: user.Gender,
|
||||
MiniProgramID: user.MiniProgramID,
|
||||
MiniProgramName: miniProgram.Name,
|
||||
IsMember: membershipInfo.IsMember,
|
||||
CreatedAt: user.CreatedAt,
|
||||
Membership: membershipInfo,
|
||||
Stats: stats,
|
||||
}, nil
|
||||
}
|
||||
@@ -31,6 +31,14 @@ func registerAdminRoutes(router *gin.Engine, handler *adminhandler.Handler) {
|
||||
protected.POST("/mini-programs", handler.CreateMiniProgram)
|
||||
protected.PUT("/mini-programs/:id", handler.UpdateMiniProgram)
|
||||
protected.DELETE("/mini-programs/:id", handler.DeleteMiniProgram)
|
||||
|
||||
protected.GET("/users", handler.ListUsers)
|
||||
protected.GET("/users/:id", handler.GetUserDetail)
|
||||
|
||||
protected.GET("/memberships/overview", handler.MembershipOverview)
|
||||
protected.GET("/memberships/redeem-codes", handler.ListMembershipRedeemCodes)
|
||||
protected.POST("/memberships/redeem-codes", handler.CreateMembershipRedeemCodes)
|
||||
protected.POST("/memberships/redeem-codes/:id/status", handler.UpdateMembershipRedeemCodeStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user