feat(distribution): 完善分销收号资产双视图、商户提现、自主加价、品牌装修与数据大盘
This commit is contained in:
@@ -10,6 +10,11 @@ type DistributionSite struct {
|
||||
Domain string `gorm:"size:255;not null;uniqueIndex" json:"domain"`
|
||||
Status string `gorm:"size:16;not null;default:'active';index" json:"status"`
|
||||
DisplayAccountEnabled bool `gorm:"not null;default:true" json:"display_account_enabled"`
|
||||
LogoURL string `gorm:"size:512;not null;default:''" json:"logo_url"`
|
||||
FaviconURL string `gorm:"size:512;not null;default:''" json:"favicon_url"`
|
||||
BannersJSON string `gorm:"type:text" json:"banners_json"`
|
||||
AnnouncementsJSON string `gorm:"type:text" json:"announcements_json"`
|
||||
FooterInfoJSON string `gorm:"type:text" json:"footer_info_json"`
|
||||
DefaultSupportGroupID *uint64 `gorm:"index" json:"default_support_group_id,omitempty"`
|
||||
TaxEnabled bool `gorm:"not null;default:false" json:"tax_enabled"`
|
||||
TaxRateBPS int64 `gorm:"not null;default:100" json:"tax_rate_bps"`
|
||||
@@ -40,6 +45,7 @@ type DistributionSiteAccount struct {
|
||||
AccountID uint64 `gorm:"not null;uniqueIndex:uk_distribution_site_account;index" json:"account_id"`
|
||||
ListingID uint64 `gorm:"not null;index" json:"listing_id"`
|
||||
DisplayStatus string `gorm:"size:16;not null;default:'enabled';index" json:"display_status"`
|
||||
MarkupAmountCent int64 `gorm:"not null;default:0" json:"markup_amount_cent"`
|
||||
DisplayStartedAt *time.Time `json:"display_started_at,omitempty"`
|
||||
DisplayEndedAt *time.Time `json:"display_ended_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
@@ -9,6 +9,11 @@ type SiteDTO struct {
|
||||
Domain string `json:"domain"`
|
||||
Status string `json:"status"`
|
||||
DisplayAccountEnabled bool `json:"display_account_enabled"`
|
||||
LogoURL string `json:"logo_url"`
|
||||
FaviconURL string `json:"favicon_url"`
|
||||
BannersJSON string `json:"banners_json"`
|
||||
AnnouncementsJSON string `json:"announcements_json"`
|
||||
FooterInfoJSON string `json:"footer_info_json"`
|
||||
DefaultSupportGroupID *uint64 `json:"default_support_group_id,omitempty"`
|
||||
TaxEnabled bool `json:"tax_enabled"`
|
||||
TaxRateBPS int64 `json:"tax_rate_bps"`
|
||||
@@ -34,6 +39,11 @@ type UpdateSiteRequest struct {
|
||||
DefaultSupportGroupID *uint64 `json:"default_support_group_id"`
|
||||
TaxEnabled *bool `json:"tax_enabled"`
|
||||
TaxRateBPS *int64 `json:"tax_rate_bps"`
|
||||
LogoURL *string `json:"logo_url"`
|
||||
FaviconURL *string `json:"favicon_url"`
|
||||
BannersJSON *string `json:"banners_json"`
|
||||
AnnouncementsJSON *string `json:"announcements_json"`
|
||||
FooterInfoJSON *string `json:"footer_info_json"`
|
||||
}
|
||||
|
||||
type RuleDTO struct {
|
||||
@@ -126,6 +136,7 @@ type AccountDisplayDTO struct {
|
||||
AccountID uint64 `json:"account_id"`
|
||||
ListingID uint64 `json:"listing_id"`
|
||||
DisplayStatus string `json:"display_status"`
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
ListingNo string `json:"listing_no,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
GameName string `json:"game_name,omitempty"`
|
||||
@@ -157,6 +168,7 @@ type PublicDistributionListingDTO struct {
|
||||
HafCoinAmount int64 `json:"haf_coin_amount"`
|
||||
PriceCent int64 `json:"price_cent"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
InTransaction bool `json:"in_transaction"`
|
||||
SiteCode string `json:"site_code"`
|
||||
SiteName string `json:"site_name"`
|
||||
@@ -175,6 +187,11 @@ type PublicSiteInfoDTO struct {
|
||||
Domain string `json:"domain"`
|
||||
Status string `json:"status"`
|
||||
DisplayAccountEnabled bool `json:"display_account_enabled"`
|
||||
LogoURL string `json:"logo_url"`
|
||||
FaviconURL string `json:"favicon_url"`
|
||||
BannersJSON string `json:"banners_json"`
|
||||
AnnouncementsJSON string `json:"announcements_json"`
|
||||
FooterInfoJSON string `json:"footer_info_json"`
|
||||
DefaultSupportGroupID *uint64 `json:"default_support_group_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -283,3 +300,135 @@ type OnboardSiteResponse struct {
|
||||
Member DistributionCurrentMemberDTO `json:"member"`
|
||||
}
|
||||
|
||||
// ReceivedListingDTO 是分销站点收到的账号资产信息
|
||||
type ReceivedListingDTO struct {
|
||||
ListingID uint64 `json:"listing_id"`
|
||||
ListingNo string `json:"listing_no"`
|
||||
SiteID uint64 `json:"site_id"`
|
||||
AccountID uint64 `json:"account_id"`
|
||||
OwnerID uint64 `json:"owner_id"`
|
||||
OwnerPhone string `json:"owner_phone"`
|
||||
OwnerNickname string `json:"owner_nickname"`
|
||||
Title string `json:"title"`
|
||||
GameName string `json:"game_name"`
|
||||
ServerRegion string `json:"server_region"`
|
||||
LoginPlatform string `json:"login_platform"`
|
||||
RankLevel string `json:"rank_level"`
|
||||
HafCoinAmount int64 `json:"haf_coin_amount"`
|
||||
PriceCent int64 `json:"price_cent"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
||||
Status string `json:"status"`
|
||||
ReviewStatus string `json:"review_status"`
|
||||
ReviewReason string `json:"review_reason"`
|
||||
PublishedAt *time.Time `json:"published_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type ReceivedListingQuery struct {
|
||||
SiteID uint64
|
||||
Status string
|
||||
ReviewStatus string
|
||||
GameName string
|
||||
Keyword string
|
||||
Page int
|
||||
PageSize int
|
||||
Size int
|
||||
}
|
||||
|
||||
type ReceivedListingPage struct {
|
||||
Items []ReceivedListingDTO `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type UpdateReceivedListingStatusRequest struct {
|
||||
Status string `json:"status" binding:"required,oneof=published offline"`
|
||||
}
|
||||
|
||||
// 站点品牌与装修 DTO
|
||||
type UpdateSiteBrandingRequest struct {
|
||||
LogoURL string `json:"logo_url"`
|
||||
FaviconURL string `json:"favicon_url"`
|
||||
BannersJSON string `json:"banners_json"`
|
||||
AnnouncementsJSON string `json:"announcements_json"`
|
||||
FooterInfoJSON string `json:"footer_info_json"`
|
||||
}
|
||||
|
||||
// 自主加价 DTO
|
||||
type UpdateAccountMarkupRequest struct {
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
}
|
||||
|
||||
type BatchUpdateAccountMarkupRequest struct {
|
||||
ListingIDs []uint64 `json:"listing_ids" binding:"required"`
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
}
|
||||
|
||||
// 商户钱包与提现 DTO
|
||||
type SiteWalletDTO struct {
|
||||
SiteID uint64 `json:"site_id"`
|
||||
AvailableBalanceCent int64 `json:"available_balance_cent"`
|
||||
FrozenBalanceCent int64 `json:"frozen_balance_cent"`
|
||||
TotalSettledCent int64 `json:"total_settled_cent"`
|
||||
TotalWithdrawnCent int64 `json:"total_withdrawn_cent"`
|
||||
}
|
||||
|
||||
type ApplySiteWithdrawalRequest struct {
|
||||
SiteID uint64 `json:"site_id" binding:"required"`
|
||||
AmountCent int64 `json:"amount_cent" binding:"required,gt=0"`
|
||||
AccountType string `json:"account_type" binding:"required,oneof=alipay bank"`
|
||||
AccountName string `json:"account_name" binding:"required"`
|
||||
AccountNo string `json:"account_no" binding:"required"`
|
||||
BankName string `json:"bank_name"`
|
||||
BankBranch string `json:"bank_branch"`
|
||||
}
|
||||
|
||||
type SiteWithdrawalDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
WithdrawNo string `json:"withdraw_no"`
|
||||
SiteID uint64 `json:"site_id"`
|
||||
AmountCent int64 `json:"amount_cent"`
|
||||
FeeCent int64 `json:"fee_cent"`
|
||||
ActualAmountCent int64 `json:"actual_amount_cent"`
|
||||
AccountType string `json:"account_type"`
|
||||
AccountName string `json:"account_name"`
|
||||
AccountNo string `json:"account_no"`
|
||||
BankName string `json:"bank_name"`
|
||||
BankBranch string `json:"bank_branch"`
|
||||
Status string `json:"status"`
|
||||
ReviewRemark string `json:"review_remark"`
|
||||
PaymentProofURL string `json:"payment_proof_url"`
|
||||
PaymentRemark string `json:"payment_remark"`
|
||||
PaidAt *time.Time `json:"paid_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type SiteWithdrawalPage struct {
|
||||
Items []SiteWithdrawalDTO `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// 运营大盘统计 DTO
|
||||
type DistributionAnalyticsDTO struct {
|
||||
TodayReceivedCount int64 `json:"today_received_count"`
|
||||
TotalReceivedCount int64 `json:"total_received_count"`
|
||||
ActiveListingCount int64 `json:"active_listing_count"`
|
||||
TodayOrderCount int64 `json:"today_order_count"`
|
||||
TodayGMVCent int64 `json:"today_gmv_cent"`
|
||||
TodayProfitCent int64 `json:"today_profit_cent"`
|
||||
MonthProfitCent int64 `json:"month_profit_cent"`
|
||||
AvailableBalanceCent int64 `json:"available_balance_cent"`
|
||||
TrendDays []DistributionTrendPoint `json:"trend_days"`
|
||||
}
|
||||
|
||||
type DistributionTrendPoint struct {
|
||||
Date string `json:"date"`
|
||||
ReceivedCount int64 `json:"received_count"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
GMVCent int64 `json:"gmv_cent"`
|
||||
ProfitCent int64 `json:"profit_cent"`
|
||||
}
|
||||
|
||||
|
||||
@@ -486,6 +486,219 @@ func (h *Handler) OnboardSite(c *gin.Context) {
|
||||
response.Created(c, resp)
|
||||
}
|
||||
|
||||
func (h *Handler) resolveSiteID(c *gin.Context) (uint64, error) {
|
||||
adminID, _ := currentAdminID(c)
|
||||
var requested uint64
|
||||
if raw := c.Param("id"); raw != "" {
|
||||
if id, err := strconv.ParseUint(raw, 10, 64); err == nil && id > 0 {
|
||||
requested = id
|
||||
}
|
||||
}
|
||||
if requested == 0 {
|
||||
if q := parseQueryUint(c, "site_id"); q > 0 {
|
||||
requested = uint64(q)
|
||||
}
|
||||
}
|
||||
return h.service.ScopeSiteID(c.Request.Context(), adminID, requested)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateSiteBranding(c *gin.Context) {
|
||||
siteID, err := h.resolveSiteID(c)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
if siteID == 0 {
|
||||
response.BadRequest(c, "未指定分销站点")
|
||||
return
|
||||
}
|
||||
var req UpdateSiteBrandingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "品牌装修参数格式不正确")
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
dto, err := h.service.UpdateSiteBranding(c.Request.Context(), adminID, siteID, req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, dto)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateAccountMarkup(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req UpdateAccountMarkupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "加价参数格式不正确")
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
dto, err := h.service.UpdateAccountMarkup(c.Request.Context(), adminID, id, req.MarkupAmountCent)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, dto)
|
||||
}
|
||||
|
||||
func (h *Handler) BatchUpdateAccountMarkup(c *gin.Context) {
|
||||
siteID, err := h.resolveSiteID(c)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
var req BatchUpdateAccountMarkupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "批量加价参数格式不正确")
|
||||
return
|
||||
}
|
||||
if siteID == 0 {
|
||||
response.BadRequest(c, "未指定分销站点")
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
if err := h.service.BatchUpdateAccountMarkup(c.Request.Context(), adminID, siteID, req); err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func (h *Handler) ListReceivedListings(c *gin.Context) {
|
||||
siteID, err := h.resolveSiteID(c)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
if siteID == 0 {
|
||||
response.BadRequest(c, "未指定分销站点")
|
||||
return
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
q := ReceivedListingQuery{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Status: c.Query("status"),
|
||||
ReviewStatus: c.Query("review_status"),
|
||||
GameName: c.Query("game_name"),
|
||||
Keyword: c.Query("keyword"),
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
resp, err := h.service.ListReceivedListings(c.Request.Context(), adminID, siteID, q)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, resp)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateReceivedListingStatus(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req UpdateReceivedListingStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "状态参数格式不正确")
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
if err := h.service.UpdateReceivedListingStatus(c.Request.Context(), adminID, id, req.Status); err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func (h *Handler) GetSiteWallet(c *gin.Context) {
|
||||
siteID, err := h.resolveSiteID(c)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
if siteID == 0 {
|
||||
response.BadRequest(c, "未指定分销站点")
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
walletDTO, err := h.service.GetSiteWallet(c.Request.Context(), adminID, siteID)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, walletDTO)
|
||||
}
|
||||
|
||||
func (h *Handler) ApplySiteWithdrawal(c *gin.Context) {
|
||||
siteID, err := h.resolveSiteID(c)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
if siteID == 0 {
|
||||
response.BadRequest(c, "未指定分销站点")
|
||||
return
|
||||
}
|
||||
var req ApplySiteWithdrawalRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "提现申请参数格式不正确")
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
dto, err := h.service.ApplySiteWithdrawal(c.Request.Context(), adminID, siteID, req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.Created(c, dto)
|
||||
}
|
||||
|
||||
func (h *Handler) ListSiteWithdrawals(c *gin.Context) {
|
||||
siteID, err := h.resolveSiteID(c)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
if siteID == 0 {
|
||||
response.BadRequest(c, "未指定分销站点")
|
||||
return
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
adminID, _ := currentAdminID(c)
|
||||
pageResult, err := h.service.ListSiteWithdrawals(c.Request.Context(), adminID, siteID, page, pageSize)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, pageResult)
|
||||
}
|
||||
|
||||
func (h *Handler) GetDashboardAnalytics(c *gin.Context) {
|
||||
siteID, err := h.resolveSiteID(c)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
if siteID == 0 {
|
||||
response.BadRequest(c, "未指定分销站点")
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
analytics, err := h.service.GetDashboardAnalytics(c.Request.Context(), adminID, siteID)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, analytics)
|
||||
}
|
||||
|
||||
|
||||
func isHTTPSRequest(c *gin.Context) bool {
|
||||
if c.Request.TLS != nil {
|
||||
return true
|
||||
|
||||
@@ -2,6 +2,8 @@ package distribution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -10,6 +12,7 @@ import (
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -187,6 +190,21 @@ func (r *Repository) UpdateSite(ctx context.Context, adminID, id uint64, req Upd
|
||||
if req.TaxRateBPS != nil {
|
||||
row.TaxRateBPS = *req.TaxRateBPS
|
||||
}
|
||||
if req.LogoURL != nil {
|
||||
row.LogoURL = strings.TrimSpace(*req.LogoURL)
|
||||
}
|
||||
if req.FaviconURL != nil {
|
||||
row.FaviconURL = strings.TrimSpace(*req.FaviconURL)
|
||||
}
|
||||
if req.BannersJSON != nil {
|
||||
row.BannersJSON = *req.BannersJSON
|
||||
}
|
||||
if req.AnnouncementsJSON != nil {
|
||||
row.AnnouncementsJSON = *req.AnnouncementsJSON
|
||||
}
|
||||
if req.FooterInfoJSON != nil {
|
||||
row.FooterInfoJSON = *req.FooterInfoJSON
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Save(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -284,7 +302,24 @@ func (r *Repository) UpdateRuleStatus(ctx context.Context, adminID, id uint64, s
|
||||
}
|
||||
|
||||
func siteDTO(row model.DistributionSite) SiteDTO {
|
||||
return SiteDTO{ID: row.ID, SiteCode: row.SiteCode, SiteName: row.SiteName, Domain: row.Domain, Status: row.Status, DisplayAccountEnabled: row.DisplayAccountEnabled, DefaultSupportGroupID: row.DefaultSupportGroupID, TaxEnabled: row.TaxEnabled, TaxRateBPS: row.TaxRateBPS, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}
|
||||
return SiteDTO{
|
||||
ID: row.ID,
|
||||
SiteCode: row.SiteCode,
|
||||
SiteName: row.SiteName,
|
||||
Domain: row.Domain,
|
||||
Status: row.Status,
|
||||
DisplayAccountEnabled: row.DisplayAccountEnabled,
|
||||
DefaultSupportGroupID: row.DefaultSupportGroupID,
|
||||
TaxEnabled: row.TaxEnabled,
|
||||
TaxRateBPS: row.TaxRateBPS,
|
||||
LogoURL: row.LogoURL,
|
||||
FaviconURL: row.FaviconURL,
|
||||
BannersJSON: row.BannersJSON,
|
||||
AnnouncementsJSON: row.AnnouncementsJSON,
|
||||
FooterInfoJSON: row.FooterInfoJSON,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ruleDTO(row model.DistributionRuleVersion) RuleDTO {
|
||||
@@ -352,6 +387,7 @@ func (r *Repository) ListAccountDisplays(ctx context.Context, siteID uint64, sta
|
||||
AccountID uint64 `gorm:"column:account_id"`
|
||||
ListingID uint64 `gorm:"column:listing_id"`
|
||||
DisplayStatus string `gorm:"column:display_status"`
|
||||
MarkupAmountCent int64 `gorm:"column:markup_amount_cent"`
|
||||
ListingNo string `gorm:"column:listing_no"`
|
||||
Title string `gorm:"column:title"`
|
||||
GameName string `gorm:"column:game_name"`
|
||||
@@ -374,6 +410,7 @@ func (r *Repository) ListAccountDisplays(ctx context.Context, siteID uint64, sta
|
||||
dsa.account_id,
|
||||
dsa.listing_id,
|
||||
dsa.display_status,
|
||||
COALESCE(dsa.markup_amount_cent, 0) AS markup_amount_cent,
|
||||
COALESCE(l.listing_no, '') AS listing_no,
|
||||
COALESCE(ga.title, '') AS title,
|
||||
COALESCE(ga.game_name, '') AS game_name,
|
||||
@@ -404,6 +441,7 @@ func (r *Repository) ListAccountDisplays(ctx context.Context, siteID uint64, sta
|
||||
AccountID: row.AccountID,
|
||||
ListingID: row.ListingID,
|
||||
DisplayStatus: row.DisplayStatus,
|
||||
MarkupAmountCent: row.MarkupAmountCent,
|
||||
ListingNo: row.ListingNo,
|
||||
Title: row.Title,
|
||||
GameName: row.GameName,
|
||||
@@ -678,6 +716,18 @@ func (r *Repository) MarkSettlementBatch(ctx context.Context, adminID, id uint64
|
||||
Updates(map[string]any{"settlement_status": "settled", "settled_at": now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := wallet.AppendEntries(tx, wallet.Entry{
|
||||
UserID: 0,
|
||||
DistributionSiteID: row.SiteID,
|
||||
Direction: "in",
|
||||
AmountCent: row.TotalAmountCent,
|
||||
BalanceType: "available",
|
||||
BizType: "distribution_settlement",
|
||||
BizNo: row.BatchNo,
|
||||
Remark: fmt.Sprintf("分销结算批次入账 %s", row.BatchNo),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
row.Status = "settled"
|
||||
row.SettledAt = &now
|
||||
return tx.Save(&row).Error
|
||||
@@ -1113,6 +1163,7 @@ func (r *Repository) ListPublicSiteListings(ctx context.Context, siteCode string
|
||||
HafCoinAmount int64 `gorm:"column:haf_coin_amount"`
|
||||
PriceCent int64 `gorm:"column:price_cent"`
|
||||
DepositAmountCent int64 `gorm:"column:deposit_amount_cent"`
|
||||
MarkupAmountCent int64 `gorm:"column:markup_amount_cent"`
|
||||
InTransaction bool `gorm:"column:in_transaction"`
|
||||
}
|
||||
|
||||
@@ -1139,8 +1190,9 @@ func (r *Repository) ListPublicSiteListings(ctx context.Context, siteCode string
|
||||
COALESCE(ga.login_platform, '') AS login_platform,
|
||||
COALESCE(ga.rank_level, '') AS rank_level,
|
||||
COALESCE(ga.haf_coin_amount, 0) AS haf_coin_amount,
|
||||
COALESCE(l.price_cent, 0) AS price_cent,
|
||||
COALESCE(l.price_cent, 0) + COALESCE(dsa.markup_amount_cent, 0) AS price_cent,
|
||||
COALESCE(l.deposit_amount_cent, 0) AS deposit_amount_cent,
|
||||
COALESCE(dsa.markup_amount_cent, 0) AS markup_amount_cent,
|
||||
COALESCE(l.in_transaction, 0) AS in_transaction
|
||||
`).
|
||||
Order("dsa.id DESC").
|
||||
@@ -1165,6 +1217,7 @@ func (r *Repository) ListPublicSiteListings(ctx context.Context, siteCode string
|
||||
HafCoinAmount: row.HafCoinAmount,
|
||||
PriceCent: row.PriceCent,
|
||||
DepositAmountCent: row.DepositAmountCent,
|
||||
MarkupAmountCent: row.MarkupAmountCent,
|
||||
InTransaction: row.InTransaction,
|
||||
SiteCode: site.SiteCode,
|
||||
SiteName: site.SiteName,
|
||||
@@ -1211,5 +1264,557 @@ func (r *Repository) GetPublicSiteInfo(ctx context.Context, code, domain string)
|
||||
Status: site.Status,
|
||||
DisplayAccountEnabled: site.DisplayAccountEnabled,
|
||||
DefaultSupportGroupID: site.DefaultSupportGroupID,
|
||||
LogoURL: site.LogoURL,
|
||||
FaviconURL: site.FaviconURL,
|
||||
BannersJSON: site.BannersJSON,
|
||||
AnnouncementsJSON: site.AnnouncementsJSON,
|
||||
FooterInfoJSON: site.FooterInfoJSON,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateWithdrawNo() (string, error) {
|
||||
buf := make([]byte, 8)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("DW%d%s", time.Now().Unix(), hex.EncodeToString(buf)[:8]), nil
|
||||
}
|
||||
|
||||
func maskAccountNo(accountNo, accountType string) string {
|
||||
length := len(accountNo)
|
||||
if length <= 4 {
|
||||
return accountNo
|
||||
}
|
||||
switch accountType {
|
||||
case "alipay", "wechat":
|
||||
if length == 11 {
|
||||
return accountNo[:3] + "****" + accountNo[7:]
|
||||
}
|
||||
return accountNo[:2] + "****" + accountNo[length-2:]
|
||||
case "bank":
|
||||
if length > 8 {
|
||||
return accountNo[:4] + "****" + accountNo[length-4:]
|
||||
}
|
||||
return accountNo[:2] + "****" + accountNo[length-2:]
|
||||
}
|
||||
return accountNo
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateSiteBranding(ctx context.Context, adminID, siteID uint64, req UpdateSiteBrandingRequest) (*SiteDTO, error) {
|
||||
if siteID == 0 {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var site model.DistributionSite
|
||||
if err := r.db.WithContext(ctx).First(&site, siteID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
site.LogoURL = strings.TrimSpace(req.LogoURL)
|
||||
site.FaviconURL = strings.TrimSpace(req.FaviconURL)
|
||||
site.BannersJSON = strings.TrimSpace(req.BannersJSON)
|
||||
site.AnnouncementsJSON = strings.TrimSpace(req.AnnouncementsJSON)
|
||||
site.FooterInfoJSON = strings.TrimSpace(req.FooterInfoJSON)
|
||||
if err := r.db.WithContext(ctx).Model(&site).Updates(map[string]any{
|
||||
"logo_url": site.LogoURL,
|
||||
"favicon_url": site.FaviconURL,
|
||||
"banners_json": site.BannersJSON,
|
||||
"announcements_json": site.AnnouncementsJSON,
|
||||
"footer_info_json": site.FooterInfoJSON,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := siteDTO(site)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateAccountMarkup(ctx context.Context, adminID, id uint64, markupCent int64) (*AccountDisplayDTO, error) {
|
||||
if markupCent < 0 {
|
||||
return nil, ErrInvalidAmount
|
||||
}
|
||||
var row model.DistributionSiteAccount
|
||||
if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrAccountDisplayNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, row.SiteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Model(&row).Update("markup_amount_cent", markupCent).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
displays, err := r.ListAccountDisplays(ctx, row.SiteID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, d := range displays {
|
||||
if d.ID == id {
|
||||
return &d, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrAccountDisplayNotFound
|
||||
}
|
||||
|
||||
func (r *Repository) BatchUpdateAccountMarkup(ctx context.Context, adminID, siteID uint64, req BatchUpdateAccountMarkupRequest) error {
|
||||
if siteID == 0 {
|
||||
return ErrSiteNotFound
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.MarkupAmountCent < 0 {
|
||||
return ErrInvalidAmount
|
||||
}
|
||||
query := r.db.WithContext(ctx).Model(&model.DistributionSiteAccount{}).Where("site_id = ?", siteID)
|
||||
if len(req.ListingIDs) > 0 {
|
||||
query = query.Where("listing_id IN (?)", req.ListingIDs)
|
||||
}
|
||||
return query.Update("markup_amount_cent", req.MarkupAmountCent).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ListReceivedListings(ctx context.Context, adminID, siteID uint64, q ReceivedListingQuery) (*ReceivedListingPage, error) {
|
||||
if siteID == 0 {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page := q.Page
|
||||
size := q.PageSize
|
||||
if size <= 0 && q.Size > 0 {
|
||||
size = q.Size
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 {
|
||||
size = 20
|
||||
}
|
||||
if size > 100 {
|
||||
size = 100
|
||||
}
|
||||
|
||||
base := r.db.WithContext(ctx).Table("rental_listings rl").
|
||||
Joins("JOIN game_accounts ga ON ga.id = rl.account_id").
|
||||
Joins("LEFT JOIN users u ON u.id = rl.owner_id").
|
||||
Where("rl.distribution_site_id = ?", siteID)
|
||||
|
||||
if q.ReviewStatus != "" {
|
||||
base = base.Where("rl.review_status = ?", q.ReviewStatus)
|
||||
}
|
||||
if q.Status != "" {
|
||||
base = base.Where("rl.status = ?", q.Status)
|
||||
}
|
||||
if q.GameName != "" {
|
||||
base = base.Where("ga.game_name = ?", q.GameName)
|
||||
}
|
||||
if q.Keyword != "" {
|
||||
kw := "%" + strings.TrimSpace(q.Keyword) + "%"
|
||||
base = base.Where("(rl.listing_no LIKE ? OR ga.title LIKE ? OR u.phone LIKE ? OR u.nickname LIKE ?)", kw, kw, kw, kw)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := base.Session(&gorm.Session{}).Select("rl.id").Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type receivedListingRow struct {
|
||||
ID uint64 `gorm:"column:id"`
|
||||
ListingNo string `gorm:"column:listing_no"`
|
||||
AccountID uint64 `gorm:"column:account_id"`
|
||||
OwnerID uint64 `gorm:"column:owner_id"`
|
||||
OwnerUsername string `gorm:"column:owner_username"`
|
||||
OwnerPhone string `gorm:"column:owner_phone"`
|
||||
GameName string `gorm:"column:game_name"`
|
||||
Title string `gorm:"column:title"`
|
||||
ServerRegion string `gorm:"column:server_region"`
|
||||
LoginPlatform string `gorm:"column:login_platform"`
|
||||
RankLevel string `gorm:"column:rank_level"`
|
||||
HafCoinAmount int64 `gorm:"column:haf_coin_amount"`
|
||||
PriceCent int64 `gorm:"column:price_cent"`
|
||||
DepositAmountCent int64 `gorm:"column:deposit_amount_cent"`
|
||||
Status string `gorm:"column:status"`
|
||||
ReviewStatus string `gorm:"column:review_status"`
|
||||
ReviewReason string `gorm:"column:review_reason"`
|
||||
InTransaction bool `gorm:"column:in_transaction"`
|
||||
DistributionSiteID uint64 `gorm:"column:distribution_site_id"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
PublishedAt *time.Time `gorm:"column:published_at"`
|
||||
}
|
||||
|
||||
var rows []receivedListingRow
|
||||
selectSQL := `
|
||||
rl.id, rl.listing_no, rl.account_id, rl.owner_id,
|
||||
COALESCE(u.nickname, '') AS owner_username,
|
||||
COALESCE(u.phone, '') AS owner_phone,
|
||||
ga.game_name, ga.title, ga.server_region, ga.login_platform, ga.rank_level, ga.haf_coin_amount,
|
||||
rl.price_cent, rl.deposit_amount_cent, rl.status, rl.review_status, rl.review_reason,
|
||||
rl.in_transaction, COALESCE(rl.distribution_site_id, 0) AS distribution_site_id,
|
||||
rl.created_at, rl.published_at
|
||||
`
|
||||
if err := base.Select(selectSQL).Order("rl.id DESC").Offset((page - 1) * size).Limit(size).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]ReceivedListingDTO, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
maskedPhone := r.OwnerPhone
|
||||
if len(maskedPhone) >= 7 {
|
||||
maskedPhone = maskedPhone[:3] + "****" + maskedPhone[len(maskedPhone)-4:]
|
||||
}
|
||||
items = append(items, ReceivedListingDTO{
|
||||
ListingID: r.ID,
|
||||
ListingNo: r.ListingNo,
|
||||
SiteID: r.DistributionSiteID,
|
||||
AccountID: r.AccountID,
|
||||
OwnerID: r.OwnerID,
|
||||
OwnerPhone: maskedPhone,
|
||||
OwnerNickname: r.OwnerUsername,
|
||||
Title: r.Title,
|
||||
GameName: r.GameName,
|
||||
ServerRegion: r.ServerRegion,
|
||||
LoginPlatform: r.LoginPlatform,
|
||||
RankLevel: r.RankLevel,
|
||||
HafCoinAmount: r.HafCoinAmount,
|
||||
PriceCent: r.PriceCent,
|
||||
DepositAmountCent: r.DepositAmountCent,
|
||||
Status: r.Status,
|
||||
ReviewStatus: r.ReviewStatus,
|
||||
ReviewReason: r.ReviewReason,
|
||||
PublishedAt: r.PublishedAt,
|
||||
CreatedAt: r.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &ReceivedListingPage{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateReceivedListingStatus(ctx context.Context, adminID, listingID uint64, status string) error {
|
||||
if listingID == 0 {
|
||||
return errors.New("invalid listing id")
|
||||
}
|
||||
var listing model.RentalListing
|
||||
if err := r.db.WithContext(ctx).First(&listing, listingID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if listing.DistributionSiteID == nil || *listing.DistributionSiteID == 0 {
|
||||
return ErrSiteAccessDenied
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, *listing.DistributionSiteID); err != nil {
|
||||
return err
|
||||
}
|
||||
if status != "published" && status != "inactive" && status != "closed" {
|
||||
return errors.New("invalid listing status")
|
||||
}
|
||||
return r.db.WithContext(ctx).Model(&listing).Update("status", status).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetSiteWallet(ctx context.Context, adminID, siteID uint64) (*SiteWalletDTO, error) {
|
||||
if siteID == 0 {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var account model.WalletAccount
|
||||
err := r.db.WithContext(ctx).Where("user_id = 0 AND distribution_site_id = ?", siteID).First(&account).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var totalSettled int64
|
||||
_ = r.db.WithContext(ctx).Table("distribution_settlement_items dsi").
|
||||
Joins("JOIN distribution_settlement_batches dsb ON dsb.id = dsi.batch_id").
|
||||
Where("dsb.site_id = ? AND dsi.status = 'settled'", siteID).
|
||||
Select("COALESCE(SUM(dsi.amount_cent), 0)").Scan(&totalSettled).Error
|
||||
|
||||
var totalWithdrawn int64
|
||||
_ = r.db.WithContext(ctx).Model(&model.WithdrawalRequest{}).
|
||||
Where("user_id = 0 AND distribution_site_id = ? AND status = 'completed'", siteID).
|
||||
Select("COALESCE(SUM(amount_cent), 0)").Scan(&totalWithdrawn).Error
|
||||
|
||||
return &SiteWalletDTO{
|
||||
SiteID: siteID,
|
||||
AvailableBalanceCent: account.AvailableBalanceCent,
|
||||
FrozenBalanceCent: account.FrozenBalanceCent,
|
||||
TotalSettledCent: totalSettled,
|
||||
TotalWithdrawnCent: totalWithdrawn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ApplySiteWithdrawal(ctx context.Context, adminID, siteID uint64, req ApplySiteWithdrawalRequest) (*SiteWithdrawalDTO, error) {
|
||||
if siteID == 0 {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.AmountCent <= 0 {
|
||||
return nil, ErrInvalidAmount
|
||||
}
|
||||
accountType := strings.TrimSpace(req.AccountType)
|
||||
accountName := strings.TrimSpace(req.AccountName)
|
||||
accountNo := strings.TrimSpace(req.AccountNo)
|
||||
if accountType == "" || accountName == "" || accountNo == "" {
|
||||
return nil, errors.New("收款账户信息不完整")
|
||||
}
|
||||
|
||||
withdrawNo, err := generateWithdrawNo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var withdrawal model.WithdrawalRequest
|
||||
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var account model.WalletAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("user_id = 0 AND distribution_site_id = ?", siteID).First(&account).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("钱包账户不存在或余额不足")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if account.AvailableBalanceCent < req.AmountCent {
|
||||
return errors.New("可提现余额不足")
|
||||
}
|
||||
|
||||
withdrawal = model.WithdrawalRequest{
|
||||
WithdrawNo: withdrawNo,
|
||||
UserID: 0,
|
||||
DistributionSiteID: siteID,
|
||||
AmountCent: req.AmountCent,
|
||||
FeeCent: 0,
|
||||
ActualAmountCent: req.AmountCent,
|
||||
AccountType: accountType,
|
||||
AccountName: accountName,
|
||||
AccountNo: accountNo,
|
||||
BankName: strings.TrimSpace(req.BankName),
|
||||
BankBranch: strings.TrimSpace(req.BankBranch),
|
||||
Status: "pending",
|
||||
}
|
||||
if err := tx.Create(&withdrawal).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return wallet.AppendEntries(tx,
|
||||
wallet.Entry{
|
||||
UserID: 0,
|
||||
DistributionSiteID: siteID,
|
||||
Direction: "out",
|
||||
AmountCent: req.AmountCent,
|
||||
BalanceType: "available",
|
||||
BizType: "withdraw_freeze",
|
||||
BizNo: withdrawNo,
|
||||
Remark: "分销提现申请冻结",
|
||||
},
|
||||
wallet.Entry{
|
||||
UserID: 0,
|
||||
DistributionSiteID: siteID,
|
||||
Direction: "in",
|
||||
AmountCent: req.AmountCent,
|
||||
BalanceType: "frozen",
|
||||
BizType: "withdraw_freeze",
|
||||
BizNo: withdrawNo,
|
||||
Remark: "分销提现申请冻结",
|
||||
},
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SiteWithdrawalDTO{
|
||||
ID: withdrawal.ID,
|
||||
WithdrawNo: withdrawal.WithdrawNo,
|
||||
SiteID: withdrawal.DistributionSiteID,
|
||||
AmountCent: withdrawal.AmountCent,
|
||||
FeeCent: withdrawal.FeeCent,
|
||||
ActualAmountCent: withdrawal.ActualAmountCent,
|
||||
AccountType: withdrawal.AccountType,
|
||||
AccountName: withdrawal.AccountName,
|
||||
AccountNo: maskAccountNo(withdrawal.AccountNo, withdrawal.AccountType),
|
||||
BankName: withdrawal.BankName,
|
||||
BankBranch: withdrawal.BankBranch,
|
||||
Status: withdrawal.Status,
|
||||
CreatedAt: withdrawal.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListSiteWithdrawals(ctx context.Context, adminID, siteID uint64, page, size int) (*SiteWithdrawalPage, error) {
|
||||
if siteID == 0 {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 {
|
||||
size = 20
|
||||
}
|
||||
if size > 100 {
|
||||
size = 100
|
||||
}
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&model.WithdrawalRequest{}).
|
||||
Where("user_id = 0 AND distribution_site_id = ?", siteID)
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rows []model.WithdrawalRequest
|
||||
if err := query.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]SiteWithdrawalDTO, 0, len(rows))
|
||||
for _, w := range rows {
|
||||
items = append(items, SiteWithdrawalDTO{
|
||||
ID: w.ID,
|
||||
WithdrawNo: w.WithdrawNo,
|
||||
SiteID: w.DistributionSiteID,
|
||||
AmountCent: w.AmountCent,
|
||||
FeeCent: w.FeeCent,
|
||||
ActualAmountCent: w.ActualAmountCent,
|
||||
AccountType: w.AccountType,
|
||||
AccountName: w.AccountName,
|
||||
AccountNo: maskAccountNo(w.AccountNo, w.AccountType),
|
||||
BankName: w.BankName,
|
||||
BankBranch: w.BankBranch,
|
||||
Status: w.Status,
|
||||
ReviewRemark: w.ReviewRemark,
|
||||
PaymentProofURL: w.PaymentProofURL,
|
||||
PaymentRemark: w.PaymentRemark,
|
||||
PaidAt: w.PaidAt,
|
||||
CreatedAt: w.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &SiteWithdrawalPage{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetDashboardAnalytics(ctx context.Context, adminID, siteID uint64) (*DistributionAnalyticsDTO, error) {
|
||||
if siteID == 0 {
|
||||
return nil, ErrSiteNotFound
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
todayStr := time.Now().Format("2006-01-02")
|
||||
monthStr := time.Now().Format("2006-01")
|
||||
|
||||
var todayReceived int64
|
||||
_ = r.db.WithContext(ctx).Model(&model.RentalListing{}).
|
||||
Where("distribution_site_id = ? AND created_at >= ?", siteID, todayStr+" 00:00:00").Count(&todayReceived).Error
|
||||
|
||||
var totalReceived int64
|
||||
_ = r.db.WithContext(ctx).Model(&model.RentalListing{}).
|
||||
Where("distribution_site_id = ?", siteID).Count(&totalReceived).Error
|
||||
|
||||
var activeListings int64
|
||||
_ = r.db.WithContext(ctx).Model(&model.RentalListing{}).
|
||||
Where("distribution_site_id = ? AND status = 'published' AND review_status = 'approved'", siteID).Count(&activeListings).Error
|
||||
|
||||
var todayOrders int64
|
||||
_ = r.db.WithContext(ctx).Model(&model.RentalOrder{}).
|
||||
Where("distribution_site_id = ? AND created_at >= ?", siteID, todayStr+" 00:00:00").Count(&todayOrders).Error
|
||||
|
||||
var todayGMV int64
|
||||
_ = r.db.WithContext(ctx).Table("rental_orders ro").
|
||||
Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").
|
||||
Where("ro.distribution_site_id = ? AND ro.created_at >= ?", siteID, todayStr+" 00:00:00").
|
||||
Select("COALESCE(SUM(f.gross_paid_amount_cent), 0)").Scan(&todayGMV).Error
|
||||
|
||||
var todayProfit int64
|
||||
_ = r.db.WithContext(ctx).Model(&model.DistributionOrderParty{}).
|
||||
Where("site_id = ? AND created_at >= ?", siteID, todayStr+" 00:00:00").
|
||||
Select("COALESCE(SUM(profit_amount_cent), 0)").Scan(&todayProfit).Error
|
||||
|
||||
var monthProfit int64
|
||||
_ = r.db.WithContext(ctx).Model(&model.DistributionOrderParty{}).
|
||||
Where("site_id = ? AND created_at >= ?", siteID, monthStr+"-01 00:00:00").
|
||||
Select("COALESCE(SUM(profit_amount_cent), 0)").Scan(&monthProfit).Error
|
||||
|
||||
var wallet model.WalletAccount
|
||||
_ = r.db.WithContext(ctx).Where("user_id = 0 AND distribution_site_id = ?", siteID).First(&wallet).Error
|
||||
|
||||
trendDays := make([]DistributionTrendPoint, 0, 7)
|
||||
type trendRow struct {
|
||||
DateStr string `gorm:"column:date_str"`
|
||||
Received int64 `gorm:"column:received"`
|
||||
Orders int64 `gorm:"column:orders"`
|
||||
GMV int64 `gorm:"column:gmv"`
|
||||
Profit int64 `gorm:"column:profit"`
|
||||
}
|
||||
var tRows []trendRow
|
||||
startDate := time.Now().AddDate(0, 0, -6).Format("2006-01-02")
|
||||
dateFormatExpr := "DATE_FORMAT(ro.created_at, '%Y-%m-%d')"
|
||||
if r.db != nil && r.db.Dialector != nil && r.db.Dialector.Name() == "sqlite" {
|
||||
dateFormatExpr = "strftime('%Y-%m-%d', ro.created_at)"
|
||||
}
|
||||
selectTrendSQL := fmt.Sprintf("%s AS date_str, COUNT(ro.id) AS orders, COALESCE(SUM(f.gross_paid_amount_cent), 0) AS gmv", dateFormatExpr)
|
||||
_ = r.db.WithContext(ctx).Table("rental_orders ro").
|
||||
Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").
|
||||
Where("ro.distribution_site_id = ? AND ro.created_at >= ?", siteID, startDate+" 00:00:00").
|
||||
Select(selectTrendSQL).
|
||||
Group(dateFormatExpr).
|
||||
Order("date_str ASC").
|
||||
Scan(&tRows).Error
|
||||
|
||||
tRowMap := make(map[string]trendRow, len(tRows))
|
||||
for _, tr := range tRows {
|
||||
tRowMap[tr.DateStr] = tr
|
||||
}
|
||||
for i := -6; i <= 0; i++ {
|
||||
dStr := time.Now().AddDate(0, 0, i).Format("2006-01-02")
|
||||
if tr, ok := tRowMap[dStr]; ok {
|
||||
trendDays = append(trendDays, DistributionTrendPoint{
|
||||
Date: dStr,
|
||||
ReceivedCount: tr.Received,
|
||||
OrderCount: tr.Orders,
|
||||
GMVCent: tr.GMV,
|
||||
ProfitCent: tr.Profit,
|
||||
})
|
||||
} else {
|
||||
trendDays = append(trendDays, DistributionTrendPoint{
|
||||
Date: dStr,
|
||||
ReceivedCount: 0,
|
||||
OrderCount: 0,
|
||||
GMVCent: 0,
|
||||
ProfitCent: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &DistributionAnalyticsDTO{
|
||||
TodayReceivedCount: todayReceived,
|
||||
TotalReceivedCount: totalReceived,
|
||||
ActiveListingCount: activeListings,
|
||||
TodayOrderCount: todayOrders,
|
||||
TodayGMVCent: todayGMV,
|
||||
TodayProfitCent: todayProfit,
|
||||
MonthProfitCent: monthProfit,
|
||||
AvailableBalanceCent: wallet.AvailableBalanceCent,
|
||||
TrendDays: trendDays,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ func distributionTestDB(t *testing.T) *gorm.DB {
|
||||
&model.DistributionSiteMember{}, &model.DistributionRuleVersion{}, &model.DistributionOrderFinancial{},
|
||||
&model.DistributionOrderParty{}, &model.DistributionSiteAccount{}, &model.DistributionSitePaymentChannel{},
|
||||
&model.DistributionSettlementBatch{}, &model.DistributionSettlementItem{},
|
||||
&model.RentalListing{}, &model.GameAccount{},
|
||||
&model.RentalListing{}, &model.GameAccount{}, &model.User{},
|
||||
&model.WalletAccount{}, &model.WalletLedger{}, &model.WithdrawalRequest{},
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -390,3 +391,228 @@ func TestGetPublicSiteInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistributionBrandingAndMarkup(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "brand_site",
|
||||
SiteName: "品牌测试站",
|
||||
Domain: "brand.example.com",
|
||||
Status: "active",
|
||||
DisplayAccountEnabled: true,
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 1. Update branding
|
||||
updated, err := repo.UpdateSiteBranding(t.Context(), 0, site.ID, UpdateSiteBrandingRequest{
|
||||
LogoURL: "https://cdn.example.com/logo.png",
|
||||
FaviconURL: "https://cdn.example.com/favicon.ico",
|
||||
BannersJSON: `[{"image_url":"https://cdn.example.com/b1.jpg"}]`,
|
||||
AnnouncementsJSON: `[{"title":"开业大吉"}]`,
|
||||
FooterInfoJSON: `{"copyright":"2026 Brand"}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateSiteBranding failed: %v", err)
|
||||
}
|
||||
if updated.LogoURL != "https://cdn.example.com/logo.png" || updated.FaviconURL != "https://cdn.example.com/favicon.ico" {
|
||||
t.Fatalf("unexpected branding: %+v", updated)
|
||||
}
|
||||
|
||||
// Verify PublicSiteInfo contains branding
|
||||
pubInfo, err := repo.GetPublicSiteInfo(t.Context(), "brand_site", "")
|
||||
if err != nil {
|
||||
t.Fatalf("GetPublicSiteInfo failed: %v", err)
|
||||
}
|
||||
if pubInfo.LogoURL != "https://cdn.example.com/logo.png" {
|
||||
t.Fatalf("expected LogoURL, got: %+v", pubInfo)
|
||||
}
|
||||
|
||||
// 2. Account Markup
|
||||
dsa := model.DistributionSiteAccount{
|
||||
SiteID: site.ID,
|
||||
AccountID: 101,
|
||||
ListingID: 201,
|
||||
DisplayStatus: "enabled",
|
||||
}
|
||||
if err := db.Create(&dsa).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
display, err := repo.UpdateAccountMarkup(t.Context(), 0, dsa.ID, 500)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateAccountMarkup failed: %v", err)
|
||||
}
|
||||
if display.MarkupAmountCent != 500 {
|
||||
t.Fatalf("expected markup 500, got %d", display.MarkupAmountCent)
|
||||
}
|
||||
|
||||
// Batch update markup
|
||||
if err := repo.BatchUpdateAccountMarkup(t.Context(), 0, site.ID, BatchUpdateAccountMarkupRequest{
|
||||
ListingIDs: []uint64{201},
|
||||
MarkupAmountCent: 800,
|
||||
}); err != nil {
|
||||
t.Fatalf("BatchUpdateAccountMarkup failed: %v", err)
|
||||
}
|
||||
|
||||
displays, err := repo.ListAccountDisplays(t.Context(), site.ID, "")
|
||||
if err != nil || len(displays) == 0 || displays[0].MarkupAmountCent != 800 {
|
||||
t.Fatalf("expected markup 800 after batch, got: %+v", displays)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistributionReceivedListingsAndWallet(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "recv_site",
|
||||
SiteName: "收号测试站",
|
||||
Domain: "recv.example.com",
|
||||
Status: "active",
|
||||
DisplayAccountEnabled: true,
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Phone: "13800001234",
|
||||
Nickname: "seller01",
|
||||
}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ga := model.GameAccount{
|
||||
OwnerID: user.ID,
|
||||
GameName: "delta_force",
|
||||
ServerRegion: "cn",
|
||||
LoginPlatform: "pc",
|
||||
Title: "满配三角洲账号",
|
||||
}
|
||||
if err := db.Create(&ga).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
listing := model.RentalListing{
|
||||
ListingNo: "RL-99001",
|
||||
AccountID: ga.ID,
|
||||
OwnerID: user.ID,
|
||||
PriceCent: 2000,
|
||||
DepositAmountCent: 5000,
|
||||
Status: "published",
|
||||
ReviewStatus: "approved",
|
||||
DistributionSiteID: &site.ID,
|
||||
}
|
||||
if err := db.Create(&listing).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 1. List received listings
|
||||
recPage, err := repo.ListReceivedListings(t.Context(), 0, site.ID, ReceivedListingQuery{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListReceivedListings failed: %v", err)
|
||||
}
|
||||
if recPage.Total != 1 || len(recPage.Items) != 1 {
|
||||
t.Fatalf("expected 1 received listing, got %d", recPage.Total)
|
||||
}
|
||||
item := recPage.Items[0]
|
||||
if item.ListingNo != "RL-99001" || item.GameName != "delta_force" || item.OwnerNickname != "seller01" {
|
||||
t.Fatalf("unexpected received listing: %+v", item)
|
||||
}
|
||||
|
||||
// Update received listing status
|
||||
if err := repo.UpdateReceivedListingStatus(t.Context(), 0, listing.ID, "inactive"); err != nil {
|
||||
t.Fatalf("UpdateReceivedListingStatus failed: %v", err)
|
||||
}
|
||||
|
||||
// 2. Settlement batch settles and credits wallet
|
||||
rule := model.DistributionRuleVersion{SiteID: &site.ID, Mode: ModeTwoPartyDistributorReceive, Version: 1, Status: "active", EffectiveFrom: time.Now()}
|
||||
if err := db.Create(&rule).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
batch := model.DistributionSettlementBatch{
|
||||
SiteID: site.ID,
|
||||
BatchNo: "B-WALLET-01",
|
||||
PeriodStart: time.Now().Add(-24 * time.Hour),
|
||||
PeriodEnd: time.Now(),
|
||||
TotalAmountCent: 10000,
|
||||
Status: "pending",
|
||||
}
|
||||
if err := db.Create(&batch).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
batchItem := model.DistributionSettlementItem{
|
||||
BatchID: batch.ID,
|
||||
OrderID: 888,
|
||||
PartyID: site.ID,
|
||||
AmountCent: 10000,
|
||||
Status: "pending",
|
||||
}
|
||||
if err := db.Create(&batchItem).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Mark batch settled -> auto-credits merchant wallet
|
||||
if _, err := repo.MarkSettlementBatch(t.Context(), 0, batch.ID); err != nil {
|
||||
t.Fatalf("MarkSettlementBatch failed: %v", err)
|
||||
}
|
||||
|
||||
walletDTO, err := repo.GetSiteWallet(t.Context(), 0, site.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSiteWallet failed: %v", err)
|
||||
}
|
||||
if walletDTO.AvailableBalanceCent != 10000 || walletDTO.TotalSettledCent != 10000 {
|
||||
t.Fatalf("expected available balance 10000, got: %+v", walletDTO)
|
||||
}
|
||||
|
||||
// 3. Apply withdrawal
|
||||
wd, err := repo.ApplySiteWithdrawal(t.Context(), 0, site.ID, ApplySiteWithdrawalRequest{
|
||||
SiteID: site.ID,
|
||||
AmountCent: 4000,
|
||||
AccountType: "alipay",
|
||||
AccountName: "分销商李四",
|
||||
AccountNo: "13800005678",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplySiteWithdrawal failed: %v", err)
|
||||
}
|
||||
if wd.AmountCent != 4000 || wd.Status != "pending" {
|
||||
t.Fatalf("unexpected withdrawal: %+v", wd)
|
||||
}
|
||||
|
||||
// Check balance after freeze
|
||||
walletAfterWD, err := repo.GetSiteWallet(t.Context(), 0, site.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSiteWallet after WD failed: %v", err)
|
||||
}
|
||||
if walletAfterWD.AvailableBalanceCent != 6000 || walletAfterWD.FrozenBalanceCent != 4000 {
|
||||
t.Fatalf("expected available 6000 frozen 4000, got: %+v", walletAfterWD)
|
||||
}
|
||||
|
||||
// 4. List withdrawals
|
||||
wdPage, err := repo.ListSiteWithdrawals(t.Context(), 0, site.ID, 1, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ListSiteWithdrawals failed: %v", err)
|
||||
}
|
||||
if wdPage.Total != 1 || len(wdPage.Items) != 1 {
|
||||
t.Fatalf("expected 1 withdrawal record, got %d", wdPage.Total)
|
||||
}
|
||||
|
||||
// 5. Dashboard analytics
|
||||
analytics, err := repo.GetDashboardAnalytics(t.Context(), 0, site.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDashboardAnalytics failed: %v", err)
|
||||
}
|
||||
if analytics.TotalReceivedCount != 1 || analytics.AvailableBalanceCent != 6000 {
|
||||
t.Fatalf("unexpected analytics: %+v", analytics)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -239,4 +239,68 @@ func (s *Service) GetPublicSiteInfo(ctx context.Context, code, domain string) (*
|
||||
return s.repo.GetPublicSiteInfo(ctx, code, domain)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateSiteBranding(ctx context.Context, adminID, siteID uint64, req UpdateSiteBrandingRequest) (*SiteDTO, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.UpdateSiteBranding(ctx, adminID, siteID, req)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateAccountMarkup(ctx context.Context, adminID, id uint64, markupCent int64) (*AccountDisplayDTO, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.UpdateAccountMarkup(ctx, adminID, id, markupCent)
|
||||
}
|
||||
|
||||
func (s *Service) BatchUpdateAccountMarkup(ctx context.Context, adminID, siteID uint64, req BatchUpdateAccountMarkupRequest) error {
|
||||
if err := s.available(); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.repo.BatchUpdateAccountMarkup(ctx, adminID, siteID, req)
|
||||
}
|
||||
|
||||
func (s *Service) ListReceivedListings(ctx context.Context, adminID, siteID uint64, q ReceivedListingQuery) (*ReceivedListingPage, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.ListReceivedListings(ctx, adminID, siteID, q)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateReceivedListingStatus(ctx context.Context, adminID, listingID uint64, status string) error {
|
||||
if err := s.available(); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.repo.UpdateReceivedListingStatus(ctx, adminID, listingID, status)
|
||||
}
|
||||
|
||||
func (s *Service) GetSiteWallet(ctx context.Context, adminID, siteID uint64) (*SiteWalletDTO, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.GetSiteWallet(ctx, adminID, siteID)
|
||||
}
|
||||
|
||||
func (s *Service) ApplySiteWithdrawal(ctx context.Context, adminID, siteID uint64, req ApplySiteWithdrawalRequest) (*SiteWithdrawalDTO, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.ApplySiteWithdrawal(ctx, adminID, siteID, req)
|
||||
}
|
||||
|
||||
func (s *Service) ListSiteWithdrawals(ctx context.Context, adminID, siteID uint64, page, size int) (*SiteWithdrawalPage, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.ListSiteWithdrawals(ctx, adminID, siteID, page, size)
|
||||
}
|
||||
|
||||
func (s *Service) GetDashboardAnalytics(ctx context.Context, adminID, siteID uint64) (*DistributionAnalyticsDTO, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.GetDashboardAnalytics(ctx, adminID, siteID)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -608,6 +608,15 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
distributionProtectedRoutes.GET("/auth/me", distributionHandler.Me)
|
||||
distributionProtectedRoutes.POST("/auth/logout", distributionHandler.Logout)
|
||||
distributionProtectedRoutes.POST("/sites/:id/sync-accounts", distributionHandler.SyncSiteAccounts)
|
||||
distributionProtectedRoutes.PUT("/sites/:id/branding", distributionHandler.UpdateSiteBranding)
|
||||
distributionProtectedRoutes.GET("/sites/:id/wallet", distributionHandler.GetSiteWallet)
|
||||
distributionProtectedRoutes.POST("/sites/:id/withdrawals", distributionHandler.ApplySiteWithdrawal)
|
||||
distributionProtectedRoutes.GET("/sites/:id/withdrawals", distributionHandler.ListSiteWithdrawals)
|
||||
distributionProtectedRoutes.GET("/sites/:id/analytics", distributionHandler.GetDashboardAnalytics)
|
||||
distributionProtectedRoutes.GET("/received-listings", distributionHandler.ListReceivedListings)
|
||||
distributionProtectedRoutes.PUT("/received-listings/:id/status", distributionHandler.UpdateReceivedListingStatus)
|
||||
distributionProtectedRoutes.PUT("/accounts/:id/markup", distributionHandler.UpdateAccountMarkup)
|
||||
distributionProtectedRoutes.POST("/accounts/batch-markup", distributionHandler.BatchUpdateAccountMarkup)
|
||||
}
|
||||
|
||||
adminRoutes := api.Group("/admin", requireAdmin, middleware.RequireAdminPasswordChanged())
|
||||
@@ -625,6 +634,13 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.POST("/distribution/sites/onboard", requirePerm("distribution:site_manage"), distributionHandler.OnboardSite)
|
||||
adminRoutes.POST("/distribution/sites/:id/sync-accounts", requirePerm("distribution:account_display"), distributionHandler.SyncSiteAccounts)
|
||||
adminRoutes.PUT("/distribution/sites/:id", requirePerm("distribution:site_manage"), distributionHandler.UpdateSite)
|
||||
adminRoutes.PUT("/distribution/sites/:id/branding", requirePerm("distribution:site_manage"), distributionHandler.UpdateSiteBranding)
|
||||
adminRoutes.GET("/distribution/sites/:id/wallet", requirePerm("distribution:settlement_view"), distributionHandler.GetSiteWallet)
|
||||
adminRoutes.POST("/distribution/sites/:id/withdrawals", requirePerm("distribution:settlement_manage"), distributionHandler.ApplySiteWithdrawal)
|
||||
adminRoutes.GET("/distribution/sites/:id/withdrawals", requirePerm("distribution:settlement_view"), distributionHandler.ListSiteWithdrawals)
|
||||
adminRoutes.GET("/distribution/sites/:id/analytics", requirePerm("distribution:site_view"), distributionHandler.GetDashboardAnalytics)
|
||||
adminRoutes.GET("/distribution/received-listings", requirePerm("distribution:account_view"), distributionHandler.ListReceivedListings)
|
||||
adminRoutes.PUT("/distribution/received-listings/:id/status", requirePerm("distribution:account_display"), distributionHandler.UpdateReceivedListingStatus)
|
||||
adminRoutes.GET("/distribution/rules", requirePerm("distribution:rule_view"), distributionHandler.ListRules)
|
||||
adminRoutes.POST("/distribution/rules", requirePerm("distribution:rule_manage"), distributionHandler.CreateRule)
|
||||
adminRoutes.PUT("/distribution/rules/:id/status", requirePerm("distribution:rule_manage"), distributionHandler.UpdateRuleStatus)
|
||||
@@ -633,6 +649,8 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.POST("/distribution/orders/:id/financial-snapshot", requirePerm("distribution:settlement_manage"), distributionHandler.CreateFinancialSnapshot)
|
||||
adminRoutes.GET("/distribution/accounts", requirePerm("distribution:account_view"), distributionHandler.ListAccountDisplays)
|
||||
adminRoutes.PUT("/distribution/accounts/:id/display", requirePerm("distribution:account_display"), distributionHandler.UpdateAccountDisplay)
|
||||
adminRoutes.PUT("/distribution/accounts/:id/markup", requirePerm("distribution:account_display"), distributionHandler.UpdateAccountMarkup)
|
||||
adminRoutes.POST("/distribution/accounts/batch-markup", requirePerm("distribution:account_display"), distributionHandler.BatchUpdateAccountMarkup)
|
||||
adminRoutes.GET("/distribution/payment-channels", requirePerm("distribution:payment_view"), distributionHandler.ListPaymentChannels)
|
||||
adminRoutes.POST("/distribution/payment-channels", requirePerm("distribution:payment_manage"), distributionHandler.BindPaymentChannel)
|
||||
adminRoutes.DELETE("/distribution/payment-channels/:id", requirePerm("distribution:payment_manage"), distributionHandler.UnbindPaymentChannel)
|
||||
|
||||
Reference in New Issue
Block a user