feat(distribution): 完善分销收号资产双视图、商户提现、自主加价、品牌装修与数据大盘
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user