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 := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||||
handler.SetUserHostClient(hostClient)
|
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() == "" {
|
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)
|
t.Fatalf("agency policy request mismatch: %+v", walletClient.lastHostSalaryPolicy)
|
||||||
}
|
}
|
||||||
if walletClient.lastHostSalaryProgress != nil {
|
if walletClient.lastHostSalaryProgress == nil || walletClient.lastHostSalaryProgress.GetHostUserId() != 42 || walletClient.lastHostSalaryProgress.GetCycleKey() != expectedCycle || walletClient.lastHostSalaryProgress.GetAppCode() == "" || walletClient.lastHostSalaryProgress.GetRequestId() == "" {
|
||||||
t.Fatalf("agency policy should not request host progress: %+v", walletClient.lastHostSalaryProgress)
|
t.Fatalf("agency owner salary progress request mismatch: %+v", walletClient.lastHostSalaryProgress)
|
||||||
}
|
}
|
||||||
var response httpkit.ResponseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
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)
|
policy := data["policy"].(map[string]any)
|
||||||
levels := policy["levels"].([]any)
|
levels := policy["levels"].([]any)
|
||||||
secondLevel := levels[1].(map[string]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 {
|
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)
|
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) {
|
func TestBDCenterOverviewUsesBDSalaryAndDirectAgencies(t *testing.T) {
|
||||||
|
|||||||
@ -28,12 +28,6 @@ type agencyCenterSalaryData struct {
|
|||||||
DisplayAmount float64 `json:"display_amount"`
|
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 {
|
type agencyCenterUserData struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
DisplayUserID string `json:"display_user_id"`
|
DisplayUserID string `json:"display_user_id"`
|
||||||
@ -126,8 +120,7 @@ func (h *Handler) getAgencyCenterPlatformPolicy(writer http.ResponseWriter, requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
cycleKey := time.Now().UTC().Format("2006-01")
|
||||||
cycleKey := now.Format("2006-01")
|
|
||||||
// Agency Center 不是主播身份,不能复用 host-center 的 active host 校验;这里固定用当前 agency 的区域读取完整工资政策。
|
// Agency Center 不是主播身份,不能复用 host-center 的 active host 校验;这里固定用当前 agency 的区域读取完整工资政策。
|
||||||
resp, err := h.walletClient.GetActiveHostSalaryPolicy(request.Context(), &walletv1.GetActiveHostSalaryPolicyRequest{
|
resp, err := h.walletClient.GetActiveHostSalaryPolicy(request.Context(), &walletv1.GetActiveHostSalaryPolicyRequest{
|
||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
@ -141,35 +134,24 @@ func (h *Handler) getAgencyCenterPlatformPolicy(writer http.ResponseWriter, requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agency 卡片展示的是本工资周期内归属该 owner 的主播钻石合计。聚合依据入账时固化的
|
// Agency 和 Host Center 的钻石卡片都代表当前登录用户本人的周期主播账户。
|
||||||
// agency_owner_user_id 快照,既不会把 owner 的用户 COIN 钱包混进来,也不会因主播后来换公会改写历史归属。
|
// 这里直接按 owner user_id 读取同一份账务事实,避免 Agency 把下属主播合计误显示成 owner 本人的钻石。
|
||||||
statsResp, err := h.walletClient.GetTeamHostSalaryStats(request.Context(), &walletv1.GetTeamHostSalaryStatsRequest{
|
progressResp, err := h.walletClient.GetHostSalaryProgress(request.Context(), &walletv1.GetHostSalaryProgressRequest{
|
||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
AgencyOwnerUserIds: []int64{agency.GetOwnerUserId()},
|
HostUserId: agency.GetOwnerUserId(),
|
||||||
CycleKeys: []string{cycleKey},
|
CycleKey: cycleKey,
|
||||||
NowMs: now.UnixMilli(),
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
progress := agencyCenterDiamondProgressData{CycleKey: cycleKey}
|
// H5 需要完整政策明细和 owner 本人的周期进度;Agency 没有单主播等级卡,因此不返回 level_progress。
|
||||||
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。
|
|
||||||
httpkit.WriteOK(writer, request, map[string]any{
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
"found": resp.GetFound(),
|
"found": resp.GetFound(),
|
||||||
"agency_region_id": agency.GetRegionId(),
|
"agency_region_id": agency.GetRegionId(),
|
||||||
"policy": hostCenterPolicyFromProto(resp.GetPolicy()),
|
"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
|
return hostdomain.EngagementStats{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.db.QueryRowContext(ctx, `
|
// 旧版 C2C 回调缺 app_code 时会把 Fami 事实写到默认租户。读取仅扩展到这两个索引范围,
|
||||||
SELECT COUNT(DISTINCT sender_user_id)
|
// 并用 users 全局主键校验发送者真实属于当前 App,既恢复历史数据又不放大跨租户计数。
|
||||||
FROM user_private_message_events
|
privateMessageSQL := `
|
||||||
WHERE app_code = ? AND target_user_id = ? AND occurred_at_ms >= ? AND occurred_at_ms < ?`,
|
SELECT COUNT(DISTINCT matched.sender_user_id)
|
||||||
appCode, query.HostUserID, query.StartAtMS, query.EndAtMS,
|
FROM (
|
||||||
).Scan(&stats.PrivateMessageSenders); err != nil {
|
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
|
return hostdomain.EngagementStats{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,13 +141,19 @@ func (r *Repository) hostOnlineDuration(ctx context.Context, query hostdomain.En
|
|||||||
return total, nil
|
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) {
|
func (r *Repository) RecordPrivateMessageEvent(ctx context.Context, event hostdomain.PrivateMessageEvent) (bool, error) {
|
||||||
result, err := r.db.ExecContext(ctx, `
|
result, err := r.db.ExecContext(ctx, `
|
||||||
INSERT IGNORE INTO user_private_message_events (
|
INSERT IGNORE INTO user_private_message_events (
|
||||||
app_code, event_id, sender_user_id, target_user_id, occurred_at_ms, created_at_ms
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
@ -20,8 +20,26 @@ func TestGetHostEngagementStatsAggregatesOwnedFacts(t *testing.T) {
|
|||||||
mock.ExpectQuery("FROM user_mic_daily_stats").
|
mock.ExpectQuery("FROM user_mic_daily_stats").
|
||||||
WithArgs(validMicDayThresholdMS, "fami", int64(42), "2026-07-06", "2026-07-12").
|
WithArgs(validMicDayThresholdMS, "fami", int64(42), "2026-07-06", "2026-07-12").
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"mic_ms", "valid_days"}).AddRow(int64(5400000), int64(1)))
|
WillReturnRows(sqlmock.NewRows([]string{"mic_ms", "valid_days"}).AddRow(int64(5400000), int64(1)))
|
||||||
mock.ExpectQuery("FROM user_private_message_events").
|
mock.ExpectQuery(regexp.QuoteMeta(`
|
||||||
WithArgs("fami", int64(42), int64(1783296000000), int64(1783900800000)).
|
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)))
|
WillReturnRows(sqlmock.NewRows([]string{"senders"}).AddRow(int64(6)))
|
||||||
mock.ExpectQuery("FROM user_follow_events").
|
mock.ExpectQuery("FROM user_follow_events").
|
||||||
WithArgs("fami", int64(42), int64(1783296000000), int64(1783900800000)).
|
WithArgs("fami", int64(42), int64(1783296000000), int64(1783900800000)).
|
||||||
@ -53,3 +71,40 @@ func TestGetHostEngagementStatsAggregatesOwnedFacts(t *testing.T) {
|
|||||||
t.Fatalf("unmet SQL expectations: %v", err)
|
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
|
package ledger
|
||||||
|
|
||||||
// HostRevenueStats 是主播中心按日期展示的 POINT 收益、已兑换数量和去重送礼人数。
|
// HostRevenueStats 是主播中心按日期展示的周期钻石收益、POINT 已兑换数量和去重送礼人数。
|
||||||
type HostRevenueStats struct {
|
type HostRevenueStats struct {
|
||||||
DiamondEarnings int64
|
DiamondEarnings int64
|
||||||
DiamondExchanged int64
|
DiamondExchanged int64
|
||||||
|
|||||||
@ -10,8 +10,10 @@ import (
|
|||||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAgencyHostGiftStats 直接读取送礼交易里的 host_point_added 快照,只统计礼物收益;
|
// GetAgencyHostGiftStats 只从送礼交易的不可变快照统计礼物收益;新政策写 host_point_added。
|
||||||
// 任务奖励、提现释放和 Agency 分成虽然也进入 POINT 账户,但都不能混入 BD 的公会收礼口径。
|
// Fami 未发布收益政策的历史/过渡区域仍写 host_period_diamond_added,且它与新政策都是先按
|
||||||
|
// 礼物类型倍率得到的主播实际收益,因此 Fami 可在政策切换日安全求和;其他 App 仍保持 POINT 口径。
|
||||||
|
// 任务奖励、提现释放和 Agency 分成不在礼物交易快照内,不会混入公会收礼口径。
|
||||||
func (r *Repository) GetAgencyHostGiftStats(ctx context.Context, query ledger.AgencyHostGiftStatsQuery) (ledger.AgencyHostGiftStats, error) {
|
func (r *Repository) GetAgencyHostGiftStats(ctx context.Context, query ledger.AgencyHostGiftStatsQuery) (ledger.AgencyHostGiftStats, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return ledger.AgencyHostGiftStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
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 {
|
if len(query.HostUserIDs) == 0 {
|
||||||
return ledger.AgencyHostGiftStats{}, nil
|
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))
|
placeholders := make([]string, len(query.HostUserIDs))
|
||||||
args := make([]any, 0, len(query.HostUserIDs)+5)
|
args := make([]any, 0, len(query.HostUserIDs)+5)
|
||||||
args = append(args, appcode.FromContext(ctx), bizTypeGiftDebit, bizTypeDirectGiftDebit, query.StartAtMS, query.EndAtMS)
|
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(`
|
querySQL := fmt.Sprintf(`
|
||||||
SELECT
|
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
|
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)
|
THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED)
|
||||||
END)
|
END)
|
||||||
FROM wallet_transactions
|
FROM wallet_transactions
|
||||||
WHERE app_code = ? AND biz_type IN (?, ?) AND status = 'succeeded'
|
WHERE app_code = ? AND biz_type IN (?, ?) AND status = 'succeeded'
|
||||||
AND created_at_ms >= ? AND created_at_ms < ?
|
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
|
var stats ledger.AgencyHostGiftStats
|
||||||
err := r.db.QueryRowContext(ctx, querySQL, args...).Scan(&stats.GiftIncome, &stats.GiftedHostCount)
|
err := r.db.QueryRowContext(ctx, querySQL, args...).Scan(&stats.GiftIncome, &stats.GiftedHostCount)
|
||||||
return stats, err
|
return stats, err
|
||||||
|
|||||||
@ -2,30 +2,51 @@ package mysql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
"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) {
|
func (r *Repository) GetHostRevenueStats(ctx context.Context, query ledger.HostRevenueStatsQuery) (ledger.HostRevenueStats, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return ledger.HostRevenueStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
return ledger.HostRevenueStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
}
|
}
|
||||||
var stats ledger.HostRevenueStats
|
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
|
SELECT
|
||||||
COALESCE(SUM(CASE WHEN e.available_delta > 0 AND t.biz_type <> ? THEN e.available_delta ELSE 0 END), 0),
|
COALESCE(SUM(diamond_delta), 0),
|
||||||
COALESCE(SUM(CASE WHEN e.available_delta < 0 THEN -e.available_delta ELSE 0 END), 0),
|
COUNT(DISTINCT CASE WHEN sender_user_id > 0 THEN sender_user_id END)
|
||||||
COUNT(DISTINCT CASE WHEN e.available_delta > 0 AND e.counterparty_user_id > 0 THEN e.counterparty_user_id END)
|
FROM host_period_diamond_entries FORCE INDEX (idx_host_period_diamond_entries_user_cycle)
|
||||||
FROM wallet_entries e
|
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
|
INNER JOIN wallet_transactions t
|
||||||
ON t.app_code = e.app_code AND t.transaction_id = e.transaction_id
|
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 = ?
|
WHERE e.app_code = ? AND e.user_id = ? AND e.asset_type = ?
|
||||||
AND e.created_at_ms >= ? AND e.created_at_ms < ?`,
|
AND e.created_at_ms >= ? AND e.created_at_ms < ?
|
||||||
bizTypeSalaryWithdrawalRelease,
|
AND e.available_delta < 0
|
||||||
|
AND t.biz_type IN (?, ?, ?)`,
|
||||||
appcode.FromContext(ctx), query.HostUserID, ledger.AssetPoint, query.StartAtMS, query.EndAtMS,
|
appcode.FromContext(ctx), query.HostUserID, ledger.AssetPoint, query.StartAtMS, query.EndAtMS,
|
||||||
).Scan(&stats.DiamondEarnings, &stats.DiamondExchanged, &stats.GiftSenders)
|
bizTypePointExchangeToCoin, bizTypePointTransferToCoinSeller, bizTypeSalaryWithdrawalFreeze,
|
||||||
return stats, err
|
).Scan(&stats.DiamondExchanged); err != nil {
|
||||||
|
return ledger.HostRevenueStats{}, err
|
||||||
|
}
|
||||||
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import (
|
|||||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetHostRevenueStatsAggregatesPointLedger(t *testing.T) {
|
func TestGetHostRevenueStatsAggregatesHostDiamondsAndPointExchanges(t *testing.T) {
|
||||||
db, mock, err := sqlmock.New()
|
db, mock, err := sqlmock.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create sqlmock: %v", err)
|
t.Fatalf("create sqlmock: %v", err)
|
||||||
@ -19,16 +19,25 @@ func TestGetHostRevenueStatsAggregatesPointLedger(t *testing.T) {
|
|||||||
|
|
||||||
mock.ExpectQuery(regexp.QuoteMeta(`
|
mock.ExpectQuery(regexp.QuoteMeta(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(CASE WHEN e.available_delta > 0 AND t.biz_type <> ? THEN e.available_delta ELSE 0 END), 0),
|
COALESCE(SUM(diamond_delta), 0),
|
||||||
COALESCE(SUM(CASE WHEN e.available_delta < 0 THEN -e.available_delta ELSE 0 END), 0),
|
COUNT(DISTINCT CASE WHEN sender_user_id > 0 THEN sender_user_id END)
|
||||||
COUNT(DISTINCT CASE WHEN e.available_delta > 0 AND e.counterparty_user_id > 0 THEN e.counterparty_user_id END)
|
FROM host_period_diamond_entries FORCE INDEX (idx_host_period_diamond_entries_user_cycle)
|
||||||
FROM wallet_entries e
|
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
|
INNER JOIN wallet_transactions t
|
||||||
ON t.app_code = e.app_code AND t.transaction_id = e.transaction_id
|
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 = ?
|
WHERE e.app_code = ? AND e.user_id = ? AND e.asset_type = ?
|
||||||
AND e.created_at_ms >= ? AND e.created_at_ms < ?`)).
|
AND e.created_at_ms >= ? AND e.created_at_ms < ?
|
||||||
WithArgs(bizTypeSalaryWithdrawalRelease, "fami", int64(42), ledger.AssetPoint, int64(1000), int64(2000)).
|
AND e.available_delta < 0
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"earnings", "exchanged", "senders"}).AddRow(int64(8800), int64(1200), int64(7)))
|
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}
|
repo := &Repository{db: db}
|
||||||
stats, err := repo.GetHostRevenueStats(appcode.WithContext(context.Background(), "fami"), ledger.HostRevenueStatsQuery{
|
stats, err := repo.GetHostRevenueStats(appcode.WithContext(context.Background(), "fami"), ledger.HostRevenueStatsQuery{
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetHostRevenueStats 返回钱包账本内的 POINT 收益、兑换和去重送礼用户。
|
// GetHostRevenueStats 返回主播周期钻石收益、POINT 兑换和去重送礼用户。
|
||||||
func (s *Server) GetHostRevenueStats(ctx context.Context, req *walletv1.GetHostRevenueStatsRequest) (*walletv1.GetHostRevenueStatsResponse, error) {
|
func (s *Server) GetHostRevenueStats(ctx context.Context, req *walletv1.GetHostRevenueStatsRequest) (*walletv1.GetHostRevenueStatsResponse, error) {
|
||||||
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||||
stats, err := s.svc.GetHostRevenueStats(ctx, req.GetAppCode(), ledger.HostRevenueStatsQuery{
|
stats, err := s.svc.GetHostRevenueStats(ctx, req.GetAppCode(), ledger.HostRevenueStatsQuery{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user