feat: add ads/sponsor/captcha modules and polish home & chat UI
This commit is contained in:
253
backend/handler/ad.go
Normal file
253
backend/handler/ad.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/middleware"
|
||||
"github.com/freefire/jiang13-bbs/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CaptchaIssue GET /api/captcha
|
||||
func (h *Handlers) CaptchaIssue(c *gin.Context) {
|
||||
if h.Ads == nil || h.Ads.Captcha() == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "验证码服务不可用"})
|
||||
return
|
||||
}
|
||||
id, img, err := h.Ads.Captcha().Issue()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成验证码失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"id": id, "image": img})
|
||||
}
|
||||
|
||||
// AdsPublicConfig GET /api/ads/config
|
||||
func (h *Handlers) AdsPublicConfig(c *gin.Context) {
|
||||
cfg, err := h.Ads.PublicConfig()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取配置失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
|
||||
type adSubmitReq struct {
|
||||
Kind string `json:"kind"`
|
||||
ContactEmail string `json:"contact_email"`
|
||||
LinkURL string `json:"link_url"`
|
||||
ImageURL string `json:"image_url"`
|
||||
Title string `json:"title"`
|
||||
TextColor string `json:"text_color"`
|
||||
BgColor string `json:"bg_color"`
|
||||
DurationDays int `json:"duration_days"`
|
||||
PaymentID string `json:"payment_id"`
|
||||
BuyerNote string `json:"buyer_note"`
|
||||
CaptchaID string `json:"captcha_id"`
|
||||
CaptchaCode string `json:"captcha_code"`
|
||||
}
|
||||
|
||||
// AdsSubmit POST /api/ads
|
||||
func (h *Handlers) AdsSubmit(c *gin.Context) {
|
||||
var req adSubmitReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误"})
|
||||
return
|
||||
}
|
||||
ad, err := h.Ads.Submit(service.AdSubmitInput{
|
||||
Kind: req.Kind,
|
||||
ContactEmail: req.ContactEmail,
|
||||
LinkURL: req.LinkURL,
|
||||
ImageURL: req.ImageURL,
|
||||
Title: req.Title,
|
||||
TextColor: req.TextColor,
|
||||
BgColor: req.BgColor,
|
||||
DurationDays: req.DurationDays,
|
||||
PaymentID: req.PaymentID,
|
||||
BuyerNote: req.BuyerNote,
|
||||
CaptchaID: req.CaptchaID,
|
||||
CaptchaCode: req.CaptchaCode,
|
||||
IP: c.ClientIP(),
|
||||
})
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
switch {
|
||||
case errors.Is(err, service.ErrAdCaptcha):
|
||||
status = http.StatusForbidden
|
||||
case errors.Is(err, service.ErrAdDisabled):
|
||||
status = http.StatusServiceUnavailable
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ad": ad, "message": "提交成功,请完成付款并等待审核"})
|
||||
}
|
||||
|
||||
// AdminGetAdsConfig GET /api/admin/ads/config
|
||||
func (h *Handlers) AdminGetAdsConfig(c *gin.Context) {
|
||||
cfg, err := h.Ads.GetConfig()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
|
||||
// AdminSaveAdsConfig PUT /api/admin/ads/config
|
||||
func (h *Handlers) AdminSaveAdsConfig(c *gin.Context) {
|
||||
var req service.AdConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误"})
|
||||
return
|
||||
}
|
||||
cfg, err := h.Ads.SaveConfig(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
|
||||
// AdminListAds GET /api/admin/ads
|
||||
func (h *Handlers) AdminListAds(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
||||
list, total, err := h.Ads.AdminList(c.Query("status"), page, size)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ads": list, "total": total, "page": page, "size": size})
|
||||
}
|
||||
|
||||
// AdminApproveAd PUT /api/admin/ads/:id/approve
|
||||
func (h *Handlers) AdminApproveAd(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效 ID"})
|
||||
return
|
||||
}
|
||||
actor := middleware.CurrentActor(c)
|
||||
ad, err := h.Ads.Approve(actor.ID, uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ad": ad})
|
||||
}
|
||||
|
||||
type adRejectReq struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// AdminRejectAd PUT /api/admin/ads/:id/reject
|
||||
func (h *Handlers) AdminRejectAd(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效 ID"})
|
||||
return
|
||||
}
|
||||
var req adRejectReq
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
actor := middleware.CurrentActor(c)
|
||||
ad, err := h.Ads.Reject(actor.ID, uint(id), req.Reason)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ad": ad})
|
||||
}
|
||||
|
||||
// AdminDeleteAd DELETE /api/admin/ads/:id
|
||||
func (h *Handlers) AdminDeleteAd(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效 ID"})
|
||||
return
|
||||
}
|
||||
if err := h.Ads.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
type adSortReq struct {
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// AdminSortAd PUT /api/admin/ads/:id/sort
|
||||
func (h *Handlers) AdminSortAd(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效 ID"})
|
||||
return
|
||||
}
|
||||
var req adSortReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误"})
|
||||
return
|
||||
}
|
||||
if err := h.Ads.SetSort(uint(id), req.SortOrder); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// AdminUploadAdQR POST /api/admin/upload/ad-qr — 收款码上传到 /uploads/ads/
|
||||
func (h *Handlers) AdminUploadAdQR(c *gin.Context) {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, service.AdAssetMaxBytes+4096)
|
||||
fh, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择图片文件"})
|
||||
return
|
||||
}
|
||||
f, err := fh.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "读取图片失败"})
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
url, err := h.Upload.SaveAdAsset(f)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"url": url})
|
||||
}
|
||||
|
||||
// AdminGetSponsorsConfig GET /api/admin/sponsors/config
|
||||
func (h *Handlers) AdminGetSponsorsConfig(c *gin.Context) {
|
||||
if h.Ads == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "服务不可用"})
|
||||
return
|
||||
}
|
||||
cfg, err := h.Ads.GetSponsorsConfig()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
|
||||
// AdminSaveSponsorsConfig PUT /api/admin/sponsors/config
|
||||
func (h *Handlers) AdminSaveSponsorsConfig(c *gin.Context) {
|
||||
if h.Ads == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "服务不可用"})
|
||||
return
|
||||
}
|
||||
var req service.SponsorsConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误"})
|
||||
return
|
||||
}
|
||||
cfg, err := h.Ads.SaveSponsorsConfig(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
@@ -33,6 +33,7 @@ type Handlers struct {
|
||||
Telemetry *service.TelemetryService
|
||||
Analytics *service.AnalyticsService
|
||||
HidePwd *service.HidePasswordCookie
|
||||
Ads *service.AdService
|
||||
}
|
||||
|
||||
// resolvePublishStatus 决定新帖/新评的初始状态:
|
||||
|
||||
@@ -38,5 +38,11 @@ func (h *Handlers) Overview(c *gin.Context) {
|
||||
"sidebar_pages": data.SidebarPages,
|
||||
"new_users": data.NewUsers,
|
||||
"checkin": data.Checkin,
|
||||
"ads": data.Ads,
|
||||
"ads_panel_title": data.AdsPanelTitle,
|
||||
"ads_enabled": data.AdsEnabled,
|
||||
"sponsors": data.Sponsors,
|
||||
"sponsors_panel_title": data.SponsorsPanelTitle,
|
||||
"sponsors_enabled": data.SponsorsEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user