feat(media-supply): add media resource supply marketplace
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
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
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>
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ type Deps struct {
|
||||
ObjectStorage *app.ObjectStorageService
|
||||
Compliance *app.ComplianceService
|
||||
Scheduler *app.SchedulerService
|
||||
MediaSupply *app.MediaSupplyService
|
||||
}
|
||||
|
||||
func (d Deps) ServerAllowedOrigins() []string {
|
||||
@@ -123,6 +124,13 @@ func RegisterRoutes(d Deps) {
|
||||
authed.POST("/jobs/:source/:id/retry", retryJobHandler(d.Jobs))
|
||||
authed.POST("/jobs/:source/:id/cancel", cancelJobHandler(d.Jobs))
|
||||
|
||||
authed.GET("/media-supply/resources", listMediaSupplyResourcesHandler(d.MediaSupply))
|
||||
authed.PUT("/media-supply/resources/:id/price", setMediaSupplyResourcePriceHandler(d.MediaSupply))
|
||||
authed.PUT("/media-supply/resources/:id/visibility", setMediaSupplyResourceVisibilityHandler(d.MediaSupply))
|
||||
authed.POST("/media-supply/sync-jobs", queueMediaSupplySyncHandler(d.MediaSupply))
|
||||
authed.GET("/media-supply/wallets", listMediaSupplyWalletsHandler(d.MediaSupply))
|
||||
authed.POST("/media-supply/wallets/adjustments", adjustMediaSupplyWalletHandler(d.MediaSupply))
|
||||
|
||||
authed.GET("/scheduler/jobs", listSchedulerJobsHandler(d.Scheduler))
|
||||
authed.GET("/scheduler/jobs/:key", getSchedulerJobHandler(d.Scheduler))
|
||||
authed.PATCH("/scheduler/jobs/:key", updateSchedulerJobHandler(d.Scheduler))
|
||||
|
||||
Reference in New Issue
Block a user