package userapi import ( "hyapp/services/gateway-service/internal/transport/http/httpkit" "net/http" "strings" userv1 "hyapp.local/api/proto/user/v1" "hyapp/services/gateway-service/internal/auth" ) type profileVisitRecordData struct { VisitorUserID string `json:"visitor_user_id"` TargetUserID string `json:"target_user_id"` VisitCount int64 `json:"visit_count"` LastVisitedAtMS int64 `json:"last_visited_at_ms"` } type followRecordData struct { FollowerUserID string `json:"follower_user_id"` FolloweeUserID string `json:"followee_user_id"` FollowedAtMS int64 `json:"followed_at_ms"` } type friendRecordData struct { UserID string `json:"user_id"` FriendUserID string `json:"friend_user_id"` FriendedAtMS int64 `json:"friended_at_ms"` } type friendApplicationData struct { RequesterUserID string `json:"requester_user_id"` TargetUserID string `json:"target_user_id"` Status string `json:"status"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } // listMyProfileVisitors 返回访问我的主页的人;这是关系 read model,不实时扫用户行为日志。 func (h *Handler) listMyProfileVisitors(writer http.ResponseWriter, request *http.Request) { if h.userSocialClient == 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.userSocialClient.ListProfileVisitors(request.Context(), &userv1.ListProfileVisitorsRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), Page: page, PageSize: pageSize, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } records := make([]profileVisitRecordData, 0, len(resp.GetRecords())) for _, record := range resp.GetRecords() { records = append(records, profileVisitData(record)) } httpkit.WriteOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()}) } // listMyFollowing 返回我关注的用户列表;关注我的列表后续单独建接口,避免语义混淆。 func (h *Handler) listMyFollowing(writer http.ResponseWriter, request *http.Request) { if h.userSocialClient == 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.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), Page: page, PageSize: pageSize, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } records := make([]followRecordData, 0, len(resp.GetRecords())) for _, record := range resp.GetRecords() { records = append(records, followData(record)) } httpkit.WriteOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()}) } // listMyFriends 返回双向好友列表。 func (h *Handler) listMyFriends(writer http.ResponseWriter, request *http.Request) { if h.userSocialClient == 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.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), Page: page, PageSize: pageSize, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } records := make([]friendRecordData, 0, len(resp.GetRecords())) for _, record := range resp.GetRecords() { records = append(records, friendData(record)) } httpkit.WriteOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()}) } // listMyFriendApplications 返回待处理申请;direction=incoming/outgoing。 func (h *Handler) listMyFriendApplications(writer http.ResponseWriter, request *http.Request) { if h.userSocialClient == 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.userSocialClient.ListFriendApplications(request.Context(), &userv1.ListFriendApplicationsRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), Direction: strings.TrimSpace(request.URL.Query().Get("direction")), Page: page, PageSize: pageSize, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } applications := make([]friendApplicationData, 0, len(resp.GetApplications())) for _, application := range resp.GetApplications() { applications = append(applications, friendApplicationDataFromProto(application)) } httpkit.WriteOK(writer, request, map[string]any{"applications": applications, "total": resp.GetTotal()}) } // userSocialAction 处理 /users/{user_id}/visit|follow|friend-requests|friend-requests/accept|friend。 func (h *Handler) userSocialAction(writer http.ResponseWriter, request *http.Request) { if h.userSocialClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } targetUserID, ok := httpkit.PositiveInt64PathValue(request, "user_id") if !ok { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } action := strings.Trim(request.PathValue("action"), "/") switch action { case "visit": h.recordProfileVisit(writer, request, targetUserID) case "follow": h.handleFollowAction(writer, request, targetUserID) case "friend-requests": h.applyFriend(writer, request, targetUserID) case "friend-requests/accept": h.acceptFriendApplication(writer, request, targetUserID) case "friend": h.deleteFriend(writer, request, targetUserID) default: httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") } } func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.Request, targetUserID int64) { if request.Method != http.MethodPost { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } resp, err := h.userSocialClient.RecordProfileVisit(request.Context(), &userv1.RecordProfileVisitRequest{ Meta: httpkit.UserMeta(request, ""), VisitorUserId: auth.UserIDFromContext(request.Context()), TargetUserId: targetUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } data := map[string]any{"recorded": resp.GetRecorded(), "target_stats": overviewStatsFromProto(resp.GetTargetStats())} if h.userProfileClient != nil { profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{ Meta: httpkit.UserMeta(request, ""), UserId: targetUserID, }) if err == nil { // 访问记录归 social read model,资料卡展示归 user-service 用户主资料;这里仅拼装 App 打开别人资料卡所需的当前快照。 data["target_profile"] = profileData(profileResp.GetUser(), 0) } } httpkit.WriteOK(writer, request, data) } func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.Request, followeeUserID int64) { userID := auth.UserIDFromContext(request.Context()) switch request.Method { case http.MethodPost: resp, err := h.userSocialClient.FollowUser(request.Context(), &userv1.FollowUserRequest{ Meta: httpkit.UserMeta(request, ""), FollowerUserId: userID, FolloweeUserId: followeeUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{"following": resp.GetFollowing(), "follower_stats": overviewStatsFromProto(resp.GetFollowerStats())}) case http.MethodDelete: resp, err := h.userSocialClient.UnfollowUser(request.Context(), &userv1.UnfollowUserRequest{ Meta: httpkit.UserMeta(request, ""), FollowerUserId: userID, FolloweeUserId: followeeUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{"following": resp.GetFollowing(), "follower_stats": overviewStatsFromProto(resp.GetFollowerStats())}) default: httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") } } func (h *Handler) applyFriend(writer http.ResponseWriter, request *http.Request, targetUserID int64) { if request.Method != http.MethodPost { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } resp, err := h.userSocialClient.ApplyFriend(request.Context(), &userv1.ApplyFriendRequest{ Meta: httpkit.UserMeta(request, ""), RequesterUserId: auth.UserIDFromContext(request.Context()), TargetUserId: targetUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{"application": friendApplicationDataFromProto(resp.GetApplication()), "already_friends": resp.GetAlreadyFriends()}) } func (h *Handler) acceptFriendApplication(writer http.ResponseWriter, request *http.Request, requesterUserID int64) { if request.Method != http.MethodPost { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } resp, err := h.userSocialClient.AcceptFriendApplication(request.Context(), &userv1.AcceptFriendApplicationRequest{ Meta: httpkit.UserMeta(request, ""), AccepterUserId: auth.UserIDFromContext(request.Context()), RequesterUserId: requesterUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{"friend": friendData(resp.GetFriend())}) } func (h *Handler) deleteFriend(writer http.ResponseWriter, request *http.Request, friendUserID int64) { if request.Method != http.MethodDelete { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } resp, err := h.userSocialClient.DeleteFriend(request.Context(), &userv1.DeleteFriendRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), FriendUserId: friendUserID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{"deleted": resp.GetDeleted()}) } func socialPage(request *http.Request) (int32, int32, bool) { page, ok := httpkit.PositiveInt32Query(request, "page", 1) if !ok { return 0, 0, false } pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20) if !ok { return 0, 0, false } return page, pageSize, true } func profileVisitData(record *userv1.ProfileVisitRecord) profileVisitRecordData { if record == nil { return profileVisitRecordData{} } return profileVisitRecordData{ VisitorUserID: userIDString(record.GetVisitorUserId()), TargetUserID: userIDString(record.GetTargetUserId()), VisitCount: record.GetVisitCount(), LastVisitedAtMS: record.GetLastVisitedAtMs(), } } func followData(record *userv1.FollowRecord) followRecordData { if record == nil { return followRecordData{} } return followRecordData{ FollowerUserID: userIDString(record.GetFollowerUserId()), FolloweeUserID: userIDString(record.GetFolloweeUserId()), FollowedAtMS: record.GetFollowedAtMs(), } } func friendData(record *userv1.FriendRecord) friendRecordData { if record == nil { return friendRecordData{} } return friendRecordData{ UserID: userIDString(record.GetUserId()), FriendUserID: userIDString(record.GetFriendUserId()), FriendedAtMS: record.GetFriendedAtMs(), } } func friendApplicationDataFromProto(application *userv1.FriendApplication) friendApplicationData { if application == nil { return friendApplicationData{} } return friendApplicationData{ RequesterUserID: userIDString(application.GetRequesterUserId()), TargetUserID: userIDString(application.GetTargetUserId()), Status: application.GetStatus(), CreatedAtMS: application.GetCreatedAtMs(), UpdatedAtMS: application.GetUpdatedAtMs(), } }