增加房间流水奖励修复
This commit is contained in:
parent
151cf495e4
commit
f8ec2118b9
@ -67,7 +67,7 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
|
||||
agencyOpeningSvc := agencyopeningservice.New(repository, walletClient, activityclient.NewGRPCAgencyOpeningSource(clients.userConn), roomClient)
|
||||
weeklyStarSvc := weeklystarservice.New(repository, walletClient)
|
||||
cpWeeklyRankSvc := cpweeklyrankservice.New(repository, activityclient.NewGRPCCPWeeklyRankSource(clients.userConn), walletClient)
|
||||
roomTurnoverRewardSvc := roomturnoverrewardservice.New(repository, walletClient, roomClient)
|
||||
roomTurnoverRewardSvc := roomturnoverrewardservice.New(repository, walletClient, roomClient, roomturnoverrewardservice.WithNoticeService(messageSvc))
|
||||
|
||||
// Tencent IM 是区域广播、房间礼物播报和红包播报的外部发布器。
|
||||
// 如果配置要求启动广播 worker,就必须先确认 publisher 可用,避免 worker 启动后只写 outbox 不发消息。
|
||||
|
||||
@ -3,6 +3,7 @@ package roomturnoverreward
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -14,9 +15,15 @@ import (
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/roomturnoverreward"
|
||||
messageservice "hyapp/services/activity-service/internal/service/message"
|
||||
)
|
||||
|
||||
const rewardReason = "room_turnover_reward"
|
||||
const (
|
||||
rewardReason = "room_turnover_reward"
|
||||
rewardNoticeProducer = "room-turnover-reward"
|
||||
rewardNoticeEventType = "room_turnover_reward_granted"
|
||||
rewardNoticeAggregateType = "room_turnover_reward_settlement"
|
||||
)
|
||||
|
||||
// Repository 是房间流水奖励配置、周期聚合和结算记录的唯一持久化边界。
|
||||
type Repository interface {
|
||||
@ -40,21 +47,41 @@ type WalletClient interface {
|
||||
CreditRoomTurnoverReward(ctx context.Context, req *walletv1.CreditRoomTurnoverRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditRoomTurnoverRewardResponse, error)
|
||||
}
|
||||
|
||||
// NoticeService 是房间流水奖励发放后的用户站内信副作用;具体写入仍由 message service owner 完成。
|
||||
type NoticeService interface {
|
||||
CreateSystemNotice(ctx context.Context, cmd messageservice.NoticeCommand) (messageservice.NoticeResult, error)
|
||||
}
|
||||
|
||||
// RoomQueryClient 只读取 room-service 的房主快照,不拥有房间状态。
|
||||
type RoomQueryClient interface {
|
||||
AdminGetRoom(ctx context.Context, req *roomv1.AdminGetRoomRequest, opts ...grpc.CallOption) (*roomv1.AdminGetRoomResponse, error)
|
||||
}
|
||||
|
||||
// Option 只注入可替换的外部副作用,避免发奖主流程在单测里依赖真实消息和房间客户端。
|
||||
type Option func(*Service)
|
||||
|
||||
// WithNoticeService 连接系统消息公共函数;正金额发奖必须能写站内信,防止出现已发币但用户无通知的静默状态。
|
||||
func WithNoticeService(notices NoticeService) Option {
|
||||
return func(s *Service) {
|
||||
s.notices = notices
|
||||
}
|
||||
}
|
||||
|
||||
// Service 承载房间贡献奖励 App 查询、后台配置、事件聚合和每周结算。
|
||||
type Service struct {
|
||||
repository Repository
|
||||
wallet WalletClient
|
||||
room RoomQueryClient
|
||||
notices NoticeService
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func New(repository Repository, wallet WalletClient, room RoomQueryClient) *Service {
|
||||
return &Service{repository: repository, wallet: wallet, room: room, now: time.Now}
|
||||
func New(repository Repository, wallet WalletClient, room RoomQueryClient, options ...Option) *Service {
|
||||
service := &Service{repository: repository, wallet: wallet, room: room, now: time.Now}
|
||||
for _, option := range options {
|
||||
option(service)
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Service) SetClock(now func() time.Time) {
|
||||
@ -347,6 +374,11 @@ func (s *Service) grantSettlement(ctx context.Context, settlement domain.Settlem
|
||||
nowMS := s.now().UTC().UnixMilli()
|
||||
return s.repository.MarkRoomTurnoverRewardSettlementGranted(ctx, settlement.SettlementID, "", nowMS)
|
||||
}
|
||||
if s.notices == nil {
|
||||
err := xerr.New(xerr.Unavailable, "room turnover reward notice service is not configured")
|
||||
_ = s.repository.MarkRoomTurnoverRewardSettlementFailed(ctx, settlement.SettlementID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
return domain.Settlement{}, err
|
||||
}
|
||||
if s.wallet == nil {
|
||||
err := xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||||
_ = s.repository.MarkRoomTurnoverRewardSettlementFailed(ctx, settlement.SettlementID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
@ -371,9 +403,52 @@ func (s *Service) grantSettlement(ctx context.Context, settlement domain.Settlem
|
||||
_ = s.repository.MarkRoomTurnoverRewardSettlementFailed(ctx, settlement.SettlementID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
return domain.Settlement{}, err
|
||||
}
|
||||
// 系统消息用 settlement_id 做生产者幂等键;钱包成功但消息写入失败时,settlement 仍保持可 retry,下一次会用同一钱包 command_id 和同一消息键补齐。
|
||||
if err := s.createRewardGrantedNotice(ctx, settlement, resp.GetTransactionId(), resp.GetGrantedAtMs()); err != nil {
|
||||
_ = s.repository.MarkRoomTurnoverRewardSettlementFailed(ctx, settlement.SettlementID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
|
||||
return domain.Settlement{}, err
|
||||
}
|
||||
return s.repository.MarkRoomTurnoverRewardSettlementGranted(ctx, settlement.SettlementID, resp.GetTransactionId(), resp.GetGrantedAtMs())
|
||||
}
|
||||
|
||||
func (s *Service) createRewardGrantedNotice(ctx context.Context, settlement domain.Settlement, walletTransactionID string, grantedAtMS int64) error {
|
||||
if s.notices == nil {
|
||||
return xerr.New(xerr.Unavailable, "room turnover reward notice service is not configured")
|
||||
}
|
||||
if grantedAtMS <= 0 {
|
||||
grantedAtMS = s.now().UTC().UnixMilli()
|
||||
}
|
||||
summary := roomTurnoverRewardNoticeSummary(settlement.RewardCoinAmount)
|
||||
_, err := s.notices.CreateSystemNotice(ctx, messageservice.NoticeCommand{
|
||||
TargetUserID: settlement.OwnerUserID,
|
||||
Producer: rewardNoticeProducer,
|
||||
ProducerEventID: settlement.SettlementID,
|
||||
ProducerEventType: rewardNoticeEventType,
|
||||
AggregateType: rewardNoticeAggregateType,
|
||||
AggregateID: settlement.SettlementID,
|
||||
Title: "Room turnover reward",
|
||||
Summary: summary,
|
||||
Body: summary,
|
||||
SentAtMS: grantedAtMS,
|
||||
Metadata: map[string]any{
|
||||
"settlement_id": settlement.SettlementID,
|
||||
"room_id": settlement.RoomID,
|
||||
"period_start_ms": settlement.PeriodStartMS,
|
||||
"period_end_ms": settlement.PeriodEndMS,
|
||||
"coin_spent": settlement.CoinSpent,
|
||||
"reward_coin_amount": settlement.RewardCoinAmount,
|
||||
"tier_id": settlement.TierID,
|
||||
"tier_code": settlement.TierCode,
|
||||
"wallet_transaction_id": walletTransactionID,
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func roomTurnoverRewardNoticeSummary(amount int64) string {
|
||||
return "Congratulations! You earned " + strconv.FormatInt(amount, 10) + " coins from this week's room turnover reward."
|
||||
}
|
||||
|
||||
func (s *Service) fetchRoomOwner(ctx context.Context, roomID string) (int64, error) {
|
||||
if s.room == nil {
|
||||
return 0, xerr.New(xerr.Unavailable, "room query client is not configured")
|
||||
|
||||
@ -2,6 +2,7 @@ package roomturnoverreward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -12,6 +13,7 @@ import (
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
domain "hyapp/services/activity-service/internal/domain/roomturnoverreward"
|
||||
messageservice "hyapp/services/activity-service/internal/service/message"
|
||||
)
|
||||
|
||||
func TestWeekWindowUsesUTCOneMondayBoundary(t *testing.T) {
|
||||
@ -160,6 +162,96 @@ func TestHandleRoomEventConsumesGiftValueAsHeatValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantSettlementCreatesSystemNoticeBeforeMarkGranted(t *testing.T) {
|
||||
repo := &retryBoundaryRepository{
|
||||
settlement: roomTurnoverRewardGrantableSettlement(),
|
||||
}
|
||||
wallet := &retryBoundaryWallet{}
|
||||
notices := &fakeRewardNoticeService{}
|
||||
svc := New(repo, wallet, nil, WithNoticeService(notices))
|
||||
svc.SetClock(func() time.Time { return mustParseTime(t, "2026-06-09T01:00:00Z") })
|
||||
|
||||
granted, err := svc.grantSettlement(appcode.WithContext(context.Background(), "lalu"), repo.settlement)
|
||||
if err != nil {
|
||||
t.Fatalf("grantSettlement returned error: %v", err)
|
||||
}
|
||||
if wallet.calls != 1 || repo.grantCalls != 1 || repo.failCalls != 0 {
|
||||
t.Fatalf("grant path mismatch: wallet=%d grant=%d fail=%d", wallet.calls, repo.grantCalls, repo.failCalls)
|
||||
}
|
||||
if notices.calls != 1 {
|
||||
t.Fatalf("expected one system notice, got %d", notices.calls)
|
||||
}
|
||||
if granted.Status != domain.SettlementStatusGranted || granted.WalletTransactionID != "wallet-tx-1" {
|
||||
t.Fatalf("settlement should be granted with wallet transaction, got %+v", granted)
|
||||
}
|
||||
|
||||
cmd := notices.command
|
||||
if cmd.TargetUserID != repo.settlement.OwnerUserID || cmd.Producer != rewardNoticeProducer || cmd.ProducerEventID != repo.settlement.SettlementID {
|
||||
t.Fatalf("notice idempotency or target mismatch: %+v", cmd)
|
||||
}
|
||||
if cmd.ProducerEventType != rewardNoticeEventType || cmd.AggregateType != rewardNoticeAggregateType || cmd.AggregateID != repo.settlement.SettlementID {
|
||||
t.Fatalf("notice aggregate mismatch: %+v", cmd)
|
||||
}
|
||||
wantSummary := "Congratulations! You earned 123 coins from this week's room turnover reward."
|
||||
if cmd.Title != "Room turnover reward" || cmd.Summary != wantSummary || cmd.Body != wantSummary {
|
||||
t.Fatalf("notice content mismatch: title=%q summary=%q body=%q", cmd.Title, cmd.Summary, cmd.Body)
|
||||
}
|
||||
if cmd.SentAtMS != 1234 {
|
||||
t.Fatalf("notice sent_at_ms should use wallet grant time, got %d", cmd.SentAtMS)
|
||||
}
|
||||
if cmd.Metadata["settlement_id"] != repo.settlement.SettlementID ||
|
||||
cmd.Metadata["room_id"] != repo.settlement.RoomID ||
|
||||
cmd.Metadata["reward_coin_amount"] != repo.settlement.RewardCoinAmount ||
|
||||
cmd.Metadata["wallet_transaction_id"] != "wallet-tx-1" {
|
||||
t.Fatalf("notice metadata mismatch: %+v", cmd.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantSettlementFailsWhenSystemNoticeCannotBeCreated(t *testing.T) {
|
||||
repo := &retryBoundaryRepository{
|
||||
settlement: roomTurnoverRewardGrantableSettlement(),
|
||||
}
|
||||
wallet := &retryBoundaryWallet{}
|
||||
notices := &fakeRewardNoticeService{err: errors.New("message unavailable")}
|
||||
svc := New(repo, wallet, nil, WithNoticeService(notices))
|
||||
|
||||
if _, err := svc.grantSettlement(appcode.WithContext(context.Background(), "lalu"), repo.settlement); err == nil {
|
||||
t.Fatal("expected grantSettlement to fail when notice creation fails")
|
||||
}
|
||||
if wallet.calls != 1 || notices.calls != 1 {
|
||||
t.Fatalf("wallet should be credited once and notice attempted once, wallet=%d notices=%d", wallet.calls, notices.calls)
|
||||
}
|
||||
if repo.grantCalls != 0 || repo.failCalls != 1 || repo.settlement.Status != domain.SettlementStatusFailed {
|
||||
t.Fatalf("notice failure must not mark granted: grant=%d fail=%d settlement=%+v", repo.grantCalls, repo.failCalls, repo.settlement)
|
||||
}
|
||||
}
|
||||
|
||||
func roomTurnoverRewardGrantableSettlement() domain.Settlement {
|
||||
return domain.Settlement{
|
||||
SettlementID: "settlement-room-turnover-1",
|
||||
AppCode: "lalu",
|
||||
RoomID: "room-1",
|
||||
OwnerUserID: 42,
|
||||
PeriodStartMS: mustUnixMilli("2026-06-01T01:00:00Z"),
|
||||
PeriodEndMS: mustUnixMilli("2026-06-08T01:00:00Z"),
|
||||
CoinSpent: 9000,
|
||||
TierID: 2,
|
||||
TierCode: "silver",
|
||||
ThresholdCoinSpent: 5000,
|
||||
RewardCoinAmount: 123,
|
||||
Status: domain.SettlementStatusPending,
|
||||
WalletCommandID: "room_turnover_reward:lalu:20260601:room-1",
|
||||
}
|
||||
}
|
||||
|
||||
func mustUnixMilli(value string) int64 {
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return parsed.UnixMilli()
|
||||
}
|
||||
|
||||
func mustParseTime(t *testing.T, value string) time.Time {
|
||||
t.Helper()
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
@ -314,9 +406,26 @@ func (r *retryBoundaryRoom) AdminGetRoom(context.Context, *roomv1.AdminGetRoomRe
|
||||
|
||||
type retryBoundaryWallet struct {
|
||||
calls int
|
||||
req *walletv1.CreditRoomTurnoverRewardRequest
|
||||
}
|
||||
|
||||
func (w *retryBoundaryWallet) CreditRoomTurnoverReward(context.Context, *walletv1.CreditRoomTurnoverRewardRequest, ...grpc.CallOption) (*walletv1.CreditRoomTurnoverRewardResponse, error) {
|
||||
func (w *retryBoundaryWallet) CreditRoomTurnoverReward(_ context.Context, req *walletv1.CreditRoomTurnoverRewardRequest, _ ...grpc.CallOption) (*walletv1.CreditRoomTurnoverRewardResponse, error) {
|
||||
w.calls++
|
||||
w.req = req
|
||||
return &walletv1.CreditRoomTurnoverRewardResponse{TransactionId: "wallet-tx-1", GrantedAtMs: 1234}, nil
|
||||
}
|
||||
|
||||
type fakeRewardNoticeService struct {
|
||||
calls int
|
||||
command messageservice.NoticeCommand
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *fakeRewardNoticeService) CreateSystemNotice(_ context.Context, cmd messageservice.NoticeCommand) (messageservice.NoticeResult, error) {
|
||||
s.calls++
|
||||
s.command = cmd
|
||||
if s.err != nil {
|
||||
return messageservice.NoticeResult{}, s.err
|
||||
}
|
||||
return messageservice.NoticeResult{Created: true}, nil
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package activityapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
@ -142,9 +143,24 @@ func (h *Handler) listAchievements(writer http.ResponseWriter, request *http.Req
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
rawUserID := strings.TrimSpace(request.URL.Query().Get("user_id"))
|
||||
if rawUserID != "" {
|
||||
targetUserID, err := strconv.ParseInt(rawUserID, 10, 64)
|
||||
if err != nil || targetUserID <= 0 {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
// 成就 H5 支持分享/外部入口按目标用户查看;显式 user_id 是查询目标,不代表操作者身份。
|
||||
userID = targetUserID
|
||||
}
|
||||
if userID <= 0 {
|
||||
httpkit.WriteError(writer, request, http.StatusUnauthorized, httpkit.CodeUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
resp, err := h.achievementClient.ListAchievements(request.Context(), &activityv1.ListAchievementsRequest{
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
UserId: userID,
|
||||
CollectionType: strings.TrimSpace(request.URL.Query().Get("collection_type")),
|
||||
CollectionId: strings.TrimSpace(request.URL.Query().Get("collection_id")),
|
||||
Page: page,
|
||||
|
||||
@ -117,6 +117,7 @@ type UserHandlers struct {
|
||||
ListCPIntimacyLeaderboard http.HandlerFunc
|
||||
BreakCPRelationship http.HandlerFunc
|
||||
GetMyProfile http.HandlerFunc
|
||||
GetUserProfile http.HandlerFunc
|
||||
GetMyAppearance http.HandlerFunc
|
||||
GetUserAppearance http.HandlerFunc
|
||||
ListMyResources http.HandlerFunc
|
||||
@ -411,6 +412,8 @@ func (r routes) registerAppRoutes() {
|
||||
func (r routes) registerUserRoutes() {
|
||||
h := r.config.User
|
||||
r.public("/users/by-display-user-id/{display_user_id}", "", h.ResolveDisplayUserID)
|
||||
r.public("/users/by-id/{user_id}/profile", http.MethodGet, h.GetUserProfile)
|
||||
r.public("/users/by-id/{user_id}/appearance", http.MethodGet, h.GetUserAppearance)
|
||||
r.profile("/users/profiles:batch", "", h.BatchUserProfiles)
|
||||
r.profile("/users/room-display-profiles:batch", http.MethodGet, h.BatchRoomDisplayProfiles)
|
||||
r.profile("/users/me/overview", "", h.GetMyOverview)
|
||||
@ -575,7 +578,7 @@ func (r routes) registerLevelRoutes() {
|
||||
|
||||
func (r routes) registerAchievementRoutes() {
|
||||
h := r.config.Achievement
|
||||
r.profile("/achievements", http.MethodGet, h.ListAchievements)
|
||||
r.public("/achievements", http.MethodGet, h.ListAchievements)
|
||||
r.profile("/badges/me", http.MethodGet, h.ListMyBadges)
|
||||
r.profile("/badges/display", http.MethodPost, h.SetBadgeDisplay)
|
||||
}
|
||||
|
||||
@ -6473,6 +6473,67 @@ func TestAchievementAndBadgeRoutesEmbedResourceMaterials(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAchievementListAllowsExplicitUserIDWithoutToken(t *testing.T) {
|
||||
achievementClient := &fakeAchievementClient{
|
||||
listResp: &activityv1.ListAchievementsResponse{
|
||||
Total: 0,
|
||||
ServerTimeMs: 1778256000000,
|
||||
},
|
||||
}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetAchievementClient(achievementClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/achievements?user_id=77&page=3&page_size=6&collection_type=event", nil)
|
||||
request.Header.Set("X-App-Code", "lalu")
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("achievement status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if achievementClient.lastList == nil || achievementClient.lastList.GetUserId() != 77 || achievementClient.lastList.GetPage() != 3 || achievementClient.lastList.GetPageSize() != 6 || achievementClient.lastList.GetCollectionType() != "event" {
|
||||
t.Fatalf("achievement user_id request mismatch: %+v", achievementClient.lastList)
|
||||
}
|
||||
|
||||
achievementClient.lastList = nil
|
||||
request = httptest.NewRequest(http.MethodGet, "/api/v1/achievements?page=1&page_size=6", nil)
|
||||
request.Header.Set("X-App-Code", "lalu")
|
||||
recorder = httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("anonymous achievement without user_id must stay unauthorized: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if achievementClient.lastList != nil {
|
||||
t.Fatalf("anonymous achievement without user_id must not reach upstream: %+v", achievementClient.lastList)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicUserProfileRouteUsesPathUserID(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{
|
||||
usersByID: map[int64]*userv1.User{
|
||||
77: {
|
||||
UserId: 77,
|
||||
DisplayUserId: "100077",
|
||||
Username: "target-user",
|
||||
Avatar: "https://cdn.example/target.png",
|
||||
},
|
||||
},
|
||||
}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-id/77/profile", nil)
|
||||
request.Header.Set("X-App-Code", "lalu")
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("public profile status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if profileClient.lastGet == nil || profileClient.lastGet.GetUserId() != 77 {
|
||||
t.Fatalf("public profile must use path user_id: %+v", profileClient.lastGet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCPWeeklyRankActivityUsesWeeklyScoreAndActivityClient(t *testing.T) {
|
||||
cpWeeklyClient := &fakeCPWeeklyRankClient{
|
||||
statusResp: &activityv1.GetCPWeeklyRankStatusResponse{
|
||||
|
||||
@ -98,6 +98,7 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
||||
ListCPIntimacyLeaderboard: h.listCPIntimacyLeaderboard,
|
||||
BreakCPRelationship: h.breakCPRelationship,
|
||||
GetMyProfile: h.getMyProfile,
|
||||
GetUserProfile: h.getUserProfile,
|
||||
GetMyAppearance: h.getMyAppearance,
|
||||
GetUserAppearance: h.getUserAppearance,
|
||||
UserSocialAction: h.userSocialAction,
|
||||
|
||||
@ -305,6 +305,30 @@ func (h *Handler) getMyProfile(writer http.ResponseWriter, request *http.Request
|
||||
httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0))
|
||||
}
|
||||
|
||||
func (h *Handler) getUserProfile(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.userProfileClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
userID, ok := httpkit.PositiveInt64PathValue(request, "user_id")
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
|
||||
// 成就页的 user_id 模式是只读展示目标用户资料,不能回退到 token 用户,否则分享页会出现资料和成就归属不一致。
|
||||
resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0))
|
||||
}
|
||||
|
||||
// getMyInviteOverview 返回当前登录用户自己的邀请码和邀请计数。
|
||||
func (h *Handler) getMyInviteOverview(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.userProfileClient == nil {
|
||||
|
||||
@ -75,7 +75,7 @@ func (s *Service) MicUp(ctx context.Context, req *roomv1.MicUpRequest) (*roomv1.
|
||||
targetGiftValue := roomUserGiftValue(current, cmd.ActorUserID())
|
||||
|
||||
// RoomMicChanged 是房间外消费者和腾讯云 IM 共同使用的麦位事件源;展示快照固化在事件体,direct IM 和 outbox 补偿共用。
|
||||
micChanged := roomMicChangedEvent(roomeventsv1.RoomMicChanged{
|
||||
micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
|
||||
ActorUserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.ActorUserID(),
|
||||
FromSeat: 0,
|
||||
@ -190,7 +190,7 @@ func (s *Service) micDown(ctx context.Context, cmd command.MicDown) (mutationRes
|
||||
current.Version++
|
||||
seatStatus := current.MicSeats[index].SeatStatus()
|
||||
|
||||
micChanged := roomMicChangedEvent(roomeventsv1.RoomMicChanged{
|
||||
micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
|
||||
ActorUserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
FromSeat: seat.SeatNo,
|
||||
@ -293,7 +293,7 @@ func (s *Service) ChangeMicSeat(ctx context.Context, req *roomv1.ChangeMicSeatRe
|
||||
seatStatus := current.MicSeats[toIndex].SeatStatus()
|
||||
targetGiftValue := roomUserGiftValue(current, cmd.TargetUserID)
|
||||
|
||||
micChanged := roomMicChangedEvent(roomeventsv1.RoomMicChanged{
|
||||
micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
|
||||
ActorUserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
FromSeat: fromSeat.SeatNo,
|
||||
@ -397,7 +397,7 @@ func (s *Service) ConfirmMicPublishing(ctx context.Context, req *roomv1.ConfirmM
|
||||
seatStatus := current.MicSeats[index].SeatStatus()
|
||||
targetGiftValue := roomUserGiftValue(current, cmd.TargetUserID)
|
||||
|
||||
micChanged := roomMicChangedEvent(roomeventsv1.RoomMicChanged{
|
||||
micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
|
||||
ActorUserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
FromSeat: seat.SeatNo,
|
||||
@ -565,7 +565,7 @@ func (s *Service) SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest)
|
||||
if cmd.Muted {
|
||||
action = "mic_mute"
|
||||
}
|
||||
micChanged := roomMicChangedEvent(roomeventsv1.RoomMicChanged{
|
||||
micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
|
||||
ActorUserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
FromSeat: seat.SeatNo,
|
||||
|
||||
@ -13,12 +13,15 @@ import (
|
||||
|
||||
// roomMicChangedEvent 建立 RoomMicChanged 事实;展示快照只来自命令入口,
|
||||
// room-service 不反查 user-service,保证 Room Cell 主链路仍只维护房态。
|
||||
func roomMicChangedEvent(event roomeventsv1.RoomMicChanged, profile command.UserDisplayProfile) *roomeventsv1.RoomMicChanged {
|
||||
func roomMicChangedEvent(event *roomeventsv1.RoomMicChanged, profile command.UserDisplayProfile) *roomeventsv1.RoomMicChanged {
|
||||
if event == nil {
|
||||
event = &roomeventsv1.RoomMicChanged{}
|
||||
}
|
||||
event.TargetNickname = strings.TrimSpace(profile.Nickname)
|
||||
event.TargetAvatar = strings.TrimSpace(profile.Avatar)
|
||||
event.TargetDisplayUserId = strings.TrimSpace(profile.DisplayUserID)
|
||||
event.TargetPrettyDisplayUserId = strings.TrimSpace(profile.PrettyDisplayUserID)
|
||||
return &event
|
||||
return event
|
||||
}
|
||||
|
||||
// roomMicChangedIMAttributes 是 direct IM 和 outbox 补偿的唯一麦位 attributes 映射。
|
||||
@ -60,7 +63,7 @@ func roomMicChangedIMAttributes(event *roomeventsv1.RoomMicChanged) map[string]s
|
||||
// RoomMicChanged/down fact, so downstream duration consumers never need to infer
|
||||
// mic closure from leave/kick/close side effects.
|
||||
func buildMicDownEventForSeat(roomID string, roomVersion int64, now time.Time, actorUserID int64, targetUserID int64, seatNo int32, micSessionID string, reason string) (outbox.Record, error) {
|
||||
return outbox.Build(roomID, "RoomMicChanged", roomVersion, now, roomMicChangedEvent(roomeventsv1.RoomMicChanged{
|
||||
return outbox.Build(roomID, "RoomMicChanged", roomVersion, now, roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
|
||||
ActorUserId: actorUserID,
|
||||
TargetUserId: targetUserID,
|
||||
FromSeat: seatNo,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user