feat(admin): add watermark table list APIs (#44)

This commit is contained in:
root
2026-03-10 17:02:53 +08:00
parent 6f1f75d983
commit 2ddb2403e4
3 changed files with 562 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
package handler
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
adminservice "wx_service/internal/admin/service"
"wx_service/internal/model"
)
func (h *Handler) ListVideoParseLogs(c *gin.Context) {
page, err := parsePage(c.DefaultQuery("page", "1"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid page"))
return
}
pageSize, err := parsePageSize(c.DefaultQuery("page_size", "20"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid page_size"))
return
}
miniProgramID, err := parseOptionalUint(c.Query("mini_program_id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid mini_program_id"))
return
}
userID, err := parseOptionalUint(c.Query("user_id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid user_id"))
return
}
freeQuotaUsed, err := parseOptionalBool(c.Query("free_quota_used"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid free_quota_used"))
return
}
dateFrom, err := parseOptionalDateBoundary(c.Query("date_from"), false)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid date_from, expected YYYY-MM-DD"))
return
}
dateTo, err := parseOptionalDateBoundary(c.Query("date_to"), true)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid date_to, expected YYYY-MM-DD"))
return
}
data, err := h.svc.ListVideoParseLogs(c.Request.Context(), adminservice.ListVideoParseLogsQuery{
Page: page,
PageSize: pageSize,
MiniProgramID: miniProgramID,
UserID: userID,
FreeQuotaUsed: freeQuotaUsed,
Keyword: c.Query("keyword"),
DateFrom: dateFrom,
DateTo: dateTo,
})
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load video parse logs failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) ListVideoParseUnlocks(c *gin.Context) {
page, err := parsePage(c.DefaultQuery("page", "1"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid page"))
return
}
pageSize, err := parsePageSize(c.DefaultQuery("page_size", "20"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid page_size"))
return
}
miniProgramID, err := parseOptionalUint(c.Query("mini_program_id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid mini_program_id"))
return
}
userID, err := parseOptionalUint(c.Query("user_id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid user_id"))
return
}
dateFrom, err := parseOptionalDateBoundary(c.Query("date_from"), false)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid date_from, expected YYYY-MM-DD"))
return
}
dateTo, err := parseOptionalDateBoundary(c.Query("date_to"), true)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid date_to, expected YYYY-MM-DD"))
return
}
data, err := h.svc.ListVideoParseUnlocks(c.Request.Context(), adminservice.ListVideoParseUnlocksQuery{
Page: page,
PageSize: pageSize,
MiniProgramID: miniProgramID,
UserID: userID,
DateFrom: dateFrom,
DateTo: dateTo,
})
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load video parse unlocks failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) ListVideoDownloadFailures(c *gin.Context) {
page, err := parsePage(c.DefaultQuery("page", "1"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid page"))
return
}
pageSize, err := parsePageSize(c.DefaultQuery("page_size", "20"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid page_size"))
return
}
dateFrom, err := parseOptionalDateBoundary(c.Query("date_from"), false)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid date_from, expected YYYY-MM-DD"))
return
}
dateTo, err := parseOptionalDateBoundary(c.Query("date_to"), true)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid date_to, expected YYYY-MM-DD"))
return
}
data, err := h.svc.ListVideoDownloadFailures(c.Request.Context(), adminservice.ListVideoDownloadFailuresQuery{
Page: page,
PageSize: pageSize,
Domain: c.Query("domain"),
Keyword: c.Query("keyword"),
DateFrom: dateFrom,
DateTo: dateTo,
})
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load video download failures failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func parsePage(raw string) (int, error) {
value, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil || value < 1 {
return 0, fmt.Errorf("invalid page")
}
return value, nil
}
func parsePageSize(raw string) (int, error) {
value, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil || value < 1 {
return 0, fmt.Errorf("invalid page_size")
}
return value, nil
}
func parseOptionalUint(raw string) (uint, error) {
text := strings.TrimSpace(raw)
if text == "" {
return 0, nil
}
value, err := strconv.ParseUint(text, 10, 64)
if err != nil {
return 0, err
}
return uint(value), nil
}
func parseOptionalBool(raw string) (*bool, error) {
text := strings.TrimSpace(strings.ToLower(raw))
if text == "" {
return nil, nil
}
if text == "1" || text == "true" {
v := true
return &v, nil
}
if text == "0" || text == "false" {
v := false
return &v, nil
}
return nil, fmt.Errorf("invalid bool")
}
func parseOptionalDateBoundary(raw string, endOfDay bool) (*time.Time, error) {
text := strings.TrimSpace(raw)
if text == "" {
return nil, nil
}
parsed, err := time.ParseInLocation("2006-01-02", text, time.Local)
if err != nil {
return nil, err
}
if endOfDay {
parsed = parsed.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
}
return &parsed, nil
}
+347
View File
@@ -0,0 +1,347 @@
package service
import (
"context"
"strings"
"time"
"wx_service/internal/model"
rmmodel "wx_service/internal/remove_watermark/model"
)
type ListVideoParseLogsQuery struct {
Page int
PageSize int
MiniProgramID uint
UserID uint
FreeQuotaUsed *bool
Keyword string
DateFrom *time.Time
DateTo *time.Time
}
type VideoParseLogItem struct {
ID uint `json:"id"`
MiniProgramID uint `json:"mini_program_id"`
MiniProgramName string `json:"mini_program_name"`
UserID uint `json:"user_id"`
RequestContent string `json:"request_content"`
ParsedURL string `json:"parsed_url"`
ThirdPartyStatus int `json:"third_party_status"`
FreeQuotaUsed bool `json:"free_quota_used"`
DurationMs int `json:"duration_ms"`
ErrorMessage string `json:"error_message"`
CreatedAt time.Time `json:"created_at"`
ThirdPartyPayload string `json:"third_party_payload"`
}
type ListVideoParseLogsResult struct {
List []VideoParseLogItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type ListVideoParseUnlocksQuery struct {
Page int
PageSize int
MiniProgramID uint
UserID uint
DateFrom *time.Time
DateTo *time.Time
}
type VideoParseUnlockItem struct {
ID uint `json:"id"`
MiniProgramID uint `json:"mini_program_id"`
MiniProgramName string `json:"mini_program_name"`
UserID uint `json:"user_id"`
UnlockDate time.Time `json:"unlock_date"`
AdWatchedAt time.Time `json:"ad_watched_at"`
CreatedAt time.Time `json:"created_at"`
}
type ListVideoParseUnlocksResult struct {
List []VideoParseUnlockItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type ListVideoDownloadFailuresQuery struct {
Page int
PageSize int
Domain string
Keyword string
DateFrom *time.Time
DateTo *time.Time
}
type VideoDownloadFailureItem struct {
ID uint `json:"id"`
Domain string `json:"domain"`
FailedURL string `json:"failed_url"`
ErrorMessage string `json:"error_message"`
ReportedAt time.Time `json:"reported_at"`
UserAgent string `json:"user_agent"`
ClientIP string `json:"client_ip"`
CreatedAt time.Time `json:"created_at"`
}
type ListVideoDownloadFailuresResult struct {
List []VideoDownloadFailureItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func (s *Service) ListVideoParseLogs(ctx context.Context, query ListVideoParseLogsQuery) (*ListVideoParseLogsResult, error) {
query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
query.Keyword = strings.TrimSpace(query.Keyword)
dbQuery := s.db.WithContext(ctx).Model(&rmmodel.VideoParseLog{})
if query.MiniProgramID > 0 {
dbQuery = dbQuery.Where("mini_program_id = ?", query.MiniProgramID)
}
if query.UserID > 0 {
dbQuery = dbQuery.Where("user_id = ?", query.UserID)
}
if query.FreeQuotaUsed != nil {
dbQuery = dbQuery.Where("free_quota_used = ?", *query.FreeQuotaUsed)
}
if query.Keyword != "" {
likeKeyword := "%" + query.Keyword + "%"
dbQuery = dbQuery.Where("request_content LIKE ? OR parsed_url LIKE ? OR error_message LIKE ?", likeKeyword, likeKeyword, likeKeyword)
}
if query.DateFrom != nil {
dbQuery = dbQuery.Where("created_at >= ?", *query.DateFrom)
}
if query.DateTo != nil {
dbQuery = dbQuery.Where("created_at <= ?", *query.DateTo)
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
rows := make([]rmmodel.VideoParseLog, 0)
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
}
}
miniProgramNameMap, err := s.loadMiniProgramNameMap(ctx, extractMiniProgramIDsFromLogs(rows))
if err != nil {
return nil, err
}
items := make([]VideoParseLogItem, 0, len(rows))
for _, row := range rows {
items = append(items, VideoParseLogItem{
ID: row.ID,
MiniProgramID: row.MiniProgramID,
MiniProgramName: miniProgramNameMap[row.MiniProgramID],
UserID: row.UserID,
RequestContent: row.RequestContent,
ParsedURL: row.ParsedURL,
ThirdPartyStatus: row.ThirdPartyStatus,
FreeQuotaUsed: row.FreeQuotaUsed,
DurationMs: row.DurationMs,
ErrorMessage: row.ErrorMessage,
CreatedAt: row.CreatedAt,
ThirdPartyPayload: string(row.ThirdPartyPayload),
})
}
return &ListVideoParseLogsResult{
List: items,
Total: total,
Page: query.Page,
PageSize: query.PageSize,
}, nil
}
func (s *Service) ListVideoParseUnlocks(ctx context.Context, query ListVideoParseUnlocksQuery) (*ListVideoParseUnlocksResult, error) {
query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
dbQuery := s.db.WithContext(ctx).Model(&rmmodel.VideoParseUnlock{})
if query.MiniProgramID > 0 {
dbQuery = dbQuery.Where("mini_program_id = ?", query.MiniProgramID)
}
if query.UserID > 0 {
dbQuery = dbQuery.Where("user_id = ?", query.UserID)
}
if query.DateFrom != nil {
dbQuery = dbQuery.Where("unlock_date >= ?", query.DateFrom.Format("2006-01-02"))
}
if query.DateTo != nil {
dbQuery = dbQuery.Where("unlock_date <= ?", query.DateTo.Format("2006-01-02"))
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
rows := make([]rmmodel.VideoParseUnlock, 0)
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
}
}
miniProgramNameMap, err := s.loadMiniProgramNameMap(ctx, extractMiniProgramIDsFromUnlocks(rows))
if err != nil {
return nil, err
}
items := make([]VideoParseUnlockItem, 0, len(rows))
for _, row := range rows {
items = append(items, VideoParseUnlockItem{
ID: row.ID,
MiniProgramID: row.MiniProgramID,
MiniProgramName: miniProgramNameMap[row.MiniProgramID],
UserID: row.UserID,
UnlockDate: row.UnlockDate,
AdWatchedAt: row.AdWatchedAt,
CreatedAt: row.CreatedAt,
})
}
return &ListVideoParseUnlocksResult{
List: items,
Total: total,
Page: query.Page,
PageSize: query.PageSize,
}, nil
}
func (s *Service) ListVideoDownloadFailures(ctx context.Context, query ListVideoDownloadFailuresQuery) (*ListVideoDownloadFailuresResult, error) {
query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
query.Domain = strings.TrimSpace(query.Domain)
query.Keyword = strings.TrimSpace(query.Keyword)
dbQuery := s.db.WithContext(ctx).Model(&rmmodel.VideoDownloadFailure{})
if query.Domain != "" {
dbQuery = dbQuery.Where("domain = ?", query.Domain)
}
if query.Keyword != "" {
likeKeyword := "%" + query.Keyword + "%"
dbQuery = dbQuery.Where("failed_url LIKE ? OR error_message LIKE ? OR user_agent LIKE ?", likeKeyword, likeKeyword, likeKeyword)
}
if query.DateFrom != nil {
dbQuery = dbQuery.Where("reported_at >= ?", *query.DateFrom)
}
if query.DateTo != nil {
dbQuery = dbQuery.Where("reported_at <= ?", *query.DateTo)
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
rows := make([]rmmodel.VideoDownloadFailure, 0)
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
}
}
items := make([]VideoDownloadFailureItem, 0, len(rows))
for _, row := range rows {
items = append(items, VideoDownloadFailureItem{
ID: row.ID,
Domain: row.Domain,
FailedURL: row.FailedURL,
ErrorMessage: row.ErrorMessage,
ReportedAt: row.ReportedAt,
UserAgent: row.UserAgent,
ClientIP: row.ClientIP,
CreatedAt: row.CreatedAt,
})
}
return &ListVideoDownloadFailuresResult{
List: items,
Total: total,
Page: query.Page,
PageSize: query.PageSize,
}, nil
}
func (s *Service) loadMiniProgramNameMap(ctx context.Context, ids []uint) (map[uint]string, error) {
nameMap := map[uint]string{}
if len(ids) == 0 {
return nameMap, nil
}
rows := make([]model.MiniProgram, 0)
if err := s.db.WithContext(ctx).
Select("id", "name").
Where("id IN ?", ids).
Find(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
nameMap[row.ID] = row.Name
}
return nameMap, nil
}
func extractMiniProgramIDsFromLogs(rows []rmmodel.VideoParseLog) []uint {
set := map[uint]struct{}{}
ids := make([]uint, 0, len(rows))
for _, row := range rows {
if row.MiniProgramID == 0 {
continue
}
if _, ok := set[row.MiniProgramID]; ok {
continue
}
set[row.MiniProgramID] = struct{}{}
ids = append(ids, row.MiniProgramID)
}
return ids
}
func extractMiniProgramIDsFromUnlocks(rows []rmmodel.VideoParseUnlock) []uint {
set := map[uint]struct{}{}
ids := make([]uint, 0, len(rows))
for _, row := range rows {
if row.MiniProgramID == 0 {
continue
}
if _, ok := set[row.MiniProgramID]; ok {
continue
}
set[row.MiniProgramID] = struct{}{}
ids = append(ids, row.MiniProgramID)
}
return ids
}
func normalizePage(page, pageSize int) (int, int) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
return page, pageSize
}
+4
View File
@@ -48,6 +48,10 @@ func registerAdminRoutes(
protected.PUT("/settings/system", handler.UpdateSystemConfig)
protected.PUT("/settings/password", handler.UpdatePassword)
protected.GET("/watermark/video-parse-logs", handler.ListVideoParseLogs)
protected.GET("/watermark/video-parse-unlocks", handler.ListVideoParseUnlocks)
protected.GET("/watermark/video-download-failures", handler.ListVideoDownloadFailures)
protected.GET("/memberships/overview", handler.MembershipOverview)
protected.GET("/memberships/redeem-codes", handler.ListMembershipRedeemCodes)
protected.POST("/memberships/redeem-codes", handler.CreateMembershipRedeemCodes)