Files
geo/server/internal/ops/transport/media_supply_handler.go
T
root 31811b07d4
Frontend CI / Frontend (push) Successful in 5m44s
Backend CI / Backend (push) Failing after 6m58s
feat: add media supply billing center
2026-06-29 23:13:33 +08:00

267 lines
7.8 KiB
Go

package transport
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/geo-platform/tenant-api/internal/ops/app"
"github.com/geo-platform/tenant-api/internal/shared/response"
)
type setMediaSupplyPriceRequest struct {
PriceType string `json:"price_type"`
SellPriceCents int64 `json:"sell_price_cents"`
Enabled *bool `json:"enabled,omitempty"`
}
type setMediaSupplyVisibilityRequest struct {
CustomerVisible bool `json:"customer_visible"`
}
type adjustMediaSupplyWalletRequest struct {
TenantID int64 `json:"tenant_id" binding:"required"`
UserID int64 `json:"user_id" binding:"required"`
DeltaCents int64 `json:"delta_cents" binding:"required"`
Note string `json:"note"`
}
func listMediaSupplyResourcesHandler(svc *app.MediaSupplyService) gin.HandlerFunc {
return func(c *gin.Context) {
modelID, err := parseOptionalOpsIntQuery(c, "model_id")
if err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_model_id", "媒体资源类型参数必须是数字"))
return
}
minPriceCents, err := parseOptionalOpsInt64Query(c, "min_price_cents")
if err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_min_price_cents", "最低价格参数必须是数字"))
return
}
maxPriceCents, err := parseOptionalOpsInt64Query(c, "max_price_cents")
if err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_max_price_cents", "最高价格参数必须是数字"))
return
}
page, _ := parseOptionalOpsIntQuery(c, "page")
size, _ := parseOptionalOpsIntQuery(c, "page_size")
result, err := svc.ListResources(c.Request.Context(), app.ListMediaSupplyResourcesInput{
ModelID: modelID,
Keyword: c.Query("keyword"),
RemarkKeyword: c.Query("remark_keyword"),
ChannelType: c.Query("channel_type"),
Region: c.Query("region"),
InclusionEffect: c.Query("inclusion_effect"),
LinkType: c.Query("link_type"),
MinPriceCents: minPriceCents,
MaxPriceCents: maxPriceCents,
Visibility: c.Query("visibility"),
Page: page,
Size: size,
})
if err != nil {
response.Error(c, err)
return
}
response.Success(c, result)
}
}
func setMediaSupplyResourcePriceHandler(svc *app.MediaSupplyService) gin.HandlerFunc {
return func(c *gin.Context) {
id, err := parseIDParam(c)
if err != nil {
response.Error(c, err)
return
}
var body setMediaSupplyPriceRequest
if err := c.ShouldBindJSON(&body); err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_payload", "价格参数不正确"))
return
}
enabled := true
if body.Enabled != nil {
enabled = *body.Enabled
}
if err := svc.SetResourcePrice(c.Request.Context(), actorFromGin(c), id, app.SetMediaSupplyPriceInput{
PriceType: body.PriceType,
SellPriceCents: body.SellPriceCents,
Enabled: enabled,
}); err != nil {
response.Error(c, err)
return
}
response.Success(c, gin.H{"updated": true})
}
}
func setMediaSupplyResourceVisibilityHandler(svc *app.MediaSupplyService) gin.HandlerFunc {
return func(c *gin.Context) {
id, err := parseIDParam(c)
if err != nil {
response.Error(c, err)
return
}
var body setMediaSupplyVisibilityRequest
if err := c.ShouldBindJSON(&body); err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_payload", "展示状态参数不正确"))
return
}
if err := svc.SetResourceVisibility(c.Request.Context(), actorFromGin(c), id, app.SetMediaSupplyVisibilityInput{
CustomerVisible: body.CustomerVisible,
}); err != nil {
response.Error(c, err)
return
}
response.Success(c, gin.H{"updated": true})
}
}
func queueMediaSupplySyncHandler(svc *app.MediaSupplyService) gin.HandlerFunc {
return func(c *gin.Context) {
var body struct {
ModelID int `json:"model_id"`
}
if c.Request.Body != nil {
_ = c.ShouldBindJSON(&body)
}
jobID, err := svc.QueueSync(c.Request.Context(), actorFromGin(c), body.ModelID)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, gin.H{"job_id": jobID})
}
}
func listMediaSupplyWalletsHandler(svc *app.MediaSupplyService) gin.HandlerFunc {
return func(c *gin.Context) {
tenantID, err := parseOptionalOpsInt64Query(c, "tenant_id")
if err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_tenant_id", "租户参数必须是数字"))
return
}
page, _ := parseOptionalOpsIntQuery(c, "page")
size, _ := parseOptionalOpsIntQuery(c, "page_size")
result, err := svc.ListWallets(c.Request.Context(), app.ListMediaSupplyWalletsInput{
Keyword: c.Query("keyword"),
TenantID: tenantID,
Page: page,
Size: size,
})
if err != nil {
response.Error(c, err)
return
}
response.Success(c, result)
}
}
func listMediaSupplyBillingsHandler(svc *app.MediaSupplyService) gin.HandlerFunc {
return func(c *gin.Context) {
tenantID, err := parseOptionalOpsInt64Query(c, "tenant_id")
if err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_tenant_id", "租户参数必须是数字"))
return
}
userID, err := parseOptionalOpsInt64Query(c, "user_id")
if err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_user_id", "用户参数必须是数字"))
return
}
orderID, err := parseOptionalOpsInt64Query(c, "order_id")
if err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_order_id", "订单参数必须是数字"))
return
}
startAt, err := parseOptionalOpsTimeQuery(c, "start_at")
if err != nil {
response.Error(c, response.ErrBadRequest(40003, "invalid_start_at", "start_at 必须是 RFC3339"))
return
}
endAt, err := parseOptionalOpsTimeQuery(c, "end_at")
if err != nil {
response.Error(c, response.ErrBadRequest(40004, "invalid_end_at", "end_at 必须是 RFC3339"))
return
}
if startAt != nil && endAt != nil && !startAt.Before(*endAt) {
response.Error(c, response.ErrBadRequest(40005, "invalid_billing_time_range", "结束时间必须晚于开始时间"))
return
}
page, _ := parseOptionalOpsIntQuery(c, "page")
size, _ := parseOptionalOpsIntQuery(c, "page_size")
result, err := svc.ListBillings(c.Request.Context(), app.ListMediaSupplyBillingsInput{
Keyword: c.Query("keyword"),
TenantID: tenantID,
UserID: userID,
OrderID: orderID,
Reason: c.Query("reason"),
StartAt: startAt,
EndAt: endAt,
Page: page,
Size: size,
})
if err != nil {
response.Error(c, err)
return
}
response.Success(c, result)
}
}
func adjustMediaSupplyWalletHandler(svc *app.MediaSupplyService) gin.HandlerFunc {
return func(c *gin.Context) {
var body adjustMediaSupplyWalletRequest
if err := c.ShouldBindJSON(&body); err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_payload", "余额调整参数不正确"))
return
}
actor := actorFromGin(c)
operatorTag := "ops manual adjustment"
if actor != nil && actor.DisplayName != "" {
operatorTag = "ops " + actor.DisplayName
}
result, err := svc.AdjustWallet(c.Request.Context(), actor, app.AdjustMediaSupplyWalletInput{
TenantID: body.TenantID,
UserID: body.UserID,
DeltaCents: body.DeltaCents,
Note: body.Note,
OperatorTag: operatorTag,
})
if err != nil {
response.Error(c, err)
return
}
response.Success(c, result)
}
}
func parseOptionalOpsIntQuery(c *gin.Context, key string) (int, error) {
raw := c.Query(key)
if raw == "" {
return 0, nil
}
return strconv.Atoi(raw)
}
func parseOptionalOpsInt64Query(c *gin.Context, key string) (int64, error) {
raw := c.Query(key)
if raw == "" {
return 0, nil
}
return strconv.ParseInt(raw, 10, 64)
}
func parseOptionalOpsTimeQuery(c *gin.Context, key string) (*time.Time, error) {
raw := c.Query(key)
if raw == "" {
return nil, nil
}
parsed, err := time.Parse(time.RFC3339, raw)
if err != nil {
return nil, err
}
return &parsed, nil
}