package userapi import ( "net/http" "strings" userv1 "hyapp.local/api/proto/user/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/transport/http/httpkit" ) type cpUserProfileData struct { UserID string `json:"user_id"` DisplayUserID string `json:"display_user_id"` Username string `json:"username"` Avatar string `json:"avatar"` } type cpGiftSnapshotData struct { GiftID string `json:"gift_id"` GiftName string `json:"gift_name"` GiftIconURL string `json:"gift_icon_url"` GiftAnimationURL string `json:"gift_animation_url"` GiftCount int32 `json:"gift_count"` GiftValue int64 `json:"gift_value"` BillingReceiptID string `json:"billing_receipt_id"` } type cpApplicationData struct { ApplicationID string `json:"application_id"` RelationType string `json:"relation_type"` Status string `json:"status"` Requester *cpUserProfileData `json:"requester,omitempty"` Target *cpUserProfileData `json:"target,omitempty"` RoomID string `json:"room_id"` RoomRegionID int64 `json:"room_region_id"` Gift *cpGiftSnapshotData `json:"gift,omitempty"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` ExpiresAtMS int64 `json:"expires_at_ms"` DecidedAtMS int64 `json:"decided_at_ms"` } type cpRelationshipData struct { RelationshipID string `json:"relationship_id"` RelationType string `json:"relation_type"` Status string `json:"status"` Me *cpUserProfileData `json:"me,omitempty"` Partner *cpUserProfileData `json:"partner,omitempty"` IntimacyValue int64 `json:"intimacy_value"` Level int32 `json:"level"` CurrentLevelThreshold int64 `json:"current_level_threshold"` NextLevelThreshold int64 `json:"next_level_threshold"` NeededForNextLevel int64 `json:"needed_for_next_level"` LevelProgressPercent int32 `json:"level_progress_percent"` MaxLevel bool `json:"max_level"` BreakupCostCoins int64 `json:"breakup_cost_coins"` FormedAtMS int64 `json:"formed_at_ms"` EndedAtMS int64 `json:"ended_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type cpRejectRequest struct { Reason string `json:"reason"` } type cpBreakRelationshipRequest struct { CommandID string `json:"command_id"` } // listCPApplications 返回当前用户收到或发出的 CP/兄弟/姐妹申请。 func (h *Handler) listCPApplications(writer http.ResponseWriter, request *http.Request) { if h.userCPClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } page, pageSize, ok := socialPage(request) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.userCPClient.ListCPApplications(request.Context(), &userv1.ListCPApplicationsRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), Direction: strings.TrimSpace(request.URL.Query().Get("direction")), Status: strings.TrimSpace(request.URL.Query().Get("status")), Page: page, PageSize: pageSize, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } applications := make([]cpApplicationData, 0, len(resp.GetApplications())) for _, item := range resp.GetApplications() { applications = append(applications, cpApplicationFromProto(item)) } httpkit.WriteOK(writer, request, map[string]any{ "applications": applications, "total": resp.GetTotal(), "server_time_ms": resp.GetServerTimeMs(), }) } // acceptCPApplication 同意一条申请;同意后同一对用户其他 pending 类型由 user-service 阻断。 func (h *Handler) acceptCPApplication(writer http.ResponseWriter, request *http.Request) { if h.userCPClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } applicationID := strings.TrimSpace(request.PathValue("application_id")) if applicationID == "" { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } resp, err := h.userCPClient.AcceptCPApplication(request.Context(), &userv1.AcceptCPApplicationRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), ApplicationId: applicationID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{ "application": cpApplicationFromProto(resp.GetApplication()), "relationship": cpRelationshipFromProto(resp.GetRelationship()), }) } // rejectCPApplication 拒绝一条申请;拒绝不会删除历史申请,客户端仍可看到 decided 状态。 func (h *Handler) rejectCPApplication(writer http.ResponseWriter, request *http.Request) { if h.userCPClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } applicationID := strings.TrimSpace(request.PathValue("application_id")) if applicationID == "" { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } var body cpRejectRequest if request.Body != nil && request.ContentLength != 0 && !httpkit.Decode(writer, request, &body) { return } resp, err := h.userCPClient.RejectCPApplication(request.Context(), &userv1.RejectCPApplicationRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), ApplicationId: applicationID, Reason: body.Reason, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{"application": cpApplicationFromProto(resp.GetApplication())}) } // listCPRelationships 返回当前用户 active 关系;服务端固定限制一个用户只能同时存在一条关系。 func (h *Handler) listCPRelationships(writer http.ResponseWriter, request *http.Request) { if h.userCPClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } page, pageSize, ok := socialPage(request) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.userCPClient.ListCPRelationships(request.Context(), &userv1.ListCPRelationshipsRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), RelationType: strings.TrimSpace(request.URL.Query().Get("relation_type")), Page: page, PageSize: pageSize, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } relationships := make([]cpRelationshipData, 0, len(resp.GetRelationships())) for _, item := range resp.GetRelationships() { relationships = append(relationships, cpRelationshipFromProto(item)) } httpkit.WriteOK(writer, request, map[string]any{ "relationships": relationships, "total": resp.GetTotal(), }) } // breakCPRelationship 先在 user-service 创建解除占位,再扣钱包,最后确认关系 ended,避免并发请求重复扣费。 func (h *Handler) breakCPRelationship(writer http.ResponseWriter, request *http.Request) { if h.userCPClient == nil || h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } relationshipID := strings.TrimSpace(request.PathValue("relationship_id")) if relationshipID == "" { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } var body cpBreakRelationshipRequest if !httpkit.Decode(writer, request, &body) { return } commandID := strings.TrimSpace(body.CommandID) if commandID == "" { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "command_id is required") return } userID := auth.UserIDFromContext(request.Context()) meta := httpkit.UserMeta(request, "") prepare, err := h.userCPClient.PrepareBreakCPRelationship(request.Context(), &userv1.PrepareBreakCPRelationshipRequest{ Meta: meta, UserId: userID, RelationshipId: relationshipID, CommandId: commandID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } if prepare.GetStatus() == "confirmed" { httpkit.WriteOK(writer, request, map[string]any{ "relationship": cpRelationshipFromProto(prepare.GetRelationship()), "breakup_cost_coins": prepare.GetBreakupCostCoins(), "wallet_transaction_id": prepare.GetWalletTransactionId(), "coin_balance_after": prepare.GetCoinBalanceAfter(), }) return } walletTransactionID := "" coinBalanceAfter := int64(0) cost := prepare.GetBreakupCostCoins() if cost > 0 { walletResp, walletErr := h.walletClient.DebitCPBreakupFee(request.Context(), &walletv1.DebitCPBreakupFeeRequest{ CommandId: commandID, AppCode: meta.GetAppCode(), UserId: userID, RelationshipId: relationshipID, RelationType: prepare.GetRelationship().GetRelationType(), Amount: cost, }) if walletErr != nil { // 钱包失败表示关系不应解除;取消 pending 占位后把真实钱包错误返回给客户端。 _, _ = h.userCPClient.CancelBreakCPRelationship(request.Context(), &userv1.CancelBreakCPRelationshipRequest{ Meta: meta, UserId: userID, RelationshipId: relationshipID, CommandId: commandID, Reason: "wallet_debit_failed", }) httpkit.WriteRPCError(writer, request, walletErr) return } walletTransactionID = walletResp.GetTransactionId() coinBalanceAfter = walletResp.GetCoinBalanceAfter() } confirmed, err := h.userCPClient.ConfirmBreakCPRelationship(request.Context(), &userv1.ConfirmBreakCPRelationshipRequest{ Meta: meta, UserId: userID, RelationshipId: relationshipID, CommandId: commandID, WalletTransactionId: walletTransactionID, PaidCoinAmount: cost, CoinBalanceAfter: coinBalanceAfter, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{ "relationship": cpRelationshipFromProto(confirmed.GetRelationship()), "breakup_cost_coins": confirmed.GetBreakupCostCoins(), "wallet_transaction_id": confirmed.GetWalletTransactionId(), "coin_balance_after": confirmed.GetCoinBalanceAfter(), }) } func cpApplicationFromProto(item *userv1.CPApplication) cpApplicationData { if item == nil { return cpApplicationData{} } return cpApplicationData{ ApplicationID: item.GetApplicationId(), RelationType: item.GetRelationType(), Status: item.GetStatus(), Requester: cpUserProfileFromProto(item.GetRequester()), Target: cpUserProfileFromProto(item.GetTarget()), RoomID: item.GetRoomId(), RoomRegionID: item.GetRoomRegionId(), Gift: cpGiftSnapshotFromProto(item.GetGift()), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), ExpiresAtMS: item.GetExpiresAtMs(), DecidedAtMS: item.GetDecidedAtMs(), } } func cpRelationshipFromProto(item *userv1.CPRelationship) cpRelationshipData { if item == nil { return cpRelationshipData{} } return cpRelationshipData{ RelationshipID: item.GetRelationshipId(), RelationType: item.GetRelationType(), Status: item.GetStatus(), Me: cpUserProfileFromProto(item.GetMe()), Partner: cpUserProfileFromProto(item.GetPartner()), IntimacyValue: item.GetIntimacyValue(), Level: item.GetLevel(), CurrentLevelThreshold: item.GetCurrentLevelThreshold(), NextLevelThreshold: item.GetNextLevelThreshold(), NeededForNextLevel: item.GetNeededForNextLevel(), LevelProgressPercent: item.GetLevelProgressPercent(), MaxLevel: item.GetMaxLevel(), BreakupCostCoins: item.GetBreakupCostCoins(), FormedAtMS: item.GetFormedAtMs(), EndedAtMS: item.GetEndedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), } } func cpUserProfileFromProto(item *userv1.CPUserProfile) *cpUserProfileData { if item == nil || item.GetUserId() <= 0 { return nil } return &cpUserProfileData{ UserID: userIDString(item.GetUserId()), DisplayUserID: item.GetDisplayUserId(), Username: item.GetUsername(), Avatar: item.GetAvatar(), } } func cpGiftSnapshotFromProto(item *userv1.CPGiftSnapshot) *cpGiftSnapshotData { if item == nil { return nil } return &cpGiftSnapshotData{ GiftID: item.GetGiftId(), GiftName: item.GetGiftName(), GiftIconURL: item.GetGiftIconUrl(), GiftAnimationURL: item.GetGiftAnimationUrl(), GiftCount: item.GetGiftCount(), GiftValue: item.GetGiftValue(), BillingReceiptID: item.GetBillingReceiptId(), } }