feat(marketing): add user logo management module

Users can now save uploaded logos to the backend (marketing_user_logos table),
avoiding repeated uploads. Includes CRUD endpoints: list, save, delete with
a per-user limit of 10 logos.

Made-with: Cursor
This commit is contained in:
nepiedg
2026-04-04 03:46:57 +08:00
parent 1eab1b99c1
commit b4170b4863
7 changed files with 223 additions and 1 deletions
+5
View File
@@ -82,6 +82,7 @@ func main() {
&marketingmodel.MarketingCategory{},
&marketingmodel.MarketingTemplate{},
&marketingmodel.MarketingDownload{},
&marketingmodel.UserLogo{},
&quitcheckinmodel.Profile{},
&quitcheckinmodel.DailyStatus{},
&quitcheckinmodel.RelapseEvent{},
@@ -156,12 +157,15 @@ func main() {
categoryRepo := marketingrepo.NewCategoryRepository(database.DB)
templateRepo := marketingrepo.NewTemplateRepository(database.DB)
downloadRepo := marketingrepo.NewDownloadRepository(database.DB)
userLogoRepo := marketingrepo.NewUserLogoRepository(database.DB)
categorySvc := marketingservice.NewCategoryService(categoryRepo)
templateSvc := marketingservice.NewTemplateService(templateRepo)
downloadSvc := marketingservice.NewDownloadService(downloadRepo, templateRepo)
userLogoSvc := marketingservice.NewUserLogoService(userLogoRepo)
marketingCategoryHandler := marketinghandler.NewCategoryHandler(categorySvc)
marketingTemplateHandler := marketinghandler.NewTemplateHandler(templateSvc)
marketingDownloadHandler := marketinghandler.NewDownloadHandler(downloadSvc)
marketingUserLogoHandler := marketinghandler.NewUserLogoHandler(userLogoSvc)
adminService := adminmodule.NewService(
database.DB,
@@ -194,6 +198,7 @@ func main() {
marketingCategoryHandler,
marketingTemplateHandler,
marketingDownloadHandler,
marketingUserLogoHandler,
quitCheckinHandler,
)
@@ -0,0 +1,73 @@
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"wx_service/internal/marketing/repository"
"wx_service/internal/marketing/service"
"wx_service/internal/middleware"
"wx_service/internal/model"
)
type UserLogoHandler struct {
svc *service.UserLogoService
}
func NewUserLogoHandler(svc *service.UserLogoService) *UserLogoHandler {
return &UserLogoHandler{svc: svc}
}
func (h *UserLogoHandler) List(c *gin.Context) {
user := middleware.MustCurrentUser(c)
logos, err := h.svc.List(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取 Logo 列表失败"))
return
}
c.JSON(http.StatusOK, model.Success(logos))
}
func (h *UserLogoHandler) Save(c *gin.Context) {
user := middleware.MustCurrentUser(c)
var req service.SaveLogoRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
return
}
logo, err := h.svc.Save(user.ID, req)
if err != nil {
if errors.Is(err, service.ErrLogoLimitReached) {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "保存 Logo 失败"))
return
}
c.JSON(http.StatusOK, model.Success(logo))
}
func (h *UserLogoHandler) Delete(c *gin.Context) {
user := middleware.MustCurrentUser(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "无效的 ID"))
return
}
if err := h.svc.Delete(uint(id), user.ID); err != nil {
if errors.Is(err, repository.ErrUserLogoNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "Logo 不存在"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "删除失败"))
return
}
c.JSON(http.StatusOK, model.Success(nil))
}
+16
View File
@@ -0,0 +1,16 @@
package model
import "time"
type UserLogo struct {
ID uint `json:"id" gorm:"primaryKey;comment:主键ID"`
UserID uint `json:"user_id" gorm:"not null;index;comment:用户ID"`
URL string `json:"url" gorm:"size:500;not null;comment:Logo CDN地址"`
Filename string `json:"filename" gorm:"size:255;comment:原始文件名"`
FileSize int64 `json:"file_size" gorm:"comment:文件大小(字节)"`
CreatedAt time.Time `json:"created_at" gorm:"comment:创建时间"`
}
func (UserLogo) TableName() string {
return "marketing_user_logos"
}
@@ -0,0 +1,65 @@
package repository
import (
"errors"
"fmt"
"gorm.io/gorm"
"wx_service/internal/marketing/model"
)
var ErrUserLogoNotFound = errors.New("user logo not found")
type UserLogoRepository struct {
db *gorm.DB
}
func NewUserLogoRepository(db *gorm.DB) *UserLogoRepository {
return &UserLogoRepository{db: db}
}
func (r *UserLogoRepository) Create(logo *model.UserLogo) error {
if err := r.db.Create(logo).Error; err != nil {
return fmt.Errorf("create user logo: %w", err)
}
return nil
}
func (r *UserLogoRepository) FindByUser(userID uint) ([]model.UserLogo, error) {
var logos []model.UserLogo
err := r.db.Where("user_id = ?", userID).Order("id DESC").Find(&logos).Error
if err != nil {
return nil, fmt.Errorf("list user logos: %w", err)
}
return logos, nil
}
func (r *UserLogoRepository) FindByID(id, userID uint) (*model.UserLogo, error) {
var logo model.UserLogo
err := r.db.Where("id = ? AND user_id = ?", id, userID).First(&logo).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserLogoNotFound
}
return nil, fmt.Errorf("find user logo: %w", err)
}
return &logo, nil
}
func (r *UserLogoRepository) Delete(id, userID uint) error {
tx := r.db.Where("id = ? AND user_id = ?", id, userID).Delete(&model.UserLogo{})
if tx.Error != nil {
return fmt.Errorf("delete user logo: %w", tx.Error)
}
if tx.RowsAffected == 0 {
return ErrUserLogoNotFound
}
return nil
}
func (r *UserLogoRepository) CountByUser(userID uint) (int64, error) {
var count int64
err := r.db.Model(&model.UserLogo{}).Where("user_id = ?", userID).Count(&count).Error
return count, err
}
@@ -0,0 +1,55 @@
package service
import (
"errors"
"wx_service/internal/marketing/model"
"wx_service/internal/marketing/repository"
)
const MaxLogosPerUser = 10
var ErrLogoLimitReached = errors.New("Logo 数量已达上限")
type UserLogoService struct {
repo *repository.UserLogoRepository
}
func NewUserLogoService(repo *repository.UserLogoRepository) *UserLogoService {
return &UserLogoService{repo: repo}
}
type SaveLogoRequest struct {
URL string `json:"url" binding:"required"`
Filename string `json:"filename"`
FileSize int64 `json:"file_size"`
}
func (s *UserLogoService) Save(userID uint, req SaveLogoRequest) (*model.UserLogo, error) {
count, err := s.repo.CountByUser(userID)
if err != nil {
return nil, err
}
if count >= MaxLogosPerUser {
return nil, ErrLogoLimitReached
}
logo := &model.UserLogo{
UserID: userID,
URL: req.URL,
Filename: req.Filename,
FileSize: req.FileSize,
}
if err := s.repo.Create(logo); err != nil {
return nil, err
}
return logo, nil
}
func (s *UserLogoService) List(userID uint) ([]model.UserLogo, error) {
return s.repo.FindByUser(userID)
}
func (s *UserLogoService) Delete(id, userID uint) error {
return s.repo.Delete(id, userID)
}
+7
View File
@@ -13,6 +13,7 @@ func registerMarketingRoutes(
categoryHandler *marketinghandler.CategoryHandler,
templateHandler *marketinghandler.TemplateHandler,
downloadHandler *marketinghandler.DownloadHandler,
userLogoHandler *marketinghandler.UserLogoHandler,
) {
if categoryHandler == nil || templateHandler == nil || downloadHandler == nil {
return
@@ -30,6 +31,12 @@ func registerMarketingRoutes(
protectedMarketing.POST("/downloads", downloadHandler.Create)
protectedMarketing.POST("/ad_callback", downloadHandler.AdCallback)
protectedMarketing.GET("/downloads", downloadHandler.ListByUser)
if userLogoHandler != nil {
protectedMarketing.GET("/logos", userLogoHandler.List)
protectedMarketing.POST("/logos", userLogoHandler.Save)
protectedMarketing.DELETE("/logos/:id", userLogoHandler.Delete)
}
}
admin := api.Group("/admin/marketing")
+2 -1
View File
@@ -39,6 +39,7 @@ func Register(
marketingCategoryHandler *marketinghandler.CategoryHandler,
marketingTemplateHandler *marketinghandler.TemplateHandler,
marketingDownloadHandler *marketinghandler.DownloadHandler,
marketingUserLogoHandler *marketinghandler.UserLogoHandler,
quitCheckinHandler *quitcheckinhandler.Handler,
) {
// Register 用来集中注册所有 HTTP 路由,便于工程结构更清晰:
@@ -70,7 +71,7 @@ func Register(
registerSmokeRoutes(protected, smokeHandler, quitPlanHandler)
}
registerMarketingRoutes(api, protected, adminToken, marketingCategoryHandler, marketingTemplateHandler, marketingDownloadHandler)
registerMarketingRoutes(api, protected, adminToken, marketingCategoryHandler, marketingTemplateHandler, marketingDownloadHandler, marketingUserLogoHandler)
}
apiV2 := router.Group("/api/v2")