2026-06-08 19:39:22 +08:00

352 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(),
}
}