Files
geo/server/internal/ops/transport/media_supply_handler.go
T
root 9d6181260a
Desktop Client Build / Resolve Build Metadata (push) Successful in 43s
Frontend CI / Frontend (push) Successful in 3m49s
Backend CI / Backend (push) Failing after 7m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m4s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 28s
feat(media-supply): add media resource supply marketplace
Introduce an end-to-end media-supply feature: tenant-side resource sync
service/worker backed by a Meijiequan supplier client, ops-side management
APIs, and admin/ops web views for resources, orders, favorites and
submission. Adds a shared digitocr helper, MediaSupply config blocks for
tenant and ops, shared types, and migrations for supplier media resources,
price overrides, customer visibility and order refunds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 23:17:01 +08:00

202 lines
5.9 KiB
Go

package transport
import (
"strconv"
"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 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)
}