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.MarketingCategory{},
|
||||||
&marketingmodel.MarketingTemplate{},
|
&marketingmodel.MarketingTemplate{},
|
||||||
&marketingmodel.MarketingDownload{},
|
&marketingmodel.MarketingDownload{},
|
||||||
|
&marketingmodel.UserLogo{},
|
||||||
&quitcheckinmodel.Profile{},
|
&quitcheckinmodel.Profile{},
|
||||||
&quitcheckinmodel.DailyStatus{},
|
&quitcheckinmodel.DailyStatus{},
|
||||||
&quitcheckinmodel.RelapseEvent{},
|
&quitcheckinmodel.RelapseEvent{},
|
||||||
@@ -156,12 +157,15 @@ func main() {
|
|||||||
categoryRepo := marketingrepo.NewCategoryRepository(database.DB)
|
categoryRepo := marketingrepo.NewCategoryRepository(database.DB)
|
||||||
templateRepo := marketingrepo.NewTemplateRepository(database.DB)
|
templateRepo := marketingrepo.NewTemplateRepository(database.DB)
|
||||||
downloadRepo := marketingrepo.NewDownloadRepository(database.DB)
|
downloadRepo := marketingrepo.NewDownloadRepository(database.DB)
|
||||||
|
userLogoRepo := marketingrepo.NewUserLogoRepository(database.DB)
|
||||||
categorySvc := marketingservice.NewCategoryService(categoryRepo)
|
categorySvc := marketingservice.NewCategoryService(categoryRepo)
|
||||||
templateSvc := marketingservice.NewTemplateService(templateRepo)
|
templateSvc := marketingservice.NewTemplateService(templateRepo)
|
||||||
downloadSvc := marketingservice.NewDownloadService(downloadRepo, templateRepo)
|
downloadSvc := marketingservice.NewDownloadService(downloadRepo, templateRepo)
|
||||||
|
userLogoSvc := marketingservice.NewUserLogoService(userLogoRepo)
|
||||||
marketingCategoryHandler := marketinghandler.NewCategoryHandler(categorySvc)
|
marketingCategoryHandler := marketinghandler.NewCategoryHandler(categorySvc)
|
||||||
marketingTemplateHandler := marketinghandler.NewTemplateHandler(templateSvc)
|
marketingTemplateHandler := marketinghandler.NewTemplateHandler(templateSvc)
|
||||||
marketingDownloadHandler := marketinghandler.NewDownloadHandler(downloadSvc)
|
marketingDownloadHandler := marketinghandler.NewDownloadHandler(downloadSvc)
|
||||||
|
marketingUserLogoHandler := marketinghandler.NewUserLogoHandler(userLogoSvc)
|
||||||
|
|
||||||
adminService := adminmodule.NewService(
|
adminService := adminmodule.NewService(
|
||||||
database.DB,
|
database.DB,
|
||||||
@@ -194,6 +198,7 @@ func main() {
|
|||||||
marketingCategoryHandler,
|
marketingCategoryHandler,
|
||||||
marketingTemplateHandler,
|
marketingTemplateHandler,
|
||||||
marketingDownloadHandler,
|
marketingDownloadHandler,
|
||||||
|
marketingUserLogoHandler,
|
||||||
quitCheckinHandler,
|
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,
|
categoryHandler *marketinghandler.CategoryHandler,
|
||||||
templateHandler *marketinghandler.TemplateHandler,
|
templateHandler *marketinghandler.TemplateHandler,
|
||||||
downloadHandler *marketinghandler.DownloadHandler,
|
downloadHandler *marketinghandler.DownloadHandler,
|
||||||
|
userLogoHandler *marketinghandler.UserLogoHandler,
|
||||||
) {
|
) {
|
||||||
if categoryHandler == nil || templateHandler == nil || downloadHandler == nil {
|
if categoryHandler == nil || templateHandler == nil || downloadHandler == nil {
|
||||||
return
|
return
|
||||||
@@ -30,6 +31,12 @@ func registerMarketingRoutes(
|
|||||||
protectedMarketing.POST("/downloads", downloadHandler.Create)
|
protectedMarketing.POST("/downloads", downloadHandler.Create)
|
||||||
protectedMarketing.POST("/ad_callback", downloadHandler.AdCallback)
|
protectedMarketing.POST("/ad_callback", downloadHandler.AdCallback)
|
||||||
protectedMarketing.GET("/downloads", downloadHandler.ListByUser)
|
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")
|
admin := api.Group("/admin/marketing")
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ func Register(
|
|||||||
marketingCategoryHandler *marketinghandler.CategoryHandler,
|
marketingCategoryHandler *marketinghandler.CategoryHandler,
|
||||||
marketingTemplateHandler *marketinghandler.TemplateHandler,
|
marketingTemplateHandler *marketinghandler.TemplateHandler,
|
||||||
marketingDownloadHandler *marketinghandler.DownloadHandler,
|
marketingDownloadHandler *marketinghandler.DownloadHandler,
|
||||||
|
marketingUserLogoHandler *marketinghandler.UserLogoHandler,
|
||||||
quitCheckinHandler *quitcheckinhandler.Handler,
|
quitCheckinHandler *quitcheckinhandler.Handler,
|
||||||
) {
|
) {
|
||||||
// Register 用来集中注册所有 HTTP 路由,便于工程结构更清晰:
|
// Register 用来集中注册所有 HTTP 路由,便于工程结构更清晰:
|
||||||
@@ -70,7 +71,7 @@ func Register(
|
|||||||
registerSmokeRoutes(protected, smokeHandler, quitPlanHandler)
|
registerSmokeRoutes(protected, smokeHandler, quitPlanHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMarketingRoutes(api, protected, adminToken, marketingCategoryHandler, marketingTemplateHandler, marketingDownloadHandler)
|
registerMarketingRoutes(api, protected, adminToken, marketingCategoryHandler, marketingTemplateHandler, marketingDownloadHandler, marketingUserLogoHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiV2 := router.Group("/api/v2")
|
apiV2 := router.Group("/api/v2")
|
||||||
|
|||||||
Reference in New Issue
Block a user