feat: 完成后台Issue#9 小程序管理接口模块
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"wx_service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) ListMiniPrograms(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.ListMiniPrograms(c.Request.Context(), ListMiniProgramsQuery{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
Keyword: c.Query("keyword"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load mini-programs failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Success(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetMiniProgram(c *gin.Context) {
|
||||||
|
id, err := parseUintID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid mini-program id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.svc.GetMiniProgram(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrMiniProgramNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "mini program not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load mini-program failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Success(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetMiniProgramStats(c *gin.Context) {
|
||||||
|
id, err := parseUintID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid mini-program id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.svc.GetMiniProgramStats(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrMiniProgramNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "mini program not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load mini-program stats failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Success(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
type createMiniProgramRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
AppID string `json:"app_id" binding:"required"`
|
||||||
|
AppSecret string `json:"app_secret" binding:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CreateMiniProgram(c *gin.Context) {
|
||||||
|
var req createMiniProgramRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.svc.CreateMiniProgram(c.Request.Context(), CreateMiniProgramInput{
|
||||||
|
Name: req.Name,
|
||||||
|
AppID: req.AppID,
|
||||||
|
AppSecret: req.AppSecret,
|
||||||
|
Description: req.Description,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrInvalidInput):
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "name/app_id/app_secret are required"))
|
||||||
|
case errors.Is(err, ErrMiniProgramAppIDUsed):
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "app_id already exists"))
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "create mini-program failed"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Success(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateMiniProgramRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
AppID string `json:"app_id" binding:"required"`
|
||||||
|
AppSecret *string `json:"app_secret"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UpdateMiniProgram(c *gin.Context) {
|
||||||
|
id, err := parseUintID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid mini-program id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req updateMiniProgramRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.svc.UpdateMiniProgram(c.Request.Context(), id, UpdateMiniProgramInput{
|
||||||
|
Name: req.Name,
|
||||||
|
AppID: req.AppID,
|
||||||
|
AppSecret: req.AppSecret,
|
||||||
|
Description: req.Description,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrMiniProgramNotFound):
|
||||||
|
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "mini program not found"))
|
||||||
|
case errors.Is(err, ErrInvalidInput):
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "name/app_id are required"))
|
||||||
|
case errors.Is(err, ErrMiniProgramAppIDUsed):
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "app_id already exists"))
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "update mini-program failed"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Success(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) DeleteMiniProgram(c *gin.Context) {
|
||||||
|
id, err := parseUintID(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid mini-program id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.svc.DeleteMiniProgram(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrMiniProgramNotFound):
|
||||||
|
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "mini program not found"))
|
||||||
|
case errors.Is(err, ErrMiniProgramHasUsers):
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "mini program has related users"))
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "delete mini-program failed"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Success(gin.H{
|
||||||
|
"message": "删除成功",
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -24,6 +24,13 @@ func registerAdminRoutes(router *gin.Engine, handler *adminhandler.Handler) {
|
|||||||
protected.GET("/stats/overview", handler.StatsOverview)
|
protected.GET("/stats/overview", handler.StatsOverview)
|
||||||
protected.GET("/stats/mini-programs", handler.StatsMiniPrograms)
|
protected.GET("/stats/mini-programs", handler.StatsMiniPrograms)
|
||||||
protected.GET("/stats/user-growth", handler.StatsUserGrowth)
|
protected.GET("/stats/user-growth", handler.StatsUserGrowth)
|
||||||
|
|
||||||
|
protected.GET("/mini-programs", handler.ListMiniPrograms)
|
||||||
|
protected.GET("/mini-programs/:id", handler.GetMiniProgram)
|
||||||
|
protected.GET("/mini-programs/:id/stats", handler.GetMiniProgramStats)
|
||||||
|
protected.POST("/mini-programs", handler.CreateMiniProgram)
|
||||||
|
protected.PUT("/mini-programs/:id", handler.UpdateMiniProgram)
|
||||||
|
protected.DELETE("/mini-programs/:id", handler.DeleteMiniProgram)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user