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:
@@ -0,0 +1,90 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wx_service/internal/marketing/model"
|
||||
)
|
||||
|
||||
var ErrCategoryNotFound = errors.New("marketing category not found")
|
||||
|
||||
type CategoryRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCategoryRepository(db *gorm.DB) *CategoryRepository {
|
||||
return &CategoryRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *CategoryRepository) Create(cat *model.MarketingCategory) error {
|
||||
if err := r.db.Create(cat).Error; err != nil {
|
||||
return fmt.Errorf("create marketing category: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CategoryRepository) Update(cat *model.MarketingCategory) error {
|
||||
tx := r.db.Model(cat).Updates(map[string]interface{}{
|
||||
"name": cat.Name,
|
||||
"sort_order": cat.SortOrder,
|
||||
"icon": cat.Icon,
|
||||
"status": cat.Status,
|
||||
})
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("update marketing category: %w", tx.Error)
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
return ErrCategoryNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CategoryRepository) Delete(id uint) error {
|
||||
tx := r.db.Delete(&model.MarketingCategory{}, id)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("delete marketing category: %w", tx.Error)
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
return ErrCategoryNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CategoryRepository) FindByID(id uint) (*model.MarketingCategory, error) {
|
||||
var cat model.MarketingCategory
|
||||
err := r.db.First(&cat, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("find marketing category: %w", err)
|
||||
}
|
||||
return &cat, nil
|
||||
}
|
||||
|
||||
// FindAll returns all categories. If onlyEnabled is true, only status=1 records are returned.
|
||||
func (r *CategoryRepository) FindAll(onlyEnabled bool) ([]model.MarketingCategory, error) {
|
||||
query := r.db.Model(&model.MarketingCategory{})
|
||||
if onlyEnabled {
|
||||
query = query.Where("status = ?", 1)
|
||||
}
|
||||
query = query.Order("sort_order DESC, id ASC")
|
||||
|
||||
var categories []model.MarketingCategory
|
||||
if err := query.Find(&categories).Error; err != nil {
|
||||
return nil, fmt.Errorf("list marketing categories: %w", err)
|
||||
}
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
func (r *CategoryRepository) HasTemplates(categoryID uint) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.MarketingTemplate{}).Where("category_id = ?", categoryID).Count(&count).Error
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("count templates by category: %w", err)
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wx_service/internal/marketing/model"
|
||||
)
|
||||
|
||||
var ErrDownloadNotFound = errors.New("marketing download not found")
|
||||
|
||||
type DownloadRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewDownloadRepository(db *gorm.DB) *DownloadRepository {
|
||||
return &DownloadRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *DownloadRepository) Create(dl *model.MarketingDownload) error {
|
||||
if err := r.db.Create(dl).Error; err != nil {
|
||||
return fmt.Errorf("create marketing download: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DownloadRepository) FindByID(id uint) (*model.MarketingDownload, error) {
|
||||
var dl model.MarketingDownload
|
||||
err := r.db.First(&dl, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrDownloadNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("find marketing download: %w", err)
|
||||
}
|
||||
return &dl, nil
|
||||
}
|
||||
|
||||
func (r *DownloadRepository) MarkAdCompleted(id uint) error {
|
||||
tx := r.db.Model(&model.MarketingDownload{}).Where("id = ?", id).Update("ad_completed", true)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("mark ad completed: %w", tx.Error)
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
return ErrDownloadNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DownloadRepository) FindByUser(userID uint, page, pageSize int) ([]model.MarketingDownload, int64, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
query := r.db.Model(&model.MarketingDownload{}).Where("user_id = ?", userID)
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("count marketing downloads: %w", err)
|
||||
}
|
||||
|
||||
var downloads []model.MarketingDownload
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Preload("Template").Order("id DESC").Offset(offset).Limit(pageSize).Find(&downloads).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("list marketing downloads: %w", err)
|
||||
}
|
||||
|
||||
return downloads, total, nil
|
||||
}
|
||||
|
||||
type DownloadStats struct {
|
||||
TotalDownloads int64
|
||||
TodayDownloads int64
|
||||
}
|
||||
|
||||
func (r *DownloadRepository) GetStats() (*DownloadStats, error) {
|
||||
var total int64
|
||||
if err := r.db.Model(&model.MarketingDownload{}).Count(&total).Error; err != nil {
|
||||
return nil, fmt.Errorf("count total downloads: %w", err)
|
||||
}
|
||||
|
||||
var today int64
|
||||
if err := r.db.Model(&model.MarketingDownload{}).
|
||||
Where("DATE(created_at) = CURDATE()").
|
||||
Count(&today).Error; err != nil {
|
||||
return nil, fmt.Errorf("count today downloads: %w", err)
|
||||
}
|
||||
|
||||
return &DownloadStats{TotalDownloads: total, TodayDownloads: today}, nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wx_service/internal/marketing/model"
|
||||
)
|
||||
|
||||
var ErrTemplateNotFound = errors.New("marketing template not found")
|
||||
|
||||
type TemplateRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewTemplateRepository(db *gorm.DB) *TemplateRepository {
|
||||
return &TemplateRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *TemplateRepository) Create(tpl *model.MarketingTemplate) error {
|
||||
if err := r.db.Create(tpl).Error; err != nil {
|
||||
return fmt.Errorf("create marketing template: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TemplateRepository) Update(tpl *model.MarketingTemplate) error {
|
||||
tx := r.db.Model(tpl).Updates(map[string]interface{}{
|
||||
"category_id": tpl.CategoryID,
|
||||
"title": tpl.Title,
|
||||
"image_url": tpl.ImageURL,
|
||||
"thumbnail_url": tpl.ThumbnailURL,
|
||||
"width": tpl.Width,
|
||||
"height": tpl.Height,
|
||||
"sort_order": tpl.SortOrder,
|
||||
"status": tpl.Status,
|
||||
})
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("update marketing template: %w", tx.Error)
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
return ErrTemplateNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TemplateRepository) Delete(id uint) error {
|
||||
tx := r.db.Delete(&model.MarketingTemplate{}, id)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("delete marketing template: %w", tx.Error)
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
return ErrTemplateNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TemplateRepository) FindByID(id uint) (*model.MarketingTemplate, error) {
|
||||
var tpl model.MarketingTemplate
|
||||
err := r.db.Preload("Category").First(&tpl, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrTemplateNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("find marketing template: %w", err)
|
||||
}
|
||||
return &tpl, nil
|
||||
}
|
||||
|
||||
type TemplateListParams struct {
|
||||
CategoryID uint
|
||||
OnlyEnabled bool
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
func (r *TemplateRepository) FindList(params TemplateListParams) ([]model.MarketingTemplate, int64, error) {
|
||||
if params.Page <= 0 {
|
||||
params.Page = 1
|
||||
}
|
||||
if params.PageSize <= 0 {
|
||||
params.PageSize = 20
|
||||
}
|
||||
if params.PageSize > 100 {
|
||||
params.PageSize = 100
|
||||
}
|
||||
|
||||
query := r.db.Model(&model.MarketingTemplate{})
|
||||
if params.CategoryID > 0 {
|
||||
query = query.Where("category_id = ?", params.CategoryID)
|
||||
}
|
||||
if params.OnlyEnabled {
|
||||
query = query.Where("status = ?", 1)
|
||||
}
|
||||
query = query.Order("sort_order DESC, id DESC")
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("count marketing templates: %w", err)
|
||||
}
|
||||
|
||||
var templates []model.MarketingTemplate
|
||||
offset := (params.Page - 1) * params.PageSize
|
||||
if err := query.Preload("Category").Offset(offset).Limit(params.PageSize).Find(&templates).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("list marketing templates: %w", err)
|
||||
}
|
||||
|
||||
return templates, total, nil
|
||||
}
|
||||
|
||||
func (r *TemplateRepository) IncrementDownloadCount(id uint) error {
|
||||
tx := r.db.Model(&model.MarketingTemplate{}).Where("id = ?", id).
|
||||
UpdateColumn("download_count", gorm.Expr("download_count + 1"))
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("increment download count: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user