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,26 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"wx_service/internal/model"
|
||||
)
|
||||
|
||||
func AdminTokenMiddleware(adminToken string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if adminToken == "" {
|
||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "未配置管理员口令"))
|
||||
return
|
||||
}
|
||||
|
||||
token := c.GetHeader("X-Admin-Token")
|
||||
if token == "" || token != adminToken {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "无权限"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"wx_service/internal/marketing/service"
|
||||
"wx_service/internal/model"
|
||||
)
|
||||
|
||||
type CategoryHandler struct {
|
||||
svc *service.CategoryService
|
||||
}
|
||||
|
||||
func NewCategoryHandler(svc *service.CategoryService) *CategoryHandler {
|
||||
return &CategoryHandler{svc: svc}
|
||||
}
|
||||
|
||||
// ListEnabled returns enabled categories for mini-program users.
|
||||
func (h *CategoryHandler) ListEnabled(c *gin.Context) {
|
||||
categories, err := h.svc.List(true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Success(categories))
|
||||
}
|
||||
|
||||
// AdminList returns all categories (including disabled) for admin.
|
||||
func (h *CategoryHandler) AdminList(c *gin.Context) {
|
||||
categories, err := h.svc.List(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Success(categories))
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) AdminCreate(c *gin.Context) {
|
||||
var req service.CategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
cat, err := h.svc.Create(req)
|
||||
if err != nil {
|
||||
if service.IsBadRequestError(err) {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(cat))
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) AdminUpdate(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req service.CategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
cat, err := h.svc.Update(id, req)
|
||||
if err != nil {
|
||||
if service.IsNotFoundError(err) {
|
||||
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "分类不存在"))
|
||||
return
|
||||
}
|
||||
if service.IsBadRequestError(err) {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(cat))
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) AdminDelete(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.Delete(id); err != nil {
|
||||
if service.IsNotFoundError(err) {
|
||||
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "分类不存在"))
|
||||
return
|
||||
}
|
||||
if service.IsBadRequestError(err) {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(nil))
|
||||
}
|
||||
|
||||
func parseID(c *gin.Context) (uint, bool) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil || id <= 0 {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "id 参数错误"))
|
||||
return 0, false
|
||||
}
|
||||
return uint(id), true
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"wx_service/internal/marketing/service"
|
||||
"wx_service/internal/middleware"
|
||||
"wx_service/internal/model"
|
||||
)
|
||||
|
||||
type DownloadHandler struct {
|
||||
svc *service.DownloadService
|
||||
}
|
||||
|
||||
func NewDownloadHandler(svc *service.DownloadService) *DownloadHandler {
|
||||
return &DownloadHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *DownloadHandler) Create(c *gin.Context) {
|
||||
user := middleware.MustCurrentUser(c)
|
||||
|
||||
var req service.CreateDownloadRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
dl, err := h.svc.Create(user.ID, req)
|
||||
if err != nil {
|
||||
if service.IsBadRequestError(err) {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(dl))
|
||||
}
|
||||
|
||||
type adCallbackRequest struct {
|
||||
DownloadID uint `json:"download_id" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *DownloadHandler) AdCallback(c *gin.Context) {
|
||||
var req adCallbackRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.MarkAdCompleted(req.DownloadID); err != nil {
|
||||
if service.IsNotFoundError(err) {
|
||||
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "记录不存在"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(nil))
|
||||
}
|
||||
|
||||
func (h *DownloadHandler) ListByUser(c *gin.Context) {
|
||||
user := middleware.MustCurrentUser(c)
|
||||
page := parseIntQuery(c, "page", 1)
|
||||
pageSize := parseIntQuery(c, "page_size", 20)
|
||||
|
||||
resp, err := h.svc.ListByUser(user.ID, page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(resp))
|
||||
}
|
||||
|
||||
func (h *DownloadHandler) AdminStats(c *gin.Context) {
|
||||
stats, err := h.svc.GetStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Success(stats))
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"wx_service/internal/marketing/repository"
|
||||
"wx_service/internal/marketing/service"
|
||||
"wx_service/internal/model"
|
||||
)
|
||||
|
||||
type TemplateHandler struct {
|
||||
svc *service.TemplateService
|
||||
}
|
||||
|
||||
func NewTemplateHandler(svc *service.TemplateService) *TemplateHandler {
|
||||
return &TemplateHandler{svc: svc}
|
||||
}
|
||||
|
||||
// ListEnabled returns enabled templates for mini-program users.
|
||||
func (h *TemplateHandler) ListEnabled(c *gin.Context) {
|
||||
categoryID := parseUintQuery(c, "category_id", 0)
|
||||
page := parseIntQuery(c, "page", 1)
|
||||
pageSize := parseIntQuery(c, "page_size", 20)
|
||||
|
||||
resp, err := h.svc.List(repository.TemplateListParams{
|
||||
CategoryID: categoryID,
|
||||
OnlyEnabled: true,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Success(resp))
|
||||
}
|
||||
|
||||
// GetDetail returns a single template by ID.
|
||||
func (h *TemplateHandler) GetDetail(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
tpl, err := h.svc.GetByID(id)
|
||||
if err != nil {
|
||||
if service.IsNotFoundError(err) {
|
||||
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "模板不存在"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Success(tpl))
|
||||
}
|
||||
|
||||
// AdminList returns all templates for admin.
|
||||
func (h *TemplateHandler) AdminList(c *gin.Context) {
|
||||
categoryID := parseUintQuery(c, "category_id", 0)
|
||||
page := parseIntQuery(c, "page", 1)
|
||||
pageSize := parseIntQuery(c, "page_size", 20)
|
||||
|
||||
resp, err := h.svc.List(repository.TemplateListParams{
|
||||
CategoryID: categoryID,
|
||||
OnlyEnabled: false,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Success(resp))
|
||||
}
|
||||
|
||||
func (h *TemplateHandler) AdminCreate(c *gin.Context) {
|
||||
var req service.TemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
tpl, err := h.svc.Create(req)
|
||||
if err != nil {
|
||||
if service.IsBadRequestError(err) {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(tpl))
|
||||
}
|
||||
|
||||
func (h *TemplateHandler) AdminUpdate(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req service.TemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
tpl, err := h.svc.Update(id, req)
|
||||
if err != nil {
|
||||
if service.IsNotFoundError(err) {
|
||||
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "模板不存在"))
|
||||
return
|
||||
}
|
||||
if service.IsBadRequestError(err) {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(tpl))
|
||||
}
|
||||
|
||||
func (h *TemplateHandler) AdminDelete(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.Delete(id); err != nil {
|
||||
if service.IsNotFoundError(err) {
|
||||
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "模板不存在"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "服务器错误"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Success(nil))
|
||||
}
|
||||
|
||||
func parseIntQuery(c *gin.Context, key string, defaultValue int) int {
|
||||
v := c.Query(key)
|
||||
if v == "" {
|
||||
return defaultValue
|
||||
}
|
||||
parsed, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func parseUintQuery(c *gin.Context, key string, defaultValue uint) uint {
|
||||
v := c.Query(key)
|
||||
if v == "" {
|
||||
return defaultValue
|
||||
}
|
||||
parsed, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return uint(parsed)
|
||||
}
|
||||
Reference in New Issue
Block a user