feat(marketing): 新增营销图管理模块

- 新增 marketing 模块:model/repository/service/handler 四层架构
- 数据模型:marketing_categories、marketing_templates、marketing_user_downloads
- 小程序端接口:分类列表、模板列表/详情、下载记录、广告回调
- 管理后台接口:分类/模板 CRUD、下载统计(X-Admin-Token 鉴权)
- 路由注册:接入现有 AuthMiddleware,新增 AdminTokenMiddleware
- Web 管理后台:单页面 Vue3 + Element Plus(分类管理、模板管理、数据概览)

Closes #37, #38, #39, #40

Made-with: Cursor
This commit is contained in:
nepiedg
2026-03-06 07:36:05 +00:00
parent 5f492929df
commit ac49e1458c
17 changed files with 1599 additions and 1 deletions
@@ -0,0 +1,103 @@
package service
import (
"errors"
"wx_service/internal/marketing/model"
"wx_service/internal/marketing/repository"
)
var (
ErrDownloadTemplateRequired = errors.New("请选择模板")
)
type DownloadService struct {
downloadRepo *repository.DownloadRepository
templateRepo *repository.TemplateRepository
}
func NewDownloadService(downloadRepo *repository.DownloadRepository, templateRepo *repository.TemplateRepository) *DownloadService {
return &DownloadService{
downloadRepo: downloadRepo,
templateRepo: templateRepo,
}
}
type CreateDownloadRequest struct {
TemplateID uint `json:"template_id"`
LogoURL string `json:"logo_url"`
LogoX float64 `json:"logo_x"`
LogoY float64 `json:"logo_y"`
LogoW float64 `json:"logo_w"`
LogoH float64 `json:"logo_h"`
}
type DownloadListResponse struct {
Downloads []model.MarketingDownload `json:"downloads"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func (s *DownloadService) Create(userID uint, req CreateDownloadRequest) (*model.MarketingDownload, error) {
if req.TemplateID == 0 {
return nil, ErrDownloadTemplateRequired
}
dl := &model.MarketingDownload{
UserID: userID,
TemplateID: req.TemplateID,
LogoURL: req.LogoURL,
LogoX: req.LogoX,
LogoY: req.LogoY,
LogoW: req.LogoW,
LogoH: req.LogoH,
}
if err := s.downloadRepo.Create(dl); err != nil {
return nil, err
}
_ = s.templateRepo.IncrementDownloadCount(req.TemplateID)
return dl, nil
}
func (s *DownloadService) MarkAdCompleted(downloadID uint) error {
return s.downloadRepo.MarkAdCompleted(downloadID)
}
func (s *DownloadService) ListByUser(userID uint, page, pageSize int) (*DownloadListResponse, error) {
downloads, total, err := s.downloadRepo.FindByUser(userID, page, pageSize)
if err != nil {
return nil, err
}
return &DownloadListResponse{
Downloads: downloads,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
func (s *DownloadService) GetStats() (*repository.DownloadStats, error) {
return s.downloadRepo.GetStats()
}
// IsBadRequestError checks if the error is a client-side validation error.
func IsBadRequestError(err error) bool {
return errors.Is(err, ErrDownloadTemplateRequired) ||
errors.Is(err, ErrCategoryNameRequired) ||
errors.Is(err, ErrCategoryNameTooLong) ||
errors.Is(err, ErrCategoryHasTemplates) ||
errors.Is(err, ErrTemplateTitleRequired) ||
errors.Is(err, ErrTemplateTitleTooLong) ||
errors.Is(err, ErrTemplateImageRequired) ||
errors.Is(err, ErrTemplateCategoryRequired)
}
// IsNotFoundError checks if the error is a not-found error.
func IsNotFoundError(err error) bool {
return errors.Is(err, repository.ErrCategoryNotFound) ||
errors.Is(err, repository.ErrTemplateNotFound) ||
errors.Is(err, repository.ErrDownloadNotFound)
}