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:
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user