diff --git a/backend/internal/handler/merchant.go b/backend/internal/handler/merchant.go index 33b37f0..c5ac5f8 100644 --- a/backend/internal/handler/merchant.go +++ b/backend/internal/handler/merchant.go @@ -3,11 +3,8 @@ package handler import ( "sort" "strconv" - "strings" - "time" "affiliate_dash/internal/middleware" - "affiliate_dash/internal/model" "affiliate_dash/internal/pkg/response" "affiliate_dash/internal/service" @@ -55,791 +52,6 @@ func mapKeys(values map[string]struct{}) []string { return keys } -func (h *MerchantHandler) ListProducts(c *gin.Context) { - page, size := pageParams(c) - list, total, err := h.merchantSvc.ListMerchantProductsWithOptions(middleware.GetMerchantID(c), page, size, service.ListMerchantProductsOptions{ - Status: strings.TrimSpace(c.Query("status")), - Keyword: strings.TrimSpace(c.Query("keyword")), - }) - if err != nil { - if err.Error() == "无效的商品状态" { - response.BadRequest(c, err.Error()) - return - } - response.ServerError(c, err.Error()) - return - } - response.Page(c, list, total, page, size) -} - -type merchantProductStatusReq struct { - Status string `json:"status" binding:"required"` -} - -// UpdateProductStatus PATCH /api/merchant/products/:id/status -// 商户仅能上架/下架商品,价格、成本等配置由平台管理员维护。 -// UpdateProduct PATCH /api/merchant/products/:id — 编辑商品(显示名称、售价、成本、库存、状态)。 -// 严格风控门禁:仅平台超级管理员(model.RoleAdmin)拥有修改商品价格、成本、库存与名称的权限,普通分销商严禁修改。 -func (h *MerchantHandler) UpdateProduct(c *gin.Context) { - if middleware.GetRole(c) != model.RoleAdmin { - response.Forbidden(c, "权限不足:仅平台超级管理员可编辑商品价格、成本与库存") - return - } - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if id == 0 { - response.BadRequest(c, "参数错误") - return - } - var req merchantProductUpdateReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误") - return - } - if err := h.merchantSvc.UpdateMerchantProduct(middleware.GetMerchantID(c), uint(id), service.UpdateMerchantProductInput{ - DisplayName: req.DisplayName, - PriceAmount: req.PriceAmount, - CostAmount: req.CostAmount, - Stock: req.Stock, - Status: req.Status, - FulfillmentConfig: req.FulfillmentConfig, - }, middleware.GetUserID(c)); err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, nil) -} - -func (h *MerchantHandler) UpdateProductStatus(c *gin.Context) { - var req merchantProductStatusReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:status 必填") - return - } - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if err := h.merchantSvc.UpdateMerchantProduct(middleware.GetMerchantID(c), uint(id), service.UpdateMerchantProductInput{ - Status: &req.Status, - }, middleware.GetUserID(c)); err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, nil) -} - -// DeleteProduct DELETE /api/merchant/products/:id — 仅已下架可删(软删除)。 -func (h *MerchantHandler) DeleteProduct(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if err := h.merchantSvc.DeleteMerchantProduct(middleware.GetMerchantID(c), uint(id), middleware.GetUserID(c)); err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, nil) -} - -func (h *MerchantHandler) ListOrders(c *gin.Context) { - page, size := pageParams(c) - orderSource := strings.TrimSpace(c.Query("order_source")) - list, total, err := h.fulfillmentSvc.ListOrders(middleware.GetMerchantID(c), page, size, c.Query("order_status"), orderSource, c.Query("keyword")) - if err != nil { - if err.Error() == "无效的订单来源" || err.Error() == "无效的订单状态" { - response.BadRequest(c, err.Error()) - return - } - response.ServerError(c, err.Error()) - return - } - response.Page(c, list, total, page, size) -} - -// ListManualOrderProducts returns active products for order creation without granting product-management access. -func (h *MerchantHandler) ListManualOrderProducts(c *gin.Context) { - list, _, err := h.merchantSvc.ListMerchantProductsForClient(middleware.GetMerchantID(c), 1, 100) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.OK(c, list) -} - -type merchantManualOrderReq struct { - ClientOrderNo string `json:"client_order_no" binding:"required,max=96"` - SKU string `json:"sku" binding:"required,max=96"` - Quantity int64 `json:"quantity"` - BuyerReference string `json:"buyer_reference" binding:"max=128"` - GameAccount string `json:"game_account" binding:"max=128"` - Note string `json:"note" binding:"max=512"` -} - -// CreateManualOrder 在商户后台创建真实订单;扣款、库存和回调与开放 API 下单保持一致。 -func (h *MerchantHandler) CreateManualOrder(c *gin.Context) { - var req merchantManualOrderReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:商户单号与商品必填") - return - } - data := map[string]string{} - if gameAccount := strings.TrimSpace(req.GameAccount); gameAccount != "" { - data["game_account"] = gameAccount - } - if note := strings.TrimSpace(req.Note); note != "" { - data["manual_note"] = note - } - result, err := h.fulfillmentSvc.CreateManualOrder(service.CreateManualOrderInput{ - MerchantID: middleware.GetMerchantID(c), - ActorUserID: middleware.GetUserID(c), - ClientOrderNo: req.ClientOrderNo, - SKU: req.SKU, - Quantity: req.Quantity, - BuyerReference: req.BuyerReference, - RequestData: data, - }) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - canShip, cannotShipReason := service.CanFulfill(result.Order) - response.OK(c, gin.H{ - "order": result.Order, - "idempotent": result.Idempotent, - "can_ship": canShip, - "cannot_ship_reason": cannotShipReason, - }) -} - -type merchantTestOrderReq struct { - SKU string `json:"sku" binding:"required"` - BuyerReference string `json:"buyer_reference"` - Note string `json:"note"` - OrderStatus string `json:"order_status"` -} - -func (h *MerchantHandler) CreateTestOrder(c *gin.Context) { - var req merchantTestOrderReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:sku 必填") - return - } - order, err := h.fulfillmentSvc.CreateTestOrder(service.CreateTestOrderInput{ - MerchantID: middleware.GetMerchantID(c), - ActorUserID: middleware.GetUserID(c), - SKU: req.SKU, - BuyerReference: req.BuyerReference, - Note: req.Note, - OrderStatus: req.OrderStatus, - }) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - canShip, cannotShipReason := service.CanFulfill(order) - response.OK(c, gin.H{ - "order": order, - "can_ship": canShip, - "cannot_ship_reason": cannotShipReason, - }) -} - -type merchantCancelOrderReq struct { - Reason string `json:"reason" binding:"required,max=512"` -} - -// CancelOrder 商户后台取消订单:全额退还积分并回补库存,取消后订单不可再发货。 -func (h *MerchantHandler) CancelOrder(c *gin.Context) { - var req merchantCancelOrderReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:取消原因必填且最长 512 字") - return - } - order, err := h.fulfillmentSvc.CancelOrderByUser(middleware.GetMerchantID(c), middleware.GetUserID(c), c.Param("order_no"), req.Reason) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, gin.H{ - "order": order, - "refund_amount": order.Amount, - }) -} - -func (h *MerchantHandler) GetDeliveryLink(c *gin.Context) { - link, err := h.deliverySvc.GetOrCreateDeliveryLink(middleware.GetMerchantID(c), c.Param("order_no"), requestBaseURL(c)) - if err != nil { - writeDeliveryError(c, err) - return - } - response.OK(c, link) -} - -func (h *MerchantHandler) RevokeDeliveryLink(c *gin.Context) { - if err := h.deliverySvc.RevokeDeliveryLink(middleware.GetMerchantID(c), middleware.GetUserID(c), c.Param("order_no")); err != nil { - writeDeliveryError(c, err) - return - } - response.OK(c, nil) -} - -func (h *MerchantHandler) RestoreDeliveryLink(c *gin.Context) { - link, err := h.deliverySvc.RestoreDeliveryLink(middleware.GetMerchantID(c), middleware.GetUserID(c), c.Param("order_no"), requestBaseURL(c)) - if err != nil { - writeDeliveryError(c, err) - return - } - response.OK(c, link) -} - -func (h *MerchantHandler) GetWallet(c *gin.Context) { - wallet, err := h.fulfillmentSvc.GetWallet(middleware.GetMerchantID(c)) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.OK(c, wallet) -} - -func (h *MerchantHandler) ListWalletLedger(c *gin.Context) { - page, size := pageParams(c) - referenceNo := c.Query("reference_no") - entryType := c.Query("type") - list, total, err := h.fulfillmentSvc.ListWalletLedger(middleware.GetMerchantID(c), page, size, referenceNo, entryType) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.Page(c, list, total, page, size) -} - -// AdminGetWallet 供平台管理员查看指定商户的钱包。 -func (h *MerchantHandler) AdminGetWallet(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if id == 0 { - response.BadRequest(c, "商户 ID 无效") - return - } - wallet, err := h.fulfillmentSvc.GetWallet(uint(id)) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.OK(c, wallet) -} - -// AdminListWalletLedger 供平台管理员查看指定商户的积分流水。 -func (h *MerchantHandler) AdminListWalletLedger(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if id == 0 { - response.BadRequest(c, "商户 ID 无效") - return - } - page, size := pageParams(c) - list, total, err := h.fulfillmentSvc.ListWalletLedger(uint(id), page, size, c.Query("reference_no"), c.Query("type")) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.Page(c, list, total, page, size) -} - -type walletAdjustReq struct { - Amount int64 `json:"amount" binding:"required"` - IdempotencyKey string `json:"idempotency_key" binding:"required"` - Note string `json:"note"` -} - -// AdminAdjustWallet 供平台管理员手动调整指定商户的积分,商户侧无任何调账入口。 -func (h *MerchantHandler) AdminAdjustWallet(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if id == 0 { - response.BadRequest(c, "商户 ID 无效") - return - } - var req walletAdjustReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:amount 与 idempotency_key 必填") - return - } - wallet, err := h.fulfillmentSvc.AdjustWallet(service.WalletAdjustInput{ - MerchantID: uint(id), - ActorUserID: middleware.GetUserID(c), - Amount: req.Amount, - IdempotencyKey: req.IdempotencyKey, - Note: req.Note, - }) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - h.rechargeSvc.CheckLowBalanceAndNotify(uint(id)) - response.OK(c, wallet) -} - -func (h *MerchantHandler) ListAPIClients(c *gin.Context) { - clients, err := h.merchantSvc.ListAPIClients(middleware.GetMerchantID(c)) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.OK(c, clients) -} - -type apiClientReq struct { - Name string `json:"name" binding:"required"` - Scopes string `json:"scopes" binding:"required"` - SignatureVersion string `json:"signature_version"` - ExpiresAt string `json:"expires_at"` -} - -func (h *MerchantHandler) CreateAPIClient(c *gin.Context) { - var req apiClientReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:name 与 scopes 必填") - return - } - var expiresAt *time.Time - if req.ExpiresAt != "" { - value, err := time.Parse(time.RFC3339, req.ExpiresAt) - if err != nil { - response.BadRequest(c, "expires_at 必须是 RFC3339 时间") - return - } - expiresAt = &value - } - credential, err := h.merchantSvc.CreateAPIClient(middleware.GetMerchantID(c), service.CreateAPIClientInput{ - Name: req.Name, - Scopes: req.Scopes, - SignatureVersion: req.SignatureVersion, - ExpiresAt: expiresAt, - }, middleware.GetUserID(c)) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, credential) -} - -type statusReq struct { - Status string `json:"status" binding:"required"` -} - -func (h *MerchantHandler) UpdateAPIClientStatus(c *gin.Context) { - var req statusReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误") - return - } - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if err := h.merchantSvc.UpdateAPIClientStatus(middleware.GetMerchantID(c), uint(id), req.Status, middleware.GetUserID(c)); err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, nil) -} - -// DeleteAPIClient DELETE /api/merchant/api-clients/:id -func (h *MerchantHandler) DeleteAPIClient(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if err := h.merchantSvc.DeleteAPIClient(middleware.GetMerchantID(c), uint(id), middleware.GetUserID(c)); err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, nil) -} - -func (h *MerchantHandler) ListCallbacks(c *gin.Context) { - subscription, err := h.callbackSvc.GetSubscription(middleware.GetMerchantID(c)) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.OK(c, subscription) -} - -type callbackReq struct { - Name string `json:"name"` - URL string `json:"url" binding:"required"` - Events string `json:"events" binding:"required"` - Status string `json:"status"` - RotateSecret bool `json:"rotate_secret"` -} - -func (h *MerchantHandler) CreateCallback(c *gin.Context) { - var req callbackReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:url、events 必填") - return - } - credential, err := h.callbackSvc.CreateSubscription(middleware.GetMerchantID(c), service.CreateCallbackInput{ - Name: req.Name, - URL: req.URL, - Events: req.Events, - Status: req.Status, - RotateSecret: req.RotateSecret, - }, middleware.GetUserID(c)) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, credential) -} - -func (h *MerchantHandler) ListMembers(c *gin.Context) { - members, err := h.merchantSvc.ListMembers(middleware.GetMerchantID(c)) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.OK(c, members) -} - -type addMemberReq struct { - UserID uint `json:"user_id"` - Username string `json:"username"` - Password string `json:"password"` - Nickname string `json:"nickname"` - Role string `json:"role" binding:"required"` - IsDefault bool `json:"is_default"` -} - -func (h *MerchantHandler) AddCurrentMerchantMember(c *gin.Context) { - var req addMemberReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:角色必填") - return - } - member, err := h.merchantSvc.AddMember(middleware.GetMerchantID(c), service.AddMemberInput{ - UserID: req.UserID, - Username: req.Username, - Password: req.Password, - Nickname: req.Nickname, - Role: req.Role, - IsDefault: req.IsDefault, - }, middleware.GetUserID(c)) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, member) -} - -type updateMemberReq struct { - Role string `json:"role" binding:"required"` - Status int `json:"status"` - IsDefault bool `json:"is_default"` -} - -func (h *MerchantHandler) UpdateCurrentMerchantMember(c *gin.Context) { - memberID, _ := strconv.ParseUint(c.Param("id"), 10, 64) - var req updateMemberReq - if memberID == 0 || c.ShouldBindJSON(&req) != nil { - response.BadRequest(c, "参数错误:角色必填") - return - } - member, err := h.merchantSvc.UpdateMember(middleware.GetMerchantID(c), uint(memberID), service.UpdateMemberInput{ - Role: req.Role, Status: req.Status, IsDefault: req.IsDefault, - }, middleware.GetUserID(c)) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, member) -} - -func (h *MerchantHandler) RemoveCurrentMerchantMember(c *gin.Context) { - memberID, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if memberID == 0 { - response.BadRequest(c, "参数错误") - return - } - if err := h.merchantSvc.RemoveMember(middleware.GetMerchantID(c), uint(memberID), middleware.GetUserID(c)); err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, nil) -} - -func (h *MerchantHandler) ListRoles(c *gin.Context) { - roles, err := h.merchantSvc.ListRoles(middleware.GetMerchantID(c)) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.OK(c, roles) -} - -type createMerchantRoleReq struct { - Code string `json:"code" binding:"required"` - Name string `json:"name" binding:"required"` - Permissions []string `json:"permissions"` -} - -func (h *MerchantHandler) CreateRole(c *gin.Context) { - var req createMerchantRoleReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:角色编码和名称必填") - return - } - role, err := h.merchantSvc.CreateRole(middleware.GetMerchantID(c), service.MerchantRoleInput{ - Code: req.Code, Name: req.Name, Permissions: req.Permissions, - }, middleware.GetUserID(c)) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, role) -} - -func (h *MerchantHandler) UpdateRole(c *gin.Context) { - roleID, _ := strconv.ParseUint(c.Param("id"), 10, 64) - var req createMerchantRoleReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:角色名称必填") - return - } - role, err := h.merchantSvc.UpdateRole(middleware.GetMerchantID(c), uint(roleID), service.MerchantRoleInput{ - Name: req.Name, Permissions: req.Permissions, - }, middleware.GetUserID(c)) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, role) -} - -func (h *MerchantHandler) ListPlatformMerchants(c *gin.Context) { - page, size := pageParams(c) - list, total, err := h.merchantSvc.ListMerchants(page, size) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.Page(c, list, total, page, size) -} - -// ListProductCatalog 返回自营商户的全部可售商品,作为平台默认商品目录供分配。 -func (h *MerchantHandler) ListProductCatalog(c *gin.Context) { - list, err := h.merchantSvc.ListProductCatalog() - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.OK(c, list) -} - -// ListMerchantProductsByAdmin 供平台管理员查看指定商户的可售商品。 -func (h *MerchantHandler) ListMerchantProductsByAdmin(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - list, err := h.merchantSvc.ListMerchantProductsByAdmin(uint(id)) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.OK(c, list) -} - -type merchantProductUpdateReq struct { - DisplayName *string `json:"display_name"` - PriceAmount *int64 `json:"price_amount"` - CostAmount *int64 `json:"cost_amount"` - Stock *int64 `json:"stock"` - Status *string `json:"status"` - FulfillmentConfig *string `json:"fulfillment_config"` -} - -// AdminUpdateMerchantProduct 供平台管理员编辑指定商户的商品(价格、成本、库存、状态等)。 -func (h *MerchantHandler) AdminUpdateMerchantProduct(c *gin.Context) { - merchantID, _ := strconv.ParseUint(c.Param("id"), 10, 64) - productID, _ := strconv.ParseUint(c.Param("pid"), 10, 64) - if merchantID == 0 || productID == 0 { - response.BadRequest(c, "参数错误") - return - } - var req merchantProductUpdateReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误") - return - } - if err := h.merchantSvc.UpdateMerchantProduct(uint(merchantID), uint(productID), service.UpdateMerchantProductInput{ - DisplayName: req.DisplayName, - PriceAmount: req.PriceAmount, - CostAmount: req.CostAmount, - Stock: req.Stock, - Status: req.Status, - FulfillmentConfig: req.FulfillmentConfig, - }, middleware.GetUserID(c)); err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, nil) -} - -type assignProductsReq struct { - CatalogIDs []uint `json:"catalog_ids"` -} - -// AssignProducts 按商品目录 ID 批量同步商户的可售商品。 -func (h *MerchantHandler) AssignProducts(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - var req assignProductsReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误") - return - } - count, err := h.merchantSvc.AssignProducts(uint(id), service.AssignProductsInput{ - CatalogIDs: req.CatalogIDs, - }, middleware.GetUserID(c)) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, gin.H{"assigned": count}) -} - -type createMerchantReq struct { - Code string `json:"code" binding:"required"` - Name string `json:"name" binding:"required"` - ContactName string `json:"contact_name"` - ContactInfo string `json:"contact_info"` - OwnerUserID uint `json:"owner_user_id"` - OwnerUsername string `json:"owner_username"` - OwnerPassword string `json:"owner_password"` - OwnerNickname string `json:"owner_nickname"` - Features string `json:"features"` - FeeType string `json:"fee_type"` - FeeRateBP int64 `json:"fee_rate_bp"` - FeeFixedAmount int64 `json:"fee_fixed_amount"` -} - -func (h *MerchantHandler) CreateMerchant(c *gin.Context) { - var req createMerchantReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:code、name 必填") - return - } - merchant, err := h.merchantSvc.CreateMerchant(service.CreateMerchantInput{ - Code: req.Code, - Name: req.Name, - ContactName: req.ContactName, - ContactInfo: req.ContactInfo, - OwnerUserID: req.OwnerUserID, - OwnerUsername: req.OwnerUsername, - OwnerPassword: req.OwnerPassword, - OwnerNickname: req.OwnerNickname, - Features: req.Features, - FeeType: req.FeeType, - FeeRateBP: req.FeeRateBP, - FeeFixedAmount: req.FeeFixedAmount, - }, middleware.GetUserID(c)) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, merchant) -} - -type updateMerchantSettingsReq struct { - Name *string `json:"name"` - Status *string `json:"status"` - ContactName *string `json:"contact_name"` - ContactInfo *string `json:"contact_info"` - Features *string `json:"features"` - FeeType *string `json:"fee_type"` - FeeRateBP *int64 `json:"fee_rate_bp"` - FeeFixedAmount *int64 `json:"fee_fixed_amount"` -} - -func (h *MerchantHandler) UpdateMerchantSettings(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - var req updateMerchantSettingsReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误") - return - } - if err := h.merchantSvc.UpdateMerchantSettings(uint(id), service.UpdateMerchantSettingsInput{ - Name: req.Name, - Status: req.Status, - ContactName: req.ContactName, - ContactInfo: req.ContactInfo, - Features: req.Features, - FeeType: req.FeeType, - FeeRateBP: req.FeeRateBP, - FeeFixedAmount: req.FeeFixedAmount, - }, middleware.GetUserID(c)); err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, nil) -} - -func (h *MerchantHandler) AddPlatformMerchantMember(c *gin.Context) { - var req addMemberReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:user_id 与 role 必填") - return - } - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - member, err := h.merchantSvc.AddMember(uint(id), service.AddMemberInput{ - UserID: req.UserID, - Username: req.Username, - Password: req.Password, - Nickname: req.Nickname, - Role: req.Role, - IsDefault: req.IsDefault, - }, middleware.GetUserID(c)) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, member) -} - -// AdminGetMerchantAccount 平台管理员查看指定商户的负责人登录账号。 -func (h *MerchantHandler) AdminGetMerchantAccount(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if id == 0 { - response.BadRequest(c, "商户 ID 无效") - return - } - accounts, err := h.merchantSvc.ListOwnerAccounts(uint(id)) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.OK(c, accounts) -} - -type adminUpdateMerchantAccountReq struct { - UserID uint `json:"user_id" binding:"required"` - Username string `json:"username" binding:"required,min=3,max=64"` - Nickname string `json:"nickname" binding:"max=64"` - Password string `json:"password" binding:"omitempty,min=6,max=128"` - Status *int `json:"status"` -} - -// AdminUpdateMerchantAccount 平台管理员维护指定商户的负责人登录账号: -// 修改用户名/昵称、重置密码、启用停用;密码留空表示不重置。 -func (h *MerchantHandler) AdminUpdateMerchantAccount(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - var req adminUpdateMerchantAccountReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:用户名必填(3-64 位),密码至少 6 位") - return - } - if id == 0 { - response.BadRequest(c, "商户 ID 无效") - return - } - account, err := h.merchantSvc.UpdateMerchantOwnerAccount(uint(id), service.UpdateMerchantOwnerAccountInput{ - UserID: req.UserID, - Username: req.Username, - Nickname: req.Nickname, - Password: req.Password, - Status: req.Status, - }, middleware.GetUserID(c)) - if err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, account) -} - func pageParams(c *gin.Context) (int, int) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) diff --git a/backend/internal/handler/merchant_api_client.go b/backend/internal/handler/merchant_api_client.go new file mode 100644 index 0000000..e2744a3 --- /dev/null +++ b/backend/internal/handler/merchant_api_client.go @@ -0,0 +1,121 @@ +package handler + +import ( + "strconv" + "time" + + "affiliate_dash/internal/middleware" + "affiliate_dash/internal/pkg/response" + "affiliate_dash/internal/service" + + "github.com/gin-gonic/gin" +) + +func (h *MerchantHandler) ListAPIClients(c *gin.Context) { + clients, err := h.merchantSvc.ListAPIClients(middleware.GetMerchantID(c)) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, clients) +} + +type apiClientReq struct { + Name string `json:"name" binding:"required"` + Scopes string `json:"scopes" binding:"required"` + SignatureVersion string `json:"signature_version"` + ExpiresAt string `json:"expires_at"` +} + +func (h *MerchantHandler) CreateAPIClient(c *gin.Context) { + var req apiClientReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:name 与 scopes 必填") + return + } + var expiresAt *time.Time + if req.ExpiresAt != "" { + value, err := time.Parse(time.RFC3339, req.ExpiresAt) + if err != nil { + response.BadRequest(c, "expires_at 必须是 RFC3339 时间") + return + } + expiresAt = &value + } + credential, err := h.merchantSvc.CreateAPIClient(middleware.GetMerchantID(c), service.CreateAPIClientInput{ + Name: req.Name, + Scopes: req.Scopes, + SignatureVersion: req.SignatureVersion, + ExpiresAt: expiresAt, + }, middleware.GetUserID(c)) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, credential) +} + +type statusReq struct { + Status string `json:"status" binding:"required"` +} + +func (h *MerchantHandler) UpdateAPIClientStatus(c *gin.Context) { + var req statusReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误") + return + } + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if err := h.merchantSvc.UpdateAPIClientStatus(middleware.GetMerchantID(c), uint(id), req.Status, middleware.GetUserID(c)); err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, nil) +} + +// DeleteAPIClient DELETE /api/merchant/api-clients/:id +func (h *MerchantHandler) DeleteAPIClient(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if err := h.merchantSvc.DeleteAPIClient(middleware.GetMerchantID(c), uint(id), middleware.GetUserID(c)); err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, nil) +} + +func (h *MerchantHandler) ListCallbacks(c *gin.Context) { + subscription, err := h.callbackSvc.GetSubscription(middleware.GetMerchantID(c)) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, subscription) +} + +type callbackReq struct { + Name string `json:"name"` + URL string `json:"url" binding:"required"` + Events string `json:"events" binding:"required"` + Status string `json:"status"` + RotateSecret bool `json:"rotate_secret"` +} + +func (h *MerchantHandler) CreateCallback(c *gin.Context) { + var req callbackReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:url、events 必填") + return + } + credential, err := h.callbackSvc.CreateSubscription(middleware.GetMerchantID(c), service.CreateCallbackInput{ + Name: req.Name, + URL: req.URL, + Events: req.Events, + Status: req.Status, + RotateSecret: req.RotateSecret, + }, middleware.GetUserID(c)) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, credential) +} diff --git a/backend/internal/handler/merchant_member.go b/backend/internal/handler/merchant_member.go new file mode 100644 index 0000000..a47963a --- /dev/null +++ b/backend/internal/handler/merchant_member.go @@ -0,0 +1,134 @@ +package handler + +import ( + "strconv" + + "affiliate_dash/internal/middleware" + "affiliate_dash/internal/pkg/response" + "affiliate_dash/internal/service" + + "github.com/gin-gonic/gin" +) + +func (h *MerchantHandler) ListMembers(c *gin.Context) { + members, err := h.merchantSvc.ListMembers(middleware.GetMerchantID(c)) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, members) +} + +type addMemberReq struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + Password string `json:"password"` + Nickname string `json:"nickname"` + Role string `json:"role" binding:"required"` + IsDefault bool `json:"is_default"` +} + +func (h *MerchantHandler) AddCurrentMerchantMember(c *gin.Context) { + var req addMemberReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:角色必填") + return + } + member, err := h.merchantSvc.AddMember(middleware.GetMerchantID(c), service.AddMemberInput{ + UserID: req.UserID, + Username: req.Username, + Password: req.Password, + Nickname: req.Nickname, + Role: req.Role, + IsDefault: req.IsDefault, + }, middleware.GetUserID(c)) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, member) +} + +type updateMemberReq struct { + Role string `json:"role" binding:"required"` + Status int `json:"status"` + IsDefault bool `json:"is_default"` +} + +func (h *MerchantHandler) UpdateCurrentMerchantMember(c *gin.Context) { + memberID, _ := strconv.ParseUint(c.Param("id"), 10, 64) + var req updateMemberReq + if memberID == 0 || c.ShouldBindJSON(&req) != nil { + response.BadRequest(c, "参数错误:角色必填") + return + } + member, err := h.merchantSvc.UpdateMember(middleware.GetMerchantID(c), uint(memberID), service.UpdateMemberInput{ + Role: req.Role, Status: req.Status, IsDefault: req.IsDefault, + }, middleware.GetUserID(c)) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, member) +} + +func (h *MerchantHandler) RemoveCurrentMerchantMember(c *gin.Context) { + memberID, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if memberID == 0 { + response.BadRequest(c, "参数错误") + return + } + if err := h.merchantSvc.RemoveMember(middleware.GetMerchantID(c), uint(memberID), middleware.GetUserID(c)); err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, nil) +} + +func (h *MerchantHandler) ListRoles(c *gin.Context) { + roles, err := h.merchantSvc.ListRoles(middleware.GetMerchantID(c)) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, roles) +} + +type createMerchantRoleReq struct { + Code string `json:"code" binding:"required"` + Name string `json:"name" binding:"required"` + Permissions []string `json:"permissions"` +} + +func (h *MerchantHandler) CreateRole(c *gin.Context) { + var req createMerchantRoleReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:角色编码和名称必填") + return + } + role, err := h.merchantSvc.CreateRole(middleware.GetMerchantID(c), service.MerchantRoleInput{ + Code: req.Code, Name: req.Name, Permissions: req.Permissions, + }, middleware.GetUserID(c)) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, role) +} + +func (h *MerchantHandler) UpdateRole(c *gin.Context) { + roleID, _ := strconv.ParseUint(c.Param("id"), 10, 64) + var req createMerchantRoleReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:角色名称必填") + return + } + role, err := h.merchantSvc.UpdateRole(middleware.GetMerchantID(c), uint(roleID), service.MerchantRoleInput{ + Name: req.Name, Permissions: req.Permissions, + }, middleware.GetUserID(c)) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, role) +} diff --git a/backend/internal/handler/merchant_order.go b/backend/internal/handler/merchant_order.go new file mode 100644 index 0000000..e7dd175 --- /dev/null +++ b/backend/internal/handler/merchant_order.go @@ -0,0 +1,152 @@ +package handler + +import ( + "strings" + + "affiliate_dash/internal/middleware" + "affiliate_dash/internal/pkg/response" + "affiliate_dash/internal/service" + + "github.com/gin-gonic/gin" +) + +func (h *MerchantHandler) ListOrders(c *gin.Context) { + page, size := pageParams(c) + orderSource := strings.TrimSpace(c.Query("order_source")) + list, total, err := h.fulfillmentSvc.ListOrders(middleware.GetMerchantID(c), page, size, c.Query("order_status"), orderSource, c.Query("keyword")) + if err != nil { + if err.Error() == "无效的订单来源" || err.Error() == "无效的订单状态" { + response.BadRequest(c, err.Error()) + return + } + response.ServerError(c, err.Error()) + return + } + response.Page(c, list, total, page, size) +} + +type merchantManualOrderReq struct { + ClientOrderNo string `json:"client_order_no" binding:"required,max=96"` + SKU string `json:"sku" binding:"required,max=96"` + Quantity int64 `json:"quantity"` + BuyerReference string `json:"buyer_reference" binding:"max=128"` + GameAccount string `json:"game_account" binding:"max=128"` + Note string `json:"note" binding:"max=512"` +} + +// CreateManualOrder 在商户后台创建真实订单;扣款、库存和回调与开放 API 下单保持一致。 +func (h *MerchantHandler) CreateManualOrder(c *gin.Context) { + var req merchantManualOrderReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:商户单号与商品必填") + return + } + data := map[string]string{} + if gameAccount := strings.TrimSpace(req.GameAccount); gameAccount != "" { + data["game_account"] = gameAccount + } + if note := strings.TrimSpace(req.Note); note != "" { + data["manual_note"] = note + } + result, err := h.fulfillmentSvc.CreateManualOrder(service.CreateManualOrderInput{ + MerchantID: middleware.GetMerchantID(c), + ActorUserID: middleware.GetUserID(c), + ClientOrderNo: req.ClientOrderNo, + SKU: req.SKU, + Quantity: req.Quantity, + BuyerReference: req.BuyerReference, + RequestData: data, + }) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + canShip, cannotShipReason := service.CanFulfill(result.Order) + response.OK(c, gin.H{ + "order": result.Order, + "idempotent": result.Idempotent, + "can_ship": canShip, + "cannot_ship_reason": cannotShipReason, + }) +} + +type merchantTestOrderReq struct { + SKU string `json:"sku" binding:"required"` + BuyerReference string `json:"buyer_reference"` + Note string `json:"note"` + OrderStatus string `json:"order_status"` +} + +func (h *MerchantHandler) CreateTestOrder(c *gin.Context) { + var req merchantTestOrderReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:sku 必填") + return + } + order, err := h.fulfillmentSvc.CreateTestOrder(service.CreateTestOrderInput{ + MerchantID: middleware.GetMerchantID(c), + ActorUserID: middleware.GetUserID(c), + SKU: req.SKU, + BuyerReference: req.BuyerReference, + Note: req.Note, + OrderStatus: req.OrderStatus, + }) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + canShip, cannotShipReason := service.CanFulfill(order) + response.OK(c, gin.H{ + "order": order, + "can_ship": canShip, + "cannot_ship_reason": cannotShipReason, + }) +} + +type merchantCancelOrderReq struct { + Reason string `json:"reason" binding:"required,max=512"` +} + +// CancelOrder 商户后台取消订单:全额退还积分并回补库存,取消后订单不可再发货。 +func (h *MerchantHandler) CancelOrder(c *gin.Context) { + var req merchantCancelOrderReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:取消原因必填且最长 512 字") + return + } + order, err := h.fulfillmentSvc.CancelOrderByUser(middleware.GetMerchantID(c), middleware.GetUserID(c), c.Param("order_no"), req.Reason) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, gin.H{ + "order": order, + "refund_amount": order.Amount, + }) +} + +func (h *MerchantHandler) GetDeliveryLink(c *gin.Context) { + link, err := h.deliverySvc.GetOrCreateDeliveryLink(middleware.GetMerchantID(c), c.Param("order_no"), requestBaseURL(c)) + if err != nil { + writeDeliveryError(c, err) + return + } + response.OK(c, link) +} + +func (h *MerchantHandler) RevokeDeliveryLink(c *gin.Context) { + if err := h.deliverySvc.RevokeDeliveryLink(middleware.GetMerchantID(c), middleware.GetUserID(c), c.Param("order_no")); err != nil { + writeDeliveryError(c, err) + return + } + response.OK(c, nil) +} + +func (h *MerchantHandler) RestoreDeliveryLink(c *gin.Context) { + link, err := h.deliverySvc.RestoreDeliveryLink(middleware.GetMerchantID(c), middleware.GetUserID(c), c.Param("order_no"), requestBaseURL(c)) + if err != nil { + writeDeliveryError(c, err) + return + } + response.OK(c, link) +} diff --git a/backend/internal/handler/merchant_platform.go b/backend/internal/handler/merchant_platform.go new file mode 100644 index 0000000..634495d --- /dev/null +++ b/backend/internal/handler/merchant_platform.go @@ -0,0 +1,169 @@ +package handler + +import ( + "strconv" + + "affiliate_dash/internal/middleware" + "affiliate_dash/internal/pkg/response" + "affiliate_dash/internal/service" + + "github.com/gin-gonic/gin" +) + +func (h *MerchantHandler) ListPlatformMerchants(c *gin.Context) { + page, size := pageParams(c) + list, total, err := h.merchantSvc.ListMerchants(page, size) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.Page(c, list, total, page, size) +} + +type createMerchantReq struct { + Code string `json:"code" binding:"required"` + Name string `json:"name" binding:"required"` + ContactName string `json:"contact_name"` + ContactInfo string `json:"contact_info"` + OwnerUserID uint `json:"owner_user_id"` + OwnerUsername string `json:"owner_username"` + OwnerPassword string `json:"owner_password"` + OwnerNickname string `json:"owner_nickname"` + Features string `json:"features"` + FeeType string `json:"fee_type"` + FeeRateBP int64 `json:"fee_rate_bp"` + FeeFixedAmount int64 `json:"fee_fixed_amount"` +} + +func (h *MerchantHandler) CreateMerchant(c *gin.Context) { + var req createMerchantReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:code、name 必填") + return + } + merchant, err := h.merchantSvc.CreateMerchant(service.CreateMerchantInput{ + Code: req.Code, + Name: req.Name, + ContactName: req.ContactName, + ContactInfo: req.ContactInfo, + OwnerUserID: req.OwnerUserID, + OwnerUsername: req.OwnerUsername, + OwnerPassword: req.OwnerPassword, + OwnerNickname: req.OwnerNickname, + Features: req.Features, + FeeType: req.FeeType, + FeeRateBP: req.FeeRateBP, + FeeFixedAmount: req.FeeFixedAmount, + }, middleware.GetUserID(c)) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, merchant) +} + +type updateMerchantSettingsReq struct { + Name *string `json:"name"` + Status *string `json:"status"` + ContactName *string `json:"contact_name"` + ContactInfo *string `json:"contact_info"` + Features *string `json:"features"` + FeeType *string `json:"fee_type"` + FeeRateBP *int64 `json:"fee_rate_bp"` + FeeFixedAmount *int64 `json:"fee_fixed_amount"` +} + +func (h *MerchantHandler) UpdateMerchantSettings(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + var req updateMerchantSettingsReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误") + return + } + if err := h.merchantSvc.UpdateMerchantSettings(uint(id), service.UpdateMerchantSettingsInput{ + Name: req.Name, + Status: req.Status, + ContactName: req.ContactName, + ContactInfo: req.ContactInfo, + Features: req.Features, + FeeType: req.FeeType, + FeeRateBP: req.FeeRateBP, + FeeFixedAmount: req.FeeFixedAmount, + }, middleware.GetUserID(c)); err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, nil) +} + +func (h *MerchantHandler) AddPlatformMerchantMember(c *gin.Context) { + var req addMemberReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:user_id 与 role 必填") + return + } + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + member, err := h.merchantSvc.AddMember(uint(id), service.AddMemberInput{ + UserID: req.UserID, + Username: req.Username, + Password: req.Password, + Nickname: req.Nickname, + Role: req.Role, + IsDefault: req.IsDefault, + }, middleware.GetUserID(c)) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, member) +} + +// AdminGetMerchantAccount 平台管理员查看指定商户的负责人登录账号。 +func (h *MerchantHandler) AdminGetMerchantAccount(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if id == 0 { + response.BadRequest(c, "商户 ID 无效") + return + } + accounts, err := h.merchantSvc.ListOwnerAccounts(uint(id)) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, accounts) +} + +type adminUpdateMerchantAccountReq struct { + UserID uint `json:"user_id" binding:"required"` + Username string `json:"username" binding:"required,min=3,max=64"` + Nickname string `json:"nickname" binding:"max=64"` + Password string `json:"password" binding:"omitempty,min=6,max=128"` + Status *int `json:"status"` +} + +// AdminUpdateMerchantAccount 平台管理员维护指定商户的负责人登录账号: +// 修改用户名/昵称、重置密码、启用停用;密码留空表示不重置。 +func (h *MerchantHandler) AdminUpdateMerchantAccount(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + var req adminUpdateMerchantAccountReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:用户名必填(3-64 位),密码至少 6 位") + return + } + if id == 0 { + response.BadRequest(c, "商户 ID 无效") + return + } + account, err := h.merchantSvc.UpdateMerchantOwnerAccount(uint(id), service.UpdateMerchantOwnerAccountInput{ + UserID: req.UserID, + Username: req.Username, + Nickname: req.Nickname, + Password: req.Password, + Status: req.Status, + }, middleware.GetUserID(c)) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, account) +} diff --git a/backend/internal/handler/merchant_product.go b/backend/internal/handler/merchant_product.go new file mode 100644 index 0000000..57737ff --- /dev/null +++ b/backend/internal/handler/merchant_product.go @@ -0,0 +1,182 @@ +package handler + +import ( + "strconv" + "strings" + + "affiliate_dash/internal/middleware" + "affiliate_dash/internal/model" + "affiliate_dash/internal/pkg/response" + "affiliate_dash/internal/service" + + "github.com/gin-gonic/gin" +) + +func (h *MerchantHandler) ListProducts(c *gin.Context) { + page, size := pageParams(c) + list, total, err := h.merchantSvc.ListMerchantProductsWithOptions(middleware.GetMerchantID(c), page, size, service.ListMerchantProductsOptions{ + Status: strings.TrimSpace(c.Query("status")), + Keyword: strings.TrimSpace(c.Query("keyword")), + }) + if err != nil { + if err.Error() == "无效的商品状态" { + response.BadRequest(c, err.Error()) + return + } + response.ServerError(c, err.Error()) + return + } + response.Page(c, list, total, page, size) +} + +type merchantProductStatusReq struct { + Status string `json:"status" binding:"required"` +} + +// UpdateProductStatus PATCH /api/merchant/products/:id/status +// 商户仅能上架/下架商品,价格、成本等配置由平台管理员维护。 +// UpdateProduct PATCH /api/merchant/products/:id — 编辑商品(显示名称、售价、成本、库存、状态)。 +// 严格风控门禁:仅平台超级管理员(model.RoleAdmin)拥有修改商品价格、成本、库存与名称的权限,普通分销商严禁修改。 +func (h *MerchantHandler) UpdateProduct(c *gin.Context) { + if middleware.GetRole(c) != model.RoleAdmin { + response.Forbidden(c, "权限不足:仅平台超级管理员可编辑商品价格、成本与库存") + return + } + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if id == 0 { + response.BadRequest(c, "参数错误") + return + } + var req merchantProductUpdateReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误") + return + } + if err := h.merchantSvc.UpdateMerchantProduct(middleware.GetMerchantID(c), uint(id), service.UpdateMerchantProductInput{ + DisplayName: req.DisplayName, + PriceAmount: req.PriceAmount, + CostAmount: req.CostAmount, + Stock: req.Stock, + Status: req.Status, + FulfillmentConfig: req.FulfillmentConfig, + }, middleware.GetUserID(c)); err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, nil) +} + +func (h *MerchantHandler) UpdateProductStatus(c *gin.Context) { + var req merchantProductStatusReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:status 必填") + return + } + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if err := h.merchantSvc.UpdateMerchantProduct(middleware.GetMerchantID(c), uint(id), service.UpdateMerchantProductInput{ + Status: &req.Status, + }, middleware.GetUserID(c)); err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, nil) +} + +// DeleteProduct DELETE /api/merchant/products/:id — 仅已下架可删(软删除)。 +func (h *MerchantHandler) DeleteProduct(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if err := h.merchantSvc.DeleteMerchantProduct(middleware.GetMerchantID(c), uint(id), middleware.GetUserID(c)); err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, nil) +} + +// ListManualOrderProducts returns active products for order creation without granting product-management access. +func (h *MerchantHandler) ListManualOrderProducts(c *gin.Context) { + list, _, err := h.merchantSvc.ListMerchantProductsForClient(middleware.GetMerchantID(c), 1, 100) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, list) +} + +// ListProductCatalog 返回自营商户的全部可售商品,作为平台默认商品目录供分配。 +func (h *MerchantHandler) ListProductCatalog(c *gin.Context) { + list, err := h.merchantSvc.ListProductCatalog() + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, list) +} + +// ListMerchantProductsByAdmin 供平台管理员查看指定商户的可售商品。 +func (h *MerchantHandler) ListMerchantProductsByAdmin(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + list, err := h.merchantSvc.ListMerchantProductsByAdmin(uint(id)) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, list) +} + +type merchantProductUpdateReq struct { + DisplayName *string `json:"display_name"` + PriceAmount *int64 `json:"price_amount"` + CostAmount *int64 `json:"cost_amount"` + Stock *int64 `json:"stock"` + Status *string `json:"status"` + FulfillmentConfig *string `json:"fulfillment_config"` +} + +// AdminUpdateMerchantProduct 供平台管理员编辑指定商户的商品(价格、成本、库存、状态等)。 +func (h *MerchantHandler) AdminUpdateMerchantProduct(c *gin.Context) { + merchantID, _ := strconv.ParseUint(c.Param("id"), 10, 64) + productID, _ := strconv.ParseUint(c.Param("pid"), 10, 64) + if merchantID == 0 || productID == 0 { + response.BadRequest(c, "参数错误") + return + } + var req merchantProductUpdateReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误") + return + } + if err := h.merchantSvc.UpdateMerchantProduct(uint(merchantID), uint(productID), service.UpdateMerchantProductInput{ + DisplayName: req.DisplayName, + PriceAmount: req.PriceAmount, + CostAmount: req.CostAmount, + Stock: req.Stock, + Status: req.Status, + FulfillmentConfig: req.FulfillmentConfig, + }, middleware.GetUserID(c)); err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, nil) +} + +type assignProductsReq struct { + CatalogIDs []uint `json:"catalog_ids"` +} + +// AssignProducts 按商品目录 ID 批量同步商户的可售商品。 +func (h *MerchantHandler) AssignProducts(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + var req assignProductsReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误") + return + } + count, err := h.merchantSvc.AssignProducts(uint(id), service.AssignProductsInput{ + CatalogIDs: req.CatalogIDs, + }, middleware.GetUserID(c)) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, gin.H{"assigned": count}) +} diff --git a/backend/internal/handler/merchant_wallet.go b/backend/internal/handler/merchant_wallet.go new file mode 100644 index 0000000..c35d123 --- /dev/null +++ b/backend/internal/handler/merchant_wallet.go @@ -0,0 +1,96 @@ +package handler + +import ( + "strconv" + + "affiliate_dash/internal/middleware" + "affiliate_dash/internal/pkg/response" + "affiliate_dash/internal/service" + + "github.com/gin-gonic/gin" +) + +func (h *MerchantHandler) GetWallet(c *gin.Context) { + wallet, err := h.fulfillmentSvc.GetWallet(middleware.GetMerchantID(c)) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, wallet) +} + +func (h *MerchantHandler) ListWalletLedger(c *gin.Context) { + page, size := pageParams(c) + referenceNo := c.Query("reference_no") + entryType := c.Query("type") + list, total, err := h.fulfillmentSvc.ListWalletLedger(middleware.GetMerchantID(c), page, size, referenceNo, entryType) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.Page(c, list, total, page, size) +} + +// AdminGetWallet 供平台管理员查看指定商户的钱包。 +func (h *MerchantHandler) AdminGetWallet(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if id == 0 { + response.BadRequest(c, "商户 ID 无效") + return + } + wallet, err := h.fulfillmentSvc.GetWallet(uint(id)) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, wallet) +} + +// AdminListWalletLedger 供平台管理员查看指定商户的积分流水。 +func (h *MerchantHandler) AdminListWalletLedger(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if id == 0 { + response.BadRequest(c, "商户 ID 无效") + return + } + page, size := pageParams(c) + list, total, err := h.fulfillmentSvc.ListWalletLedger(uint(id), page, size, c.Query("reference_no"), c.Query("type")) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.Page(c, list, total, page, size) +} + +type walletAdjustReq struct { + Amount int64 `json:"amount" binding:"required"` + IdempotencyKey string `json:"idempotency_key" binding:"required"` + Note string `json:"note"` +} + +// AdminAdjustWallet 供平台管理员手动调整指定商户的积分,商户侧无任何调账入口。 +func (h *MerchantHandler) AdminAdjustWallet(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if id == 0 { + response.BadRequest(c, "商户 ID 无效") + return + } + var req walletAdjustReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误:amount 与 idempotency_key 必填") + return + } + wallet, err := h.fulfillmentSvc.AdjustWallet(service.WalletAdjustInput{ + MerchantID: uint(id), + ActorUserID: middleware.GetUserID(c), + Amount: req.Amount, + IdempotencyKey: req.IdempotencyKey, + Note: req.Note, + }) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + h.rechargeSvc.CheckLowBalanceAndNotify(uint(id)) + response.OK(c, wallet) +}