mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-04 21:20:51 +08:00
feat: add admin affiliate record pages
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -332,6 +333,369 @@ LIMIT $2`, inviterID, limit)
|
||||
return invitees, nil
|
||||
}
|
||||
|
||||
func (r *affiliateRepository) ListAffiliateInviteRecords(ctx context.Context, filter service.AffiliateRecordFilter) ([]service.AffiliateInviteRecord, int64, error) {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
where, args := buildAffiliateRecordWhere(filter, "ua.created_at", []string{
|
||||
"inviter.email", "inviter.username", "invitee.email", "invitee.username",
|
||||
"ua.inviter_id::text", "ua.user_id::text", "inviter_aff.aff_code",
|
||||
})
|
||||
|
||||
total, err := queryAffiliateRecordCount(ctx, client, `
|
||||
SELECT COUNT(*)
|
||||
FROM user_affiliates ua
|
||||
JOIN users invitee ON invitee.id = ua.user_id
|
||||
JOIN users inviter ON inviter.id = ua.inviter_id
|
||||
JOIN user_affiliates inviter_aff ON inviter_aff.user_id = ua.inviter_id
|
||||
`+where, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
orderBy := buildAffiliateRecordOrderBy(filter, map[string]string{
|
||||
"inviter": "inviter.email",
|
||||
"invitee": "invitee.email",
|
||||
"aff_code": "inviter_aff.aff_code",
|
||||
"total_rebate": "total_rebate",
|
||||
"created_at": "ua.created_at",
|
||||
}, "ua.created_at")
|
||||
args = append(args, filter.PageSize, (filter.Page-1)*filter.PageSize)
|
||||
rows, err := client.QueryContext(ctx, `
|
||||
SELECT ua.inviter_id,
|
||||
COALESCE(inviter.email, ''),
|
||||
COALESCE(inviter.username, ''),
|
||||
ua.user_id,
|
||||
COALESCE(invitee.email, ''),
|
||||
COALESCE(invitee.username, ''),
|
||||
COALESCE(inviter_aff.aff_code, ''),
|
||||
COALESCE(SUM(ual.amount), 0)::double precision AS total_rebate,
|
||||
ua.created_at
|
||||
FROM user_affiliates ua
|
||||
JOIN users invitee ON invitee.id = ua.user_id
|
||||
JOIN users inviter ON inviter.id = ua.inviter_id
|
||||
JOIN user_affiliates inviter_aff ON inviter_aff.user_id = ua.inviter_id
|
||||
LEFT JOIN user_affiliate_ledger ual
|
||||
ON ual.user_id = ua.inviter_id
|
||||
AND ual.source_user_id = ua.user_id
|
||||
AND ual.action = 'accrue'
|
||||
`+where+`
|
||||
GROUP BY ua.inviter_id, inviter.email, inviter.username, ua.user_id, invitee.email, invitee.username, inviter_aff.aff_code, ua.created_at
|
||||
`+orderBy+`
|
||||
LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]service.AffiliateInviteRecord, 0)
|
||||
for rows.Next() {
|
||||
var item service.AffiliateInviteRecord
|
||||
if err := rows.Scan(
|
||||
&item.InviterID,
|
||||
&item.InviterEmail,
|
||||
&item.InviterUsername,
|
||||
&item.InviteeID,
|
||||
&item.InviteeEmail,
|
||||
&item.InviteeUsername,
|
||||
&item.AffCode,
|
||||
&item.TotalRebate,
|
||||
&item.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *affiliateRepository) ListAffiliateRebateRecords(ctx context.Context, filter service.AffiliateRecordFilter) ([]service.AffiliateRebateRecord, int64, error) {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
where, args := buildAffiliateRecordWhere(filter, "pal.created_at", []string{
|
||||
"inviter.email", "inviter.username", "invitee.email", "invitee.username",
|
||||
"po.id::text", "po.out_trade_no", "po.payment_type", "po.status",
|
||||
})
|
||||
baseJoin := `
|
||||
FROM payment_audit_logs pal
|
||||
JOIN payment_orders po ON po.id::text = pal.order_id
|
||||
JOIN user_affiliates invitee_aff ON invitee_aff.user_id = po.user_id
|
||||
JOIN users invitee ON invitee.id = po.user_id
|
||||
JOIN users inviter ON inviter.id = invitee_aff.inviter_id
|
||||
WHERE pal.action = 'AFFILIATE_REBATE_APPLIED'`
|
||||
if where != "" {
|
||||
where = strings.Replace(where, "WHERE ", " AND ", 1)
|
||||
}
|
||||
|
||||
total, err := queryAffiliateRecordCount(ctx, client, "SELECT COUNT(*) "+baseJoin+where, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
orderBy := buildAffiliateRecordOrderBy(filter, map[string]string{
|
||||
"order": "po.id",
|
||||
"inviter": "inviter.email",
|
||||
"invitee": "invitee.email",
|
||||
"order_amount": "po.amount",
|
||||
"pay_amount": "po.pay_amount",
|
||||
"payment_type": "po.payment_type",
|
||||
"order_status": "po.status",
|
||||
"created_at": "pal.created_at",
|
||||
}, "pal.created_at")
|
||||
args = append(args, filter.PageSize, (filter.Page-1)*filter.PageSize)
|
||||
rows, err := client.QueryContext(ctx, `
|
||||
SELECT po.id,
|
||||
po.out_trade_no,
|
||||
invitee_aff.inviter_id,
|
||||
COALESCE(inviter.email, ''),
|
||||
COALESCE(inviter.username, ''),
|
||||
po.user_id,
|
||||
COALESCE(invitee.email, ''),
|
||||
COALESCE(invitee.username, ''),
|
||||
po.amount::double precision,
|
||||
po.pay_amount::double precision,
|
||||
COALESCE(pal.detail, ''),
|
||||
po.payment_type,
|
||||
po.status,
|
||||
pal.created_at
|
||||
`+baseJoin+where+`
|
||||
`+orderBy+`
|
||||
LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]service.AffiliateRebateRecord, 0)
|
||||
for rows.Next() {
|
||||
var item service.AffiliateRebateRecord
|
||||
var detail string
|
||||
if err := rows.Scan(
|
||||
&item.OrderID,
|
||||
&item.OutTradeNo,
|
||||
&item.InviterID,
|
||||
&item.InviterEmail,
|
||||
&item.InviterUsername,
|
||||
&item.InviteeID,
|
||||
&item.InviteeEmail,
|
||||
&item.InviteeUsername,
|
||||
&item.OrderAmount,
|
||||
&item.PayAmount,
|
||||
&detail,
|
||||
&item.PaymentType,
|
||||
&item.OrderStatus,
|
||||
&item.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
item.RebateAmount = parseAffiliateRebateAmount(detail)
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *affiliateRepository) ListAffiliateTransferRecords(ctx context.Context, filter service.AffiliateRecordFilter) ([]service.AffiliateTransferRecord, int64, error) {
|
||||
client := clientFromContext(ctx, r.client)
|
||||
where, args := buildAffiliateRecordWhere(filter, "ual.created_at", []string{
|
||||
"u.email", "u.username", "u.id::text",
|
||||
})
|
||||
baseJoin := `
|
||||
FROM user_affiliate_ledger ual
|
||||
JOIN users u ON u.id = ual.user_id
|
||||
JOIN user_affiliates ua ON ua.user_id = ual.user_id
|
||||
WHERE ual.action = 'transfer'`
|
||||
if where != "" {
|
||||
where = strings.Replace(where, "WHERE ", " AND ", 1)
|
||||
}
|
||||
|
||||
total, err := queryAffiliateRecordCount(ctx, client, "SELECT COUNT(*) "+baseJoin+where, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
orderBy := buildAffiliateRecordOrderBy(filter, map[string]string{
|
||||
"user": "u.email",
|
||||
"amount": "ual.amount",
|
||||
"current_balance": "u.balance",
|
||||
"remaining_quota": "ua.aff_quota",
|
||||
"frozen_quota": "ua.aff_frozen_quota",
|
||||
"history_quota": "ua.aff_history_quota",
|
||||
"created_at": "ual.created_at",
|
||||
}, "ual.created_at")
|
||||
args = append(args, filter.PageSize, (filter.Page-1)*filter.PageSize)
|
||||
rows, err := client.QueryContext(ctx, `
|
||||
SELECT ual.id,
|
||||
ual.user_id,
|
||||
COALESCE(u.email, ''),
|
||||
COALESCE(u.username, ''),
|
||||
ual.amount::double precision,
|
||||
u.balance::double precision,
|
||||
ua.aff_quota::double precision,
|
||||
ua.aff_frozen_quota::double precision,
|
||||
ua.aff_history_quota::double precision,
|
||||
ual.created_at
|
||||
`+baseJoin+where+`
|
||||
`+orderBy+`
|
||||
LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]service.AffiliateTransferRecord, 0)
|
||||
for rows.Next() {
|
||||
var item service.AffiliateTransferRecord
|
||||
if err := rows.Scan(
|
||||
&item.LedgerID,
|
||||
&item.UserID,
|
||||
&item.UserEmail,
|
||||
&item.Username,
|
||||
&item.Amount,
|
||||
&item.CurrentBalance,
|
||||
&item.RemainingQuota,
|
||||
&item.FrozenQuota,
|
||||
&item.HistoryQuota,
|
||||
&item.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *affiliateRepository) GetAffiliateUserOverview(ctx context.Context, userID int64) (*service.AffiliateUserOverview, error) {
|
||||
if userID <= 0 {
|
||||
return nil, service.ErrUserNotFound
|
||||
}
|
||||
client := clientFromContext(ctx, r.client)
|
||||
rows, err := client.QueryContext(ctx, `
|
||||
SELECT ua.user_id,
|
||||
COALESCE(u.email, ''),
|
||||
COALESCE(u.username, ''),
|
||||
ua.aff_code,
|
||||
COALESCE(ua.aff_rebate_rate_percent, 0)::double precision,
|
||||
(ua.aff_rebate_rate_percent IS NOT NULL) AS has_custom_rate,
|
||||
ua.aff_count,
|
||||
COALESCE(rebated.rebated_invitee_count, 0),
|
||||
ua.aff_quota::double precision,
|
||||
ua.aff_history_quota::double precision
|
||||
FROM user_affiliates ua
|
||||
JOIN users u ON u.id = ua.user_id
|
||||
LEFT JOIN (
|
||||
SELECT user_id, COUNT(DISTINCT source_user_id)::integer AS rebated_invitee_count
|
||||
FROM user_affiliate_ledger
|
||||
WHERE action = 'accrue' AND source_user_id IS NOT NULL
|
||||
GROUP BY user_id
|
||||
) rebated ON rebated.user_id = ua.user_id
|
||||
WHERE ua.user_id = $1
|
||||
LIMIT 1`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
if !rows.Next() {
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, service.ErrUserNotFound
|
||||
}
|
||||
|
||||
var overview service.AffiliateUserOverview
|
||||
var customRate float64
|
||||
var hasCustomRate bool
|
||||
if err := rows.Scan(
|
||||
&overview.UserID,
|
||||
&overview.Email,
|
||||
&overview.Username,
|
||||
&overview.AffCode,
|
||||
&customRate,
|
||||
&hasCustomRate,
|
||||
&overview.InvitedCount,
|
||||
&overview.RebatedInviteeCount,
|
||||
&overview.AvailableQuota,
|
||||
&overview.HistoryQuota,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hasCustomRate {
|
||||
overview.RebateRatePercent = customRate
|
||||
overview.RebateRateCustom = true
|
||||
}
|
||||
return &overview, rows.Err()
|
||||
}
|
||||
|
||||
func buildAffiliateRecordWhere(filter service.AffiliateRecordFilter, timeColumn string, searchColumns []string) (string, []any) {
|
||||
clauses := make([]string, 0, 3)
|
||||
args := make([]any, 0, 3)
|
||||
if filter.StartAt != nil {
|
||||
args = append(args, *filter.StartAt)
|
||||
clauses = append(clauses, fmt.Sprintf("%s >= $%d", timeColumn, len(args)))
|
||||
}
|
||||
if filter.EndAt != nil {
|
||||
args = append(args, *filter.EndAt)
|
||||
clauses = append(clauses, fmt.Sprintf("%s <= $%d", timeColumn, len(args)))
|
||||
}
|
||||
search := strings.TrimSpace(filter.Search)
|
||||
if search != "" && len(searchColumns) > 0 {
|
||||
args = append(args, "%"+strings.ToLower(search)+"%")
|
||||
parts := make([]string, 0, len(searchColumns))
|
||||
for _, col := range searchColumns {
|
||||
parts = append(parts, fmt.Sprintf("LOWER(%s) LIKE $%d", col, len(args)))
|
||||
}
|
||||
clauses = append(clauses, "("+strings.Join(parts, " OR ")+")")
|
||||
}
|
||||
if len(clauses) == 0 {
|
||||
return "", args
|
||||
}
|
||||
return "WHERE " + strings.Join(clauses, " AND "), args
|
||||
}
|
||||
|
||||
func buildAffiliateRecordOrderBy(filter service.AffiliateRecordFilter, sortColumns map[string]string, fallbackColumn string) string {
|
||||
column := sortColumns[filter.SortBy]
|
||||
if column == "" {
|
||||
column = fallbackColumn
|
||||
}
|
||||
direction := "DESC"
|
||||
if !filter.SortDesc {
|
||||
direction = "ASC"
|
||||
}
|
||||
return "ORDER BY " + column + " " + direction
|
||||
}
|
||||
|
||||
func queryAffiliateRecordCount(ctx context.Context, client affiliateQueryExecer, query string, args ...any) (int64, error) {
|
||||
rows, err := client.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
if !rows.Next() {
|
||||
return 0, rows.Err()
|
||||
}
|
||||
var total int64
|
||||
if err := rows.Scan(&total); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total, rows.Err()
|
||||
}
|
||||
|
||||
func parseAffiliateRebateAmount(detail string) float64 {
|
||||
var payload struct {
|
||||
RebateAmount float64 `json:"rebateAmount"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(detail), &payload); err != nil {
|
||||
return 0
|
||||
}
|
||||
return payload.RebateAmount
|
||||
}
|
||||
|
||||
func (r *affiliateRepository) withTx(ctx context.Context, fn func(txCtx context.Context, txClient *dbent.Client) error) error {
|
||||
if tx := dbent.TxFromContext(ctx); tx != nil {
|
||||
return fn(ctx, tx.Client())
|
||||
|
||||
Reference in New Issue
Block a user