添加新接口

This commit is contained in:
zhx 2026-06-25 15:07:46 +08:00
parent 13f07d800f
commit 9b4447e158
11 changed files with 1884 additions and 1515 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2256,6 +2256,7 @@ message GetCPWeeklyRankStatusResponse {
int64 period_start_ms = 3;
int64 period_end_ms = 4;
int64 server_time_ms = 5;
repeated CPWeeklyRankEntry previous_entries = 6;
}
message GetCPWeeklyRankConfigRequest {

View File

@ -77,9 +77,10 @@ type Settlement struct {
// Status is the H5 read model for current week leaderboard and configured rewards.
type Status struct {
Config Config
Entries []Entry
PeriodStartMS int64
PeriodEndMS int64
ServerTimeMS int64
Config Config
Entries []Entry
PreviousEntries []Entry
PeriodStartMS int64
PeriodEndMS int64
ServerTimeMS int64
}

View File

@ -120,11 +120,19 @@ func (s *Service) GetStatus(ctx context.Context, limit int32, nowMS int64) (doma
if limit <= 0 {
limit = 3
}
// H5 同时需要当前周期榜单和上一完整 UTC 周期的前三名;两个窗口都从 user-service 的周期新增 gift_value 聚合读取,避免 gateway/H5 误用累计亲密榜。
entries, err := s.rankSource.ListCPWeeklyRankEntries(ctx, config.RelationType, result.PeriodStartMS, result.PeriodEndMS, limit)
if err != nil {
return domain.Status{}, err
}
result.Entries = entries
previousStart, previousEnd := PreviousWeekWindow(now)
previousLimit := int32(3)
previousEntries, err := s.rankSource.ListCPWeeklyRankEntries(ctx, config.RelationType, previousStart.UnixMilli(), previousEnd.UnixMilli(), previousLimit)
if err != nil {
return domain.Status{}, err
}
result.PreviousEntries = previousEntries
return result, nil
}
@ -136,6 +144,7 @@ func (s *Service) ListSettlements(ctx context.Context, periodStartMS int64, peri
}
// ProcessSettlementBatch freezes the previous complete UTC week and grants each Top relationship reward to both users.
// The rank source already returns weekly delta score, so settlement only persists that frozen score and never re-reads cumulative intimacy.
func (s *Service) ProcessSettlementBatch(ctx context.Context, runID string, workerID string, batchSize int32) (claimed int32, processed int32, success int32, failure int32, hasMore bool, err error) {
if err := s.requireRepository(); err != nil {
return 0, 0, 0, 0, false, err

View File

@ -0,0 +1,142 @@
package cpweeklyrank_test
import (
"context"
"errors"
"strconv"
"testing"
"time"
"hyapp/pkg/appcode"
domain "hyapp/services/activity-service/internal/domain/cpweeklyrank"
service "hyapp/services/activity-service/internal/service/cpweeklyrank"
)
func TestGetStatusReturnsPreviousEntriesFromPreviousWeek(t *testing.T) {
now := time.Date(2026, 6, 24, 12, 0, 0, 0, time.UTC)
currentStart, currentEnd := service.WeekWindow(now)
previousStart, previousEnd := service.PreviousWeekWindow(now)
rankSource := &fakeCPWeeklyRankSource{
windows: map[[2]int64][]domain.Entry{
{currentStart.UnixMilli(), currentEnd.UnixMilli()}: cpWeeklyRankEntries(2, 1000),
{previousStart.UnixMilli(), previousEnd.UnixMilli()}: cpWeeklyRankEntries(5, 5000),
},
}
svc := service.New(&fakeCPWeeklyRankRepository{config: domain.Config{
AppCode: "lalu",
ActivityCode: domain.ActivityCode,
Enabled: true,
RelationType: "cp",
TopCount: 10,
}}, rankSource, nil)
svc.SetClock(func() time.Time { return now })
status, err := svc.GetStatus(appcode.WithContext(context.Background(), "lalu"), 1, 0)
if err != nil {
t.Fatalf("GetStatus failed: %v", err)
}
if len(status.Entries) != 1 {
t.Fatalf("current entries mismatch: %+v", status.Entries)
}
if len(status.PreviousEntries) != 3 {
t.Fatalf("previous entries should be limited to 3, got %+v", status.PreviousEntries)
}
if status.PreviousEntries[0].UserA.Username != "left-1" || status.PreviousEntries[0].UserA.Avatar != "https://avatar/left-1.png" ||
status.PreviousEntries[0].UserB.Username != "right-1" || status.PreviousEntries[0].UserB.Avatar != "https://avatar/right-1.png" ||
status.PreviousEntries[0].Score != 5001 {
t.Fatalf("previous entry should keep user snapshots and intimacy score: %+v", status.PreviousEntries[0])
}
if len(rankSource.calls) != 2 {
t.Fatalf("rank source call count mismatch: %+v", rankSource.calls)
}
if rankSource.calls[0].startMS != currentStart.UnixMilli() || rankSource.calls[0].endMS != currentEnd.UnixMilli() || rankSource.calls[0].limit != 1 {
t.Fatalf("current window call mismatch: %+v", rankSource.calls[0])
}
if rankSource.calls[1].startMS != previousStart.UnixMilli() || rankSource.calls[1].endMS != previousEnd.UnixMilli() || rankSource.calls[1].limit != 3 {
t.Fatalf("previous window call mismatch: %+v", rankSource.calls[1])
}
}
type fakeCPWeeklyRankRepository struct {
config domain.Config
}
func (r *fakeCPWeeklyRankRepository) GetCPWeeklyRankConfig(context.Context) (domain.Config, bool, error) {
return r.config, true, nil
}
func (r *fakeCPWeeklyRankRepository) UpdateCPWeeklyRankConfig(context.Context, domain.Config, int64) (domain.Config, error) {
return domain.Config{}, errors.New("not implemented")
}
func (r *fakeCPWeeklyRankRepository) ListCPWeeklyRankSettlements(context.Context, int64, int64) ([]domain.Settlement, error) {
return nil, errors.New("not implemented")
}
func (r *fakeCPWeeklyRankRepository) PrepareCPWeeklyRankSettlements(context.Context, domain.Config, []domain.Entry, int64, int64, int64) ([]domain.Settlement, error) {
return nil, errors.New("not implemented")
}
func (r *fakeCPWeeklyRankRepository) ListPendingCPWeeklyRankSettlements(context.Context, int64, int64, int) ([]domain.Settlement, error) {
return nil, errors.New("not implemented")
}
func (r *fakeCPWeeklyRankRepository) MarkCPWeeklyRankSettlementGranted(context.Context, string, string, int64) error {
return errors.New("not implemented")
}
func (r *fakeCPWeeklyRankRepository) MarkCPWeeklyRankSettlementFailed(context.Context, string, string, int64) error {
return errors.New("not implemented")
}
type fakeCPWeeklyRankSource struct {
calls []fakeCPWeeklyRankSourceCall
windows map[[2]int64][]domain.Entry
}
type fakeCPWeeklyRankSourceCall struct {
relationType string
startMS int64
endMS int64
limit int32
}
func (s *fakeCPWeeklyRankSource) ListCPWeeklyRankEntries(_ context.Context, relationType string, startMS int64, endMS int64, limit int32) ([]domain.Entry, error) {
s.calls = append(s.calls, fakeCPWeeklyRankSourceCall{
relationType: relationType,
startMS: startMS,
endMS: endMS,
limit: limit,
})
entries := append([]domain.Entry(nil), s.windows[[2]int64{startMS, endMS}]...)
if limit > 0 && len(entries) > int(limit) {
entries = entries[:limit]
}
return entries, nil
}
func cpWeeklyRankEntries(count int, baseScore int64) []domain.Entry {
entries := make([]domain.Entry, 0, count)
for index := 1; index <= count; index++ {
suffix := strconv.Itoa(index)
entries = append(entries, domain.Entry{
Rank: int64(index),
RelationshipID: "cp-rel-" + suffix,
RelationType: "cp",
Score: baseScore + int64(index),
UserA: domain.User{
UserID: int64(1000 + index),
DisplayUserID: "L" + suffix,
Username: "left-" + suffix,
Avatar: "https://avatar/left-" + suffix + ".png",
},
UserB: domain.User{
UserID: int64(2000 + index),
DisplayUserID: "R" + suffix,
Username: "right-" + suffix,
Avatar: "https://avatar/right-" + suffix + ".png",
},
})
}
return entries
}

View File

@ -30,11 +30,12 @@ func (s *CPWeeklyRankServer) GetCPWeeklyRankStatus(ctx context.Context, req *act
return nil, xerr.ToGRPCError(err)
}
return &activityv1.GetCPWeeklyRankStatusResponse{
Config: cpWeeklyRankConfigToProto(status.Config),
Entries: cpWeeklyRankEntriesToProto(status.Entries),
PeriodStartMs: status.PeriodStartMS,
PeriodEndMs: status.PeriodEndMS,
ServerTimeMs: status.ServerTimeMS,
Config: cpWeeklyRankConfigToProto(status.Config),
Entries: cpWeeklyRankEntriesToProto(status.Entries),
PreviousEntries: cpWeeklyRankEntriesToProto(status.PreviousEntries),
PeriodStartMs: status.PeriodStartMS,
PeriodEndMs: status.PeriodEndMS,
ServerTimeMs: status.ServerTimeMS,
}, nil
}

View File

@ -9,12 +9,13 @@ import (
)
type cpWeeklyRankStatusData struct {
Enabled bool `json:"enabled"`
Period cpWeeklyRankPeriodData `json:"period"`
Rewards []cpWeeklyRankRewardData `json:"rewards"`
Entries []cpWeeklyRankEntryData `json:"entries"`
ServerTimeMS int64 `json:"server_time_ms"`
RelationType string `json:"relation_type"`
Enabled bool `json:"enabled"`
Period cpWeeklyRankPeriodData `json:"period"`
Rewards []cpWeeklyRankRewardData `json:"rewards"`
Entries []cpWeeklyRankEntryData `json:"entries"`
PreviousEntries []cpWeeklyRankEntryData `json:"previous_entries"`
ServerTimeMS int64 `json:"server_time_ms"`
RelationType string `json:"relation_type"`
}
type cpWeeklyRankPeriodData struct {
@ -81,12 +82,13 @@ func (h *Handler) getCPWeeklyRankStatus(writer http.ResponseWriter, request *htt
func cpWeeklyRankStatusFromProto(resp *activityv1.GetCPWeeklyRankStatusResponse, groups map[int64]checkinResourceGroupData) cpWeeklyRankStatusData {
config := resp.GetConfig()
return cpWeeklyRankStatusData{
Enabled: config.GetEnabled(),
RelationType: config.GetRelationType(),
Period: cpWeeklyRankPeriodData{StartMS: resp.GetPeriodStartMs(), EndMS: resp.GetPeriodEndMs()},
Rewards: cpWeeklyRankRewardsFromProto(config.GetRewards(), groups),
Entries: cpWeeklyRankEntriesFromProto(resp.GetEntries()),
ServerTimeMS: resp.GetServerTimeMs(),
Enabled: config.GetEnabled(),
RelationType: config.GetRelationType(),
Period: cpWeeklyRankPeriodData{StartMS: resp.GetPeriodStartMs(), EndMS: resp.GetPeriodEndMs()},
Rewards: cpWeeklyRankRewardsFromProto(config.GetRewards(), groups),
Entries: cpWeeklyRankEntriesFromProto(resp.GetEntries()),
PreviousEntries: cpWeeklyRankEntriesFromProto(resp.GetPreviousEntries()),
ServerTimeMS: resp.GetServerTimeMs(),
}
}

View File

@ -618,6 +618,16 @@ type fakeWeeklyStarClient struct {
err error
}
type fakeCPWeeklyRankClient struct {
lastStatus *activityv1.GetCPWeeklyRankStatusRequest
statusResp *activityv1.GetCPWeeklyRankStatusResponse
err error
}
type fakeUserCPClient struct {
listIntimacyLeaderboardCalls int
}
type fakeBroadcastClient struct {
lastRemove *activityv1.RemoveRegionBroadcastMemberRequest
err error
@ -1305,6 +1315,50 @@ func (f *fakeWeeklyStarClient) ListWeeklyStarHistory(_ context.Context, req *act
return &activityv1.ListWeeklyStarHistoryResponse{}, nil
}
func (f *fakeCPWeeklyRankClient) GetCPWeeklyRankStatus(_ context.Context, req *activityv1.GetCPWeeklyRankStatusRequest) (*activityv1.GetCPWeeklyRankStatusResponse, error) {
f.lastStatus = req
if f.err != nil {
return nil, f.err
}
if f.statusResp != nil {
return f.statusResp, nil
}
return &activityv1.GetCPWeeklyRankStatusResponse{}, nil
}
func (f *fakeUserCPClient) ListCPApplications(context.Context, *userv1.ListCPApplicationsRequest) (*userv1.ListCPApplicationsResponse, error) {
return &userv1.ListCPApplicationsResponse{}, nil
}
func (f *fakeUserCPClient) AcceptCPApplication(context.Context, *userv1.AcceptCPApplicationRequest) (*userv1.AcceptCPApplicationResponse, error) {
return &userv1.AcceptCPApplicationResponse{}, nil
}
func (f *fakeUserCPClient) RejectCPApplication(context.Context, *userv1.RejectCPApplicationRequest) (*userv1.RejectCPApplicationResponse, error) {
return &userv1.RejectCPApplicationResponse{}, nil
}
func (f *fakeUserCPClient) ListCPRelationships(context.Context, *userv1.ListCPRelationshipsRequest) (*userv1.ListCPRelationshipsResponse, error) {
return &userv1.ListCPRelationshipsResponse{}, nil
}
func (f *fakeUserCPClient) ListCPIntimacyLeaderboard(context.Context, *userv1.ListCPIntimacyLeaderboardRequest) (*userv1.ListCPIntimacyLeaderboardResponse, error) {
f.listIntimacyLeaderboardCalls++
return nil, errors.New("ordinary cp intimacy leaderboard must not serve cp weekly activity")
}
func (f *fakeUserCPClient) PrepareBreakCPRelationship(context.Context, *userv1.PrepareBreakCPRelationshipRequest) (*userv1.PrepareBreakCPRelationshipResponse, error) {
return &userv1.PrepareBreakCPRelationshipResponse{}, nil
}
func (f *fakeUserCPClient) ConfirmBreakCPRelationship(context.Context, *userv1.ConfirmBreakCPRelationshipRequest) (*userv1.ConfirmBreakCPRelationshipResponse, error) {
return &userv1.ConfirmBreakCPRelationshipResponse{}, nil
}
func (f *fakeUserCPClient) CancelBreakCPRelationship(context.Context, *userv1.CancelBreakCPRelationshipRequest) (*userv1.CancelBreakCPRelationshipResponse, error) {
return &userv1.CancelBreakCPRelationshipResponse{}, nil
}
func (f *fakeBroadcastClient) RemoveRegionBroadcastMember(_ context.Context, req *activityv1.RemoveRegionBroadcastMemberRequest) (*activityv1.RemoveRegionBroadcastMemberResponse, error) {
f.lastRemove = req
if f.err != nil {
@ -6069,6 +6123,94 @@ func TestAchievementAndBadgeRoutesEmbedResourceMaterials(t *testing.T) {
}
}
func TestCPWeeklyRankActivityUsesWeeklyScoreAndActivityClient(t *testing.T) {
cpWeeklyClient := &fakeCPWeeklyRankClient{
statusResp: &activityv1.GetCPWeeklyRankStatusResponse{
Config: &activityv1.CPWeeklyRankConfig{
ActivityCode: "cp_weekly_rank",
Enabled: true,
RelationType: "cp",
TopCount: 3,
},
Entries: []*activityv1.CPWeeklyRankEntry{
{
Rank: 1,
RelationshipId: "weekly_delta_high",
RelationType: "cp",
Score: 100,
UserA: &activityv1.CPWeeklyRankUser{UserId: 7003, DisplayUserId: "7003", Username: "Weekly A", Avatar: "a.png"},
UserB: &activityv1.CPWeeklyRankUser{UserId: 7004, DisplayUserId: "7004", Username: "Weekly B", Avatar: "b.png"},
FormedAtMs: 1780000000000,
UpdatedAtMs: 1780876800000,
FirstScoredAtMs: 1780876800000,
LastScoredAtMs: 1781481599999,
},
{
Rank: 2,
RelationshipId: "weekly_total_high",
RelationType: "cp",
Score: 10,
UserA: &activityv1.CPWeeklyRankUser{UserId: 7001, DisplayUserId: "7001", Username: "Total A", Avatar: "c.png"},
UserB: &activityv1.CPWeeklyRankUser{UserId: 7002, DisplayUserId: "7002", Username: "Total B", Avatar: "d.png"},
FormedAtMs: 1780000000000,
UpdatedAtMs: 1780876800000,
FirstScoredAtMs: 1780876800000,
LastScoredAtMs: 1780876800000,
},
},
PeriodStartMs: 1780876800000,
PeriodEndMs: 1781481600000,
ServerTimeMs: 1780880000000,
},
}
userCPClient := &fakeUserCPClient{}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
handler.SetCPWeeklyRankClient(cpWeeklyClient)
handler.SetUserCPClient(userCPClient)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/activities/cp-weekly-rank?limit=2", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("cp weekly rank status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
}
var envelope struct {
Code string `json:"code"`
Data struct {
Entries []struct {
Rank int64 `json:"rank"`
RelationshipID string `json:"relationship_id"`
Score int64 `json:"score"`
} `json:"entries"`
Period struct {
StartMS int64 `json:"start_ms"`
EndMS int64 `json:"end_ms"`
} `json:"period"`
} `json:"data"`
}
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
t.Fatalf("decode cp weekly rank response failed: %v", err)
}
if cpWeeklyClient.lastStatus == nil || cpWeeklyClient.lastStatus.GetLimit() != 2 || cpWeeklyClient.lastStatus.GetMeta().GetAppCode() != "lalu" {
t.Fatalf("cp weekly rank must call activity-service with request meta: %+v", cpWeeklyClient.lastStatus)
}
if userCPClient.listIntimacyLeaderboardCalls != 0 {
t.Fatalf("cp weekly activity must not read ordinary cumulative intimacy leaderboard, calls=%d", userCPClient.listIntimacyLeaderboardCalls)
}
if envelope.Code != httpkit.CodeOK || len(envelope.Data.Entries) != 2 {
t.Fatalf("cp weekly rank envelope mismatch: %+v", envelope)
}
if envelope.Data.Entries[0].RelationshipID != "weekly_delta_high" || envelope.Data.Entries[0].Score != 100 || envelope.Data.Entries[1].Score != 10 {
t.Fatalf("cp weekly rank must expose activity weekly score unchanged: %+v", envelope.Data.Entries)
}
if envelope.Data.Period.StartMS != 1780876800000 || envelope.Data.Period.EndMS != 1781481600000 {
t.Fatalf("cp weekly rank must expose activity period: %+v", envelope.Data.Period)
}
}
func TestWeeklyStarCurrentEmbedsRewardResourceGroups(t *testing.T) {
giftResource := &walletv1.Resource{
ResourceId: 7308,

View File

@ -72,7 +72,7 @@ type IntimacyLeaderboardEntry struct {
UpdatedAtMS int64 `json:"updated_at_ms"`
}
// WeeklyRankEntry 是按一个 UTC 周期聚合的 CP 关系送礼积分activity-service 用它固化和发奖
// WeeklyRankEntry 是按一个 UTC 周期聚合的 CP 关系新增亲密值activity-service 用它固化和发奖,不能回退到关系累计 intimacy_value
type WeeklyRankEntry struct {
Rank int64
RelationshipID string

View File

@ -319,7 +319,8 @@ func (r *Repository) ListActiveIntimacyLeaderboardEntries(ctx context.Context, l
return entries, rows.Err()
}
// ListWeeklyRankEntries 按消费事实聚合一个 UTC 周期内的 CP 关系送礼分数。
// ListWeeklyRankEntries 按消费事实聚合一个 UTC 周期内的 CP 关系新增亲密值。
// score 只等于窗口内 relationship_intimacy_added gift_value 之和;关系表 intimacy_value 只服务普通亲密榜和等级累计态,不参与活动周榜排名。
func (r *Repository) ListWeeklyRankEntries(ctx context.Context, relationType string, startMS int64, endMS int64, limit int) ([]cpdomain.WeeklyRankEntry, error) {
if r == nil || r.db == nil {
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
@ -342,7 +343,7 @@ func (r *Repository) ListWeeklyRankEntries(ctx context.Context, relationType str
args = append(args, relationType)
}
args = append(args, limit, appCode, appCode)
// 周榜以礼物消费位点为事实源relationship 表只提供双方用户和关系类型;即使关系后来解除,也不能让历史周榜丢失获奖对象
// 周榜以礼物消费位点为事实源relationship 表只提供双方用户、关系类型和展示时间;即使累计亲密值更高,也不能覆盖本周期新增分数排序
// 同分时用首次得分时间和 relationship_id 稳定排序,保证 cron 重试、后台查询和 H5 展示看到同一批排名。
rows, err := r.db.QueryContext(ctx, `
SELECT

View File

@ -176,6 +176,47 @@ func TestListActiveIntimacyLeaderboardEntriesOrdersAndIncludesBothUsers(t *testi
}
}
func TestListWeeklyRankEntriesUsesWeeklyGiftValueNotCumulativeIntimacy(t *testing.T) {
schema := mysqlschema.New(t)
repo := New(schema.DB)
ctx := appcode.WithContext(context.Background(), appcode.Default)
startMS := int64(1780876800000)
endMS := startMS + 7*24*60*60*1000
nowMS := startMS + 1234
for _, userID := range []int64{7001, 7002, 7003, 7004, 7005, 7006} {
seedCPTestUser(t, schema.DB, userID, fmt.Sprintf("Weekly %d", userID), nowMS)
}
seedActiveCPRelationship(t, schema.DB, "weekly_total_high", cpdomain.RelationTypeCP, 7001, 7002, 9000, 9, nowMS)
seedActiveCPRelationship(t, schema.DB, "weekly_delta_high", cpdomain.RelationTypeCP, 7003, 7004, 100, 1, nowMS+1)
seedActiveCPRelationship(t, schema.DB, "weekly_brother", cpdomain.RelationTypeBrother, 7005, 7006, 1, 1, nowMS+2)
seedCPGiftConsumption(t, schema.DB, "evt_total_high_start", "relationship_intimacy_added", "weekly_total_high", 10, startMS, nowMS)
seedCPGiftConsumption(t, schema.DB, "evt_delta_high_start", "relationship_intimacy_added", "weekly_delta_high", 70, startMS, nowMS)
seedCPGiftConsumption(t, schema.DB, "evt_delta_high_end_minus_one", "relationship_intimacy_added", "weekly_delta_high", 30, endMS-1, nowMS)
seedCPGiftConsumption(t, schema.DB, "evt_delta_high_end_excluded", "relationship_intimacy_added", "weekly_delta_high", 999, endMS, nowMS)
seedCPGiftConsumption(t, schema.DB, "evt_delta_high_zero_ignored", "relationship_intimacy_added", "weekly_delta_high", 0, startMS+1, nowMS)
seedCPGiftConsumption(t, schema.DB, "evt_delta_high_status_ignored", "skipped", "weekly_delta_high", 500, startMS+2, nowMS)
seedCPGiftConsumption(t, schema.DB, "evt_brother_relation_ignored", "relationship_intimacy_added", "weekly_brother", 1000, startMS+3, nowMS)
entries, err := repo.ListWeeklyRankEntries(ctx, cpdomain.RelationTypeCP, startMS, endMS, 10)
if err != nil {
t.Fatalf("list weekly rank entries failed: %v", err)
}
if len(entries) != 2 {
t.Fatalf("weekly entries len=%d, want 2: %+v", len(entries), entries)
}
if entries[0].RelationshipID != "weekly_delta_high" || entries[0].Score != 100 || entries[0].Rank != 1 {
t.Fatalf("weekly rank must order by in-window gift_value delta, got first=%+v", entries[0])
}
if entries[0].FirstScoredAtMS != startMS || entries[0].LastScoredAtMS != endMS-1 {
t.Fatalf("weekly rank must use [start,end) boundary times, got first=%d last=%d", entries[0].FirstScoredAtMS, entries[0].LastScoredAtMS)
}
if entries[1].RelationshipID != "weekly_total_high" || entries[1].Score != 10 || entries[1].Rank != 2 {
t.Fatalf("cumulative intimacy must not outrank weekly delta score, got second=%+v", entries[1])
}
}
func TestConsumeGiftEventAddsIntimacyForNormalGiftWhenRelationshipExists(t *testing.T) {
schema := mysqlschema.New(t)
repo := New(schema.DB)
@ -602,6 +643,21 @@ func seedActiveCPRelationship(t testing.TB, db *sql.DB, relationshipID string, r
}
}
func seedCPGiftConsumption(t testing.TB, db *sql.DB, eventID string, status string, relationshipID string, giftValue int64, occurredAtMS int64, nowMS int64) {
t.Helper()
// CP 周榜只信任消费位点里的新增 gift_value测试显式写多种状态和边界时间防止查询退回关系累计值或闭区间统计。
if _, err := db.ExecContext(context.Background(), `
INSERT INTO user_cp_gift_event_consumption (
app_code, room_event_id, status, relationship_id, gift_value,
occurred_at_ms, consumed_at_ms, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
appcode.Default, eventID, status, relationshipID, giftValue, occurredAtMS, nowMS, nowMS, nowMS,
); err != nil {
t.Fatalf("seed cp gift consumption %s failed: %v", eventID, err)
}
}
func seedPendingCPApplication(t testing.TB, db *sql.DB, applicationID string, relationType string, requesterUserID int64, targetUserID int64, nowMS int64) {
t.Helper()