fami主播侧
This commit is contained in:
parent
202fc9e7cd
commit
41eaa429af
@ -7075,6 +7075,14 @@ func TestAgencyCenterPlatformPolicyUsesAgencyRegion(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
hostSalaryProgressResp: &walletv1.GetHostSalaryProgressResponse{Progress: &walletv1.HostSalaryProgress{
|
||||
HostUserId: 42,
|
||||
CycleKey: expectedCycle,
|
||||
RegionId: 31,
|
||||
AgencyOwnerUserId: 42,
|
||||
TotalDiamonds: 30000,
|
||||
GiftDiamondTotal: 30000,
|
||||
}},
|
||||
}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetUserHostClient(hostClient)
|
||||
@ -7096,8 +7104,8 @@ func TestAgencyCenterPlatformPolicyUsesAgencyRegion(t *testing.T) {
|
||||
if walletClient.lastHostSalaryPolicy == nil || walletClient.lastHostSalaryPolicy.GetRegionId() != 31 || walletClient.lastHostSalaryPolicy.GetCycleKey() != expectedCycle || walletClient.lastHostSalaryPolicy.GetSettlementTriggerMode() != "" || walletClient.lastHostSalaryPolicy.GetAppCode() == "" || walletClient.lastHostSalaryPolicy.GetRequestId() == "" {
|
||||
t.Fatalf("agency policy request mismatch: %+v", walletClient.lastHostSalaryPolicy)
|
||||
}
|
||||
if walletClient.lastHostSalaryProgress != nil {
|
||||
t.Fatalf("agency policy should not request host progress: %+v", walletClient.lastHostSalaryProgress)
|
||||
if walletClient.lastHostSalaryProgress == nil || walletClient.lastHostSalaryProgress.GetHostUserId() != 42 || walletClient.lastHostSalaryProgress.GetCycleKey() != expectedCycle || walletClient.lastHostSalaryProgress.GetAppCode() == "" || walletClient.lastHostSalaryProgress.GetRequestId() == "" {
|
||||
t.Fatalf("agency owner salary progress request mismatch: %+v", walletClient.lastHostSalaryProgress)
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
@ -7107,9 +7115,13 @@ func TestAgencyCenterPlatformPolicyUsesAgencyRegion(t *testing.T) {
|
||||
policy := data["policy"].(map[string]any)
|
||||
levels := policy["levels"].([]any)
|
||||
secondLevel := levels[1].(map[string]any)
|
||||
progress := data["progress"].(map[string]any)
|
||||
if data["found"] != true || data["agency_region_id"] != float64(31) || policy["policy_id"] != "9101" || policy["cycle_key"] != expectedCycle || policy["policy_version"] != float64(9101) || len(levels) != 2 || secondLevel["agency_salary_usd"] != 7.2 {
|
||||
t.Fatalf("agency policy response mismatch: %+v", data)
|
||||
}
|
||||
if progress["host_user_id"] != float64(42) || progress["cycle_key"] != expectedCycle || progress["total_diamonds"] != float64(30000) {
|
||||
t.Fatalf("agency owner salary progress response mismatch: %+v", progress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBDCenterOverviewUsesBDSalaryAndDirectAgencies(t *testing.T) {
|
||||
|
||||
@ -28,12 +28,6 @@ type agencyCenterSalaryData struct {
|
||||
DisplayAmount float64 `json:"display_amount"`
|
||||
}
|
||||
|
||||
type agencyCenterDiamondProgressData struct {
|
||||
CycleKey string `json:"cycle_key"`
|
||||
TotalDiamonds int64 `json:"total_diamonds"`
|
||||
ActiveHostCount int32 `json:"active_host_count"`
|
||||
}
|
||||
|
||||
type agencyCenterUserData struct {
|
||||
UserID string `json:"user_id"`
|
||||
DisplayUserID string `json:"display_user_id"`
|
||||
@ -126,8 +120,7 @@ func (h *Handler) getAgencyCenterPlatformPolicy(writer http.ResponseWriter, requ
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
cycleKey := now.Format("2006-01")
|
||||
cycleKey := time.Now().UTC().Format("2006-01")
|
||||
// Agency Center 不是主播身份,不能复用 host-center 的 active host 校验;这里固定用当前 agency 的区域读取完整工资政策。
|
||||
resp, err := h.walletClient.GetActiveHostSalaryPolicy(request.Context(), &walletv1.GetActiveHostSalaryPolicyRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
@ -141,35 +134,24 @@ func (h *Handler) getAgencyCenterPlatformPolicy(writer http.ResponseWriter, requ
|
||||
return
|
||||
}
|
||||
|
||||
// Agency 卡片展示的是本工资周期内归属该 owner 的主播钻石合计。聚合依据入账时固化的
|
||||
// agency_owner_user_id 快照,既不会把 owner 的用户 COIN 钱包混进来,也不会因主播后来换公会改写历史归属。
|
||||
statsResp, err := h.walletClient.GetTeamHostSalaryStats(request.Context(), &walletv1.GetTeamHostSalaryStatsRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
AgencyOwnerUserIds: []int64{agency.GetOwnerUserId()},
|
||||
CycleKeys: []string{cycleKey},
|
||||
NowMs: now.UnixMilli(),
|
||||
// Agency 和 Host Center 的钻石卡片都代表当前登录用户本人的周期主播账户。
|
||||
// 这里直接按 owner user_id 读取同一份账务事实,避免 Agency 把下属主播合计误显示成 owner 本人的钻石。
|
||||
progressResp, err := h.walletClient.GetHostSalaryProgress(request.Context(), &walletv1.GetHostSalaryProgressRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
HostUserId: agency.GetOwnerUserId(),
|
||||
CycleKey: cycleKey,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
progress := agencyCenterDiamondProgressData{CycleKey: cycleKey}
|
||||
for _, stat := range statsResp.GetStats() {
|
||||
if stat.GetCycleKey() != cycleKey {
|
||||
continue
|
||||
}
|
||||
progress.TotalDiamonds = stat.GetTotalDiamonds()
|
||||
progress.ActiveHostCount = stat.GetActiveHostCount()
|
||||
break
|
||||
}
|
||||
|
||||
// H5 需要完整政策明细和 Agency 周期合计;Agency 没有单主播等级,因此不返回 host-center 的 level_progress。
|
||||
// H5 需要完整政策明细和 owner 本人的周期进度;Agency 没有单主播等级卡,因此不返回 level_progress。
|
||||
httpkit.WriteOK(writer, request, map[string]any{
|
||||
"found": resp.GetFound(),
|
||||
"agency_region_id": agency.GetRegionId(),
|
||||
"policy": hostCenterPolicyFromProto(resp.GetPolicy()),
|
||||
"progress": progress,
|
||||
"progress": hostCenterSalaryProgressFromProto(progressResp.GetProgress()),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -31,12 +31,44 @@ func (r *Repository) GetHostEngagementStats(ctx context.Context, query hostdomai
|
||||
return hostdomain.EngagementStats{}, err
|
||||
}
|
||||
|
||||
if err := r.db.QueryRowContext(ctx, `
|
||||
SELECT COUNT(DISTINCT sender_user_id)
|
||||
FROM user_private_message_events
|
||||
WHERE app_code = ? AND target_user_id = ? AND occurred_at_ms >= ? AND occurred_at_ms < ?`,
|
||||
appCode, query.HostUserID, query.StartAtMS, query.EndAtMS,
|
||||
).Scan(&stats.PrivateMessageSenders); err != nil {
|
||||
// 旧版 C2C 回调缺 app_code 时会把 Fami 事实写到默认租户。读取仅扩展到这两个索引范围,
|
||||
// 并用 users 全局主键校验发送者真实属于当前 App,既恢复历史数据又不放大跨租户计数。
|
||||
privateMessageSQL := `
|
||||
SELECT COUNT(DISTINCT matched.sender_user_id)
|
||||
FROM (
|
||||
SELECT sender_user_id
|
||||
FROM user_private_message_events FORCE INDEX (idx_private_message_target_time)
|
||||
WHERE app_code = ? AND target_user_id = ?
|
||||
AND occurred_at_ms >= ? AND occurred_at_ms < ?
|
||||
) matched
|
||||
INNER JOIN users sender
|
||||
ON sender.user_id = matched.sender_user_id AND sender.app_code = ?`
|
||||
privateMessageArgs := []any{appCode, query.HostUserID, query.StartAtMS, query.EndAtMS, appCode}
|
||||
if appCode != appcode.Default {
|
||||
// 分成两个精确 app_code 区间,避免 MySQL 对 IN 条件选择覆盖索引全扫;
|
||||
// UNION ALL 后在最终 COUNT(DISTINCT) 收敛同一发送者,历史/新记录重叠时也不会重复计数。
|
||||
privateMessageSQL = `
|
||||
SELECT COUNT(DISTINCT matched.sender_user_id)
|
||||
FROM (
|
||||
SELECT sender_user_id
|
||||
FROM user_private_message_events FORCE INDEX (idx_private_message_target_time)
|
||||
WHERE app_code = ? AND target_user_id = ?
|
||||
AND occurred_at_ms >= ? AND occurred_at_ms < ?
|
||||
UNION ALL
|
||||
SELECT sender_user_id
|
||||
FROM user_private_message_events FORCE INDEX (idx_private_message_target_time)
|
||||
WHERE app_code = ? AND target_user_id = ?
|
||||
AND occurred_at_ms >= ? AND occurred_at_ms < ?
|
||||
) matched
|
||||
INNER JOIN users sender
|
||||
ON sender.user_id = matched.sender_user_id AND sender.app_code = ?`
|
||||
privateMessageArgs = []any{
|
||||
appCode, query.HostUserID, query.StartAtMS, query.EndAtMS,
|
||||
appcode.Default, query.HostUserID, query.StartAtMS, query.EndAtMS,
|
||||
appCode,
|
||||
}
|
||||
}
|
||||
if err := r.db.QueryRowContext(ctx, privateMessageSQL, privateMessageArgs...).Scan(&stats.PrivateMessageSenders); err != nil {
|
||||
return hostdomain.EngagementStats{}, err
|
||||
}
|
||||
|
||||
@ -109,13 +141,19 @@ func (r *Repository) hostOnlineDuration(ctx context.Context, query hostdomain.En
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// RecordPrivateMessageEvent 用 app_code + event_id 幂等,不保存消息正文或扩展负载。
|
||||
// RecordPrivateMessageEvent 由 users 主数据反查租户,不信任公网回调缺省的 app_code。
|
||||
// users.user_id 是全局主键;两次主键查找同时确认发送者和接收者属于同一 App,避免 Fami 私信误落到默认租户。
|
||||
func (r *Repository) RecordPrivateMessageEvent(ctx context.Context, event hostdomain.PrivateMessageEvent) (bool, error) {
|
||||
result, err := r.db.ExecContext(ctx, `
|
||||
INSERT IGNORE INTO user_private_message_events (
|
||||
app_code, event_id, sender_user_id, target_user_id, occurred_at_ms, created_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
appcode.FromContext(ctx), event.EventID, event.SenderUserID, event.TargetUserID, event.OccurredAtMS, time.Now().UTC().UnixMilli(),
|
||||
)
|
||||
SELECT target.app_code, ?, sender.user_id, target.user_id, ?, ?
|
||||
FROM users target
|
||||
INNER JOIN users sender
|
||||
ON sender.user_id = ? AND sender.app_code = target.app_code
|
||||
WHERE target.user_id = ?`,
|
||||
event.EventID, event.OccurredAtMS, time.Now().UTC().UnixMilli(), event.SenderUserID, event.TargetUserID,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@ -20,8 +20,26 @@ func TestGetHostEngagementStatsAggregatesOwnedFacts(t *testing.T) {
|
||||
mock.ExpectQuery("FROM user_mic_daily_stats").
|
||||
WithArgs(validMicDayThresholdMS, "fami", int64(42), "2026-07-06", "2026-07-12").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"mic_ms", "valid_days"}).AddRow(int64(5400000), int64(1)))
|
||||
mock.ExpectQuery("FROM user_private_message_events").
|
||||
WithArgs("fami", int64(42), int64(1783296000000), int64(1783900800000)).
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`
|
||||
SELECT COUNT(DISTINCT matched.sender_user_id)
|
||||
FROM (
|
||||
SELECT sender_user_id
|
||||
FROM user_private_message_events FORCE INDEX (idx_private_message_target_time)
|
||||
WHERE app_code = ? AND target_user_id = ?
|
||||
AND occurred_at_ms >= ? AND occurred_at_ms < ?
|
||||
UNION ALL
|
||||
SELECT sender_user_id
|
||||
FROM user_private_message_events FORCE INDEX (idx_private_message_target_time)
|
||||
WHERE app_code = ? AND target_user_id = ?
|
||||
AND occurred_at_ms >= ? AND occurred_at_ms < ?
|
||||
) matched
|
||||
INNER JOIN users sender
|
||||
ON sender.user_id = matched.sender_user_id AND sender.app_code = ?`)).
|
||||
WithArgs(
|
||||
"fami", int64(42), int64(1783296000000), int64(1783900800000),
|
||||
"lalu", int64(42), int64(1783296000000), int64(1783900800000),
|
||||
"fami",
|
||||
).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"senders"}).AddRow(int64(6)))
|
||||
mock.ExpectQuery("FROM user_follow_events").
|
||||
WithArgs("fami", int64(42), int64(1783296000000), int64(1783900800000)).
|
||||
@ -53,3 +71,40 @@ func TestGetHostEngagementStatsAggregatesOwnedFacts(t *testing.T) {
|
||||
t.Fatalf("unmet SQL expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordPrivateMessageEventResolvesAppFromUsers(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sqlmock: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
mock.ExpectExec(regexp.QuoteMeta(`
|
||||
INSERT IGNORE INTO user_private_message_events (
|
||||
app_code, event_id, sender_user_id, target_user_id, occurred_at_ms, created_at_ms
|
||||
)
|
||||
SELECT target.app_code, ?, sender.user_id, target.user_id, ?, ?
|
||||
FROM users target
|
||||
INNER JOIN users sender
|
||||
ON sender.user_id = ? AND sender.app_code = target.app_code
|
||||
WHERE target.user_id = ?`)).
|
||||
WithArgs("im-message-1", int64(1783296000000), sqlmock.AnyArg(), int64(41), int64(42)).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
repo := New(db)
|
||||
created, err := repo.RecordPrivateMessageEvent(appcode.WithContext(context.Background(), "lalu"), hostdomain.PrivateMessageEvent{
|
||||
EventID: "im-message-1",
|
||||
SenderUserID: 41,
|
||||
TargetUserID: 42,
|
||||
OccurredAtMS: 1783296000000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RecordPrivateMessageEvent failed: %v", err)
|
||||
}
|
||||
if !created {
|
||||
t.Fatal("event must be created for users in the same authoritative app")
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("unmet SQL expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package ledger
|
||||
|
||||
// HostRevenueStats 是主播中心按日期展示的 POINT 收益、已兑换数量和去重送礼人数。
|
||||
// HostRevenueStats 是主播中心按日期展示的周期钻石收益、POINT 已兑换数量和去重送礼人数。
|
||||
type HostRevenueStats struct {
|
||||
DiamondEarnings int64
|
||||
DiamondExchanged int64
|
||||
|
||||
@ -10,8 +10,10 @@ import (
|
||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||
)
|
||||
|
||||
// GetAgencyHostGiftStats 直接读取送礼交易里的 host_point_added 快照,只统计礼物收益;
|
||||
// 任务奖励、提现释放和 Agency 分成虽然也进入 POINT 账户,但都不能混入 BD 的公会收礼口径。
|
||||
// GetAgencyHostGiftStats 只从送礼交易的不可变快照统计礼物收益;新政策写 host_point_added。
|
||||
// Fami 未发布收益政策的历史/过渡区域仍写 host_period_diamond_added,且它与新政策都是先按
|
||||
// 礼物类型倍率得到的主播实际收益,因此 Fami 可在政策切换日安全求和;其他 App 仍保持 POINT 口径。
|
||||
// 任务奖励、提现释放和 Agency 分成不在礼物交易快照内,不会混入公会收礼口径。
|
||||
func (r *Repository) GetAgencyHostGiftStats(ctx context.Context, query ledger.AgencyHostGiftStatsQuery) (ledger.AgencyHostGiftStats, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return ledger.AgencyHostGiftStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
@ -19,6 +21,11 @@ func (r *Repository) GetAgencyHostGiftStats(ctx context.Context, query ledger.Ag
|
||||
if len(query.HostUserIDs) == 0 {
|
||||
return ledger.AgencyHostGiftStats{}, nil
|
||||
}
|
||||
legacyIncomeSQL := "0"
|
||||
if appcode.FromContext(ctx) == "fami" {
|
||||
// 该片段是固定 SQL 常量,不包含用户输入;非 Fami 不解析旧字段,避免改变其他 App 的收益单位。
|
||||
legacyIncomeSQL = "CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.host_period_diamond_added')), '0') AS SIGNED)"
|
||||
}
|
||||
placeholders := make([]string, len(query.HostUserIDs))
|
||||
args := make([]any, 0, len(query.HostUserIDs)+5)
|
||||
args = append(args, appcode.FromContext(ctx), bizTypeGiftDebit, bizTypeDirectGiftDebit, query.StartAtMS, query.EndAtMS)
|
||||
@ -28,15 +35,19 @@ func (r *Repository) GetAgencyHostGiftStats(ctx context.Context, query ledger.Ag
|
||||
}
|
||||
querySQL := fmt.Sprintf(`
|
||||
SELECT
|
||||
COALESCE(SUM(CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.host_point_added')), '0') AS SIGNED)), 0),
|
||||
COALESCE(SUM(
|
||||
CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.host_point_added')), '0') AS SIGNED) +
|
||||
%s
|
||||
), 0),
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.host_point_added')), '0') AS SIGNED) > 0
|
||||
WHEN CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.host_point_added')), '0') AS SIGNED) +
|
||||
%s > 0
|
||||
THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED)
|
||||
END)
|
||||
FROM wallet_transactions
|
||||
WHERE app_code = ? AND biz_type IN (?, ?) AND status = 'succeeded'
|
||||
AND created_at_ms >= ? AND created_at_ms < ?
|
||||
AND CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) IN (%s)`, strings.Join(placeholders, ","))
|
||||
AND CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) IN (%s)`, legacyIncomeSQL, legacyIncomeSQL, strings.Join(placeholders, ","))
|
||||
var stats ledger.AgencyHostGiftStats
|
||||
err := r.db.QueryRowContext(ctx, querySQL, args...).Scan(&stats.GiftIncome, &stats.GiftedHostCount)
|
||||
return stats, err
|
||||
|
||||
@ -2,30 +2,51 @@ package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||
)
|
||||
|
||||
// GetHostRevenueStats 聚合 POINT 分录;兑换按 available 侧冻结扣减计数,审核结算不会再次累计。
|
||||
// GetHostRevenueStats 的“钻石收益/送礼人数”只读主播周期钻石流水;
|
||||
// POINT 是可兑换资产,任务奖励、Agency 分成和提现退回都不能被误算成收礼钻石。
|
||||
func (r *Repository) GetHostRevenueStats(ctx context.Context, query ledger.HostRevenueStatsQuery) (ledger.HostRevenueStats, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return ledger.HostRevenueStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
var stats ledger.HostRevenueStats
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
startCycle := time.UnixMilli(query.StartAtMS).UTC().Format("2006-01")
|
||||
endCycle := time.UnixMilli(query.EndAtMS - 1).UTC().Format("2006-01")
|
||||
// (app_code,user_id,cycle_key,created_at_ms) 索引先限定单主播和最多 13 个周期,
|
||||
// 再在已收敛的送礼流水上按时间求和/去重,不扫全表也不解析大 JSON 字段。
|
||||
if err := r.db.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN e.available_delta > 0 AND t.biz_type <> ? THEN e.available_delta ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN e.available_delta < 0 THEN -e.available_delta ELSE 0 END), 0),
|
||||
COUNT(DISTINCT CASE WHEN e.available_delta > 0 AND e.counterparty_user_id > 0 THEN e.counterparty_user_id END)
|
||||
FROM wallet_entries e
|
||||
COALESCE(SUM(diamond_delta), 0),
|
||||
COUNT(DISTINCT CASE WHEN sender_user_id > 0 THEN sender_user_id END)
|
||||
FROM host_period_diamond_entries FORCE INDEX (idx_host_period_diamond_entries_user_cycle)
|
||||
WHERE app_code = ? AND user_id = ? AND cycle_key BETWEEN ? AND ?
|
||||
AND created_at_ms >= ? AND created_at_ms < ?`,
|
||||
appcode.FromContext(ctx), query.HostUserID, startCycle, endCycle, query.StartAtMS, query.EndAtMS,
|
||||
).Scan(&stats.DiamondEarnings, &stats.GiftSenders); err != nil {
|
||||
return ledger.HostRevenueStats{}, err
|
||||
}
|
||||
|
||||
// “已兑换”保留现有 POINT 资产口径,但只计用户主动兑金币、转币商和平台提现首次冻结;
|
||||
// 审核扣 frozen 、驳回释放和其他 POINT 变动不重复计入。
|
||||
if err := r.db.QueryRowContext(ctx, `
|
||||
SELECT COALESCE(SUM(-e.available_delta), 0)
|
||||
FROM wallet_entries e FORCE INDEX (idx_wallet_entries_asset_user_time)
|
||||
INNER JOIN wallet_transactions t
|
||||
ON t.app_code = e.app_code AND t.transaction_id = e.transaction_id
|
||||
WHERE e.app_code = ? AND e.user_id = ? AND e.asset_type = ?
|
||||
AND e.created_at_ms >= ? AND e.created_at_ms < ?`,
|
||||
bizTypeSalaryWithdrawalRelease,
|
||||
AND e.created_at_ms >= ? AND e.created_at_ms < ?
|
||||
AND e.available_delta < 0
|
||||
AND t.biz_type IN (?, ?, ?)`,
|
||||
appcode.FromContext(ctx), query.HostUserID, ledger.AssetPoint, query.StartAtMS, query.EndAtMS,
|
||||
).Scan(&stats.DiamondEarnings, &stats.DiamondExchanged, &stats.GiftSenders)
|
||||
return stats, err
|
||||
bizTypePointExchangeToCoin, bizTypePointTransferToCoinSeller, bizTypeSalaryWithdrawalFreeze,
|
||||
).Scan(&stats.DiamondExchanged); err != nil {
|
||||
return ledger.HostRevenueStats{}, err
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||
)
|
||||
|
||||
func TestGetHostRevenueStatsAggregatesPointLedger(t *testing.T) {
|
||||
func TestGetHostRevenueStatsAggregatesHostDiamondsAndPointExchanges(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sqlmock: %v", err)
|
||||
@ -19,16 +19,25 @@ func TestGetHostRevenueStatsAggregatesPointLedger(t *testing.T) {
|
||||
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN e.available_delta > 0 AND t.biz_type <> ? THEN e.available_delta ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN e.available_delta < 0 THEN -e.available_delta ELSE 0 END), 0),
|
||||
COUNT(DISTINCT CASE WHEN e.available_delta > 0 AND e.counterparty_user_id > 0 THEN e.counterparty_user_id END)
|
||||
FROM wallet_entries e
|
||||
COALESCE(SUM(diamond_delta), 0),
|
||||
COUNT(DISTINCT CASE WHEN sender_user_id > 0 THEN sender_user_id END)
|
||||
FROM host_period_diamond_entries FORCE INDEX (idx_host_period_diamond_entries_user_cycle)
|
||||
WHERE app_code = ? AND user_id = ? AND cycle_key BETWEEN ? AND ?
|
||||
AND created_at_ms >= ? AND created_at_ms < ?`)).
|
||||
WithArgs("fami", int64(42), "1970-01", "1970-01", int64(1000), int64(2000)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"earnings", "senders"}).AddRow(int64(8800), int64(7)))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`
|
||||
SELECT COALESCE(SUM(-e.available_delta), 0)
|
||||
FROM wallet_entries e FORCE INDEX (idx_wallet_entries_asset_user_time)
|
||||
INNER JOIN wallet_transactions t
|
||||
ON t.app_code = e.app_code AND t.transaction_id = e.transaction_id
|
||||
WHERE e.app_code = ? AND e.user_id = ? AND e.asset_type = ?
|
||||
AND e.created_at_ms >= ? AND e.created_at_ms < ?`)).
|
||||
WithArgs(bizTypeSalaryWithdrawalRelease, "fami", int64(42), ledger.AssetPoint, int64(1000), int64(2000)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"earnings", "exchanged", "senders"}).AddRow(int64(8800), int64(1200), int64(7)))
|
||||
AND e.created_at_ms >= ? AND e.created_at_ms < ?
|
||||
AND e.available_delta < 0
|
||||
AND t.biz_type IN (?, ?, ?)`)).
|
||||
WithArgs("fami", int64(42), ledger.AssetPoint, int64(1000), int64(2000),
|
||||
bizTypePointExchangeToCoin, bizTypePointTransferToCoinSeller, bizTypeSalaryWithdrawalFreeze).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"exchanged"}).AddRow(int64(1200)))
|
||||
|
||||
repo := &Repository{db: db}
|
||||
stats, err := repo.GetHostRevenueStats(appcode.WithContext(context.Background(), "fami"), ledger.HostRevenueStatsQuery{
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||
)
|
||||
|
||||
// GetHostRevenueStats 返回钱包账本内的 POINT 收益、兑换和去重送礼用户。
|
||||
// GetHostRevenueStats 返回主播周期钻石收益、POINT 兑换和去重送礼用户。
|
||||
func (s *Server) GetHostRevenueStats(ctx context.Context, req *walletv1.GetHostRevenueStatsRequest) (*walletv1.GetHostRevenueStatsResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||
stats, err := s.svc.GetHostRevenueStats(ctx, req.GetAppCode(), ledger.HostRevenueStatsQuery{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user