diff --git a/backend/go-service/app/contact/controller/contact_controller.go b/backend/go-service/app/contact/controller/contact_controller.go new file mode 100644 index 0000000..e23724f --- /dev/null +++ b/backend/go-service/app/contact/controller/contact_controller.go @@ -0,0 +1,448 @@ +// Package controller 提供 contact 模块的 HTTP 接口处理 +package controller + +import ( + "strconv" + + "github.com/echochat/backend/app/contact/service" + "github.com/echochat/backend/app/dto" + "github.com/echochat/backend/pkg/middleware" + "github.com/echochat/backend/pkg/utils" + "github.com/gin-gonic/gin" +) + +// ContactController 联系人控制器 +type ContactController struct { + contactService *service.ContactService +} + +// NewContactController 创建 ContactController 实例 +func NewContactController(contactService *service.ContactService) *ContactController { + return &ContactController{contactService: contactService} +} + +// GetFriendList 获取好友列表 +// GET /api/v1/contacts?group_id=xx +func (ctl *ContactController) GetFriendList(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + var groupID *int64 + if gid := c.Query("group_id"); gid != "" { + id, err := strconv.ParseInt(gid, 10, 64) + if err == nil { + groupID = &id + } + } + + friends, err := ctl.contactService.GetFriendList(ctx, userID, groupID) + if err != nil { + utils.ResponseError(c, "获取好友列表失败") + return + } + utils.ResponseOK(c, friends) +} + +// SendFriendRequest 发送好友申请 +// POST /api/v1/contacts/request +func (ctl *ContactController) SendFriendRequest(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + var req dto.SendFriendRequestReq + if err := c.ShouldBindJSON(&req); err != nil { + utils.ResponseBadRequest(c, "请求参数错误: "+err.Error()) + return + } + + err := ctl.contactService.SendFriendRequest(ctx, userID, req.TargetID, req.Message) + if err != nil { + ctl.handleError(c, err) + return + } + utils.ResponseOK(c, nil) +} + +// AcceptFriendRequest 接受好友申请 +// POST /api/v1/contacts/accept +func (ctl *ContactController) AcceptFriendRequest(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + var req dto.HandleFriendRequestReq + if err := c.ShouldBindJSON(&req); err != nil { + utils.ResponseBadRequest(c, "请求参数错误: "+err.Error()) + return + } + + err := ctl.contactService.AcceptFriendRequest(ctx, req.RequestID, userID) + if err != nil { + ctl.handleError(c, err) + return + } + utils.ResponseOK(c, nil) +} + +// RejectFriendRequest 拒绝好友申请 +// POST /api/v1/contacts/reject +func (ctl *ContactController) RejectFriendRequest(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + var req dto.HandleFriendRequestReq + if err := c.ShouldBindJSON(&req); err != nil { + utils.ResponseBadRequest(c, "请求参数错误: "+err.Error()) + return + } + + err := ctl.contactService.RejectFriendRequest(ctx, req.RequestID, userID) + if err != nil { + ctl.handleError(c, err) + return + } + utils.ResponseOK(c, nil) +} + +// GetPendingRequests 获取待处理的好友申请列表 +// GET /api/v1/contacts/requests +func (ctl *ContactController) GetPendingRequests(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + requests, err := ctl.contactService.GetPendingRequests(ctx, userID) + if err != nil { + utils.ResponseError(c, "获取好友申请列表失败") + return + } + utils.ResponseOK(c, requests) +} + +// DeleteFriend 删除好友 +// DELETE /api/v1/contacts/:id +func (ctl *ContactController) DeleteFriend(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + friendID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + utils.ResponseBadRequest(c, "好友 ID 格式错误") + return + } + + if err := ctl.contactService.DeleteFriend(ctx, userID, friendID); err != nil { + utils.ResponseError(c, "删除好友失败") + return + } + utils.ResponseOK(c, nil) +} + +// UpdateRemark 设置好友备注 +// PUT /api/v1/contacts/:id/remark +func (ctl *ContactController) UpdateRemark(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + friendID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + utils.ResponseBadRequest(c, "好友 ID 格式错误") + return + } + + var req dto.UpdateRemarkReq + if err := c.ShouldBindJSON(&req); err != nil { + utils.ResponseBadRequest(c, "请求参数错误: "+err.Error()) + return + } + + if err := ctl.contactService.UpdateRemark(ctx, userID, friendID, req.Remark); err != nil { + utils.ResponseError(c, "更新备注失败") + return + } + utils.ResponseOK(c, nil) +} + +// BlockUser 拉黑用户 +// POST /api/v1/contacts/block +func (ctl *ContactController) BlockUser(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + var req dto.BlockUserReq + if err := c.ShouldBindJSON(&req); err != nil { + utils.ResponseBadRequest(c, "请求参数错误: "+err.Error()) + return + } + + err := ctl.contactService.BlockUser(ctx, userID, req.TargetID) + if err != nil { + ctl.handleError(c, err) + return + } + utils.ResponseOK(c, nil) +} + +// UnblockUser 取消拉黑 +// DELETE /api/v1/contacts/block/:user_id +func (ctl *ContactController) UnblockUser(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + targetID, err := strconv.ParseInt(c.Param("user_id"), 10, 64) + if err != nil { + utils.ResponseBadRequest(c, "用户 ID 格式错误") + return + } + + if err := ctl.contactService.UnblockUser(ctx, userID, targetID); err != nil { + utils.ResponseError(c, "取消拉黑失败") + return + } + utils.ResponseOK(c, nil) +} + +// GetBlockList 获取黑名单列表 +// GET /api/v1/contacts/block +func (ctl *ContactController) GetBlockList(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + blocked, err := ctl.contactService.GetBlockList(ctx, userID) + if err != nil { + utils.ResponseError(c, "获取黑名单失败") + return + } + utils.ResponseOK(c, blocked) +} + +// SearchUsers 搜索用户 +// GET /api/v1/users/search?keyword=xx&page=1&page_size=20 +func (ctl *ContactController) SearchUsers(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + keyword := c.Query("keyword") + if keyword == "" { + utils.ResponseBadRequest(c, "搜索关键词不能为空") + return + } + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + + users, total, err := ctl.contactService.SearchUsers(ctx, userID, keyword, page, pageSize) + if err != nil { + utils.ResponseError(c, "搜索用户失败") + return + } + utils.ResponseOK(c, gin.H{ + "list": users, + "total": total, + }) +} + +// GetRecommendFriends 好友推荐 +// GET /api/v1/contacts/recommend +func (ctl *ContactController) GetRecommendFriends(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + friends, err := ctl.contactService.GetRecommendFriends(ctx, userID) + if err != nil { + utils.ResponseError(c, "获取好友推荐失败") + return + } + utils.ResponseOK(c, friends) +} + +// GetGroups 获取好友分组列表 +// GET /api/v1/contacts/groups +func (ctl *ContactController) GetGroups(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + groups, err := ctl.contactService.GetGroups(ctx, userID) + if err != nil { + utils.ResponseError(c, "获取分组列表失败") + return + } + utils.ResponseOK(c, groups) +} + +// CreateGroup 创建好友分组 +// POST /api/v1/contacts/groups +func (ctl *ContactController) CreateGroup(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + var req dto.CreateGroupReq + if err := c.ShouldBindJSON(&req); err != nil { + utils.ResponseBadRequest(c, "请求参数错误: "+err.Error()) + return + } + + group, err := ctl.contactService.CreateGroup(ctx, userID, req.Name) + if err != nil { + utils.ResponseError(c, "创建分组失败") + return + } + utils.ResponseCreated(c, group) +} + +// UpdateGroup 修改好友分组 +// PUT /api/v1/contacts/groups/:id +func (ctl *ContactController) UpdateGroup(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + utils.ResponseBadRequest(c, "分组 ID 格式错误") + return + } + + var req dto.UpdateGroupReq + if err := c.ShouldBindJSON(&req); err != nil { + utils.ResponseBadRequest(c, "请求参数错误: "+err.Error()) + return + } + + if err := ctl.contactService.UpdateGroup(ctx, userID, groupID, req.Name, req.SortOrder); err != nil { + utils.ResponseError(c, "修改分组失败") + return + } + utils.ResponseOK(c, nil) +} + +// DeleteGroup 删除好友分组 +// DELETE /api/v1/contacts/groups/:id +func (ctl *ContactController) DeleteGroup(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + utils.ResponseBadRequest(c, "分组 ID 格式错误") + return + } + + if err := ctl.contactService.DeleteGroup(ctx, userID, groupID); err != nil { + utils.ResponseError(c, "删除分组失败") + return + } + utils.ResponseOK(c, nil) +} + +// MoveToGroup 移动好友到分组 +// PUT /api/v1/contacts/:id/group +func (ctl *ContactController) MoveToGroup(c *gin.Context) { + ctx := c.Request.Context() + userID, ok := middleware.GetCurrentUserID(c) + if !ok { + utils.ResponseUnauthorized(c, "无法获取当前用户信息") + return + } + + friendID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + utils.ResponseBadRequest(c, "好友 ID 格式错误") + return + } + + var req dto.MoveToGroupReq + if err := c.ShouldBindJSON(&req); err != nil { + utils.ResponseBadRequest(c, "请求参数错误: "+err.Error()) + return + } + + if err := ctl.contactService.MoveToGroup(ctx, userID, friendID, req.GroupID); err != nil { + utils.ResponseError(c, "移动好友到分组失败") + return + } + utils.ResponseOK(c, nil) +} + +// handleError 统一错误处理 +func (ctl *ContactController) handleError(c *gin.Context, err error) { + switch err { + case service.ErrSelfRequest: + utils.ResponseBadRequest(c, err.Error()) + case service.ErrAlreadyFriend: + utils.ResponseBadRequest(c, err.Error()) + case service.ErrPendingExists: + utils.ResponseBadRequest(c, err.Error()) + case service.ErrBlocked: + utils.ResponseForbidden(c, err.Error()) + case service.ErrRequestNotFound: + utils.ResponseNotFound(c, err.Error()) + case service.ErrFriendNotFound: + utils.ResponseNotFound(c, err.Error()) + case service.ErrGroupNotFound: + utils.ResponseNotFound(c, err.Error()) + default: + utils.ResponseError(c, "服务内部错误") + } +} diff --git a/backend/go-service/app/contact/provider.go b/backend/go-service/app/contact/provider.go new file mode 100644 index 0000000..dc13d8c --- /dev/null +++ b/backend/go-service/app/contact/provider.go @@ -0,0 +1,16 @@ +package contact + +import ( + "github.com/echochat/backend/app/contact/controller" + "github.com/echochat/backend/app/contact/dao" + "github.com/echochat/backend/app/contact/service" + "github.com/google/wire" +) + +// ContactSet Contact 模块依赖注入 Provider Set +var ContactSet = wire.NewSet( + dao.NewFriendshipDAO, + dao.NewFriendGroupDAO, + service.NewContactService, + controller.NewContactController, +) diff --git a/backend/go-service/app/contact/router.go b/backend/go-service/app/contact/router.go new file mode 100644 index 0000000..e9152af --- /dev/null +++ b/backend/go-service/app/contact/router.go @@ -0,0 +1,39 @@ +// Package contact 提供联系人/好友管理模块 +package contact + +import ( + "github.com/echochat/backend/app/contact/controller" + "github.com/gin-gonic/gin" +) + +// RegisterRoutes 注册 contact 模块的所有路由(需要 JWT 中间件) +func RegisterRoutes(r *gin.Engine, ctrl *controller.ContactController, jwtAuth gin.HandlerFunc) { + authed := r.Group("/api/v1") + authed.Use(jwtAuth) + { + // 好友关系 + authed.GET("/contacts", ctrl.GetFriendList) + authed.POST("/contacts/request", ctrl.SendFriendRequest) + authed.POST("/contacts/accept", ctrl.AcceptFriendRequest) + authed.POST("/contacts/reject", ctrl.RejectFriendRequest) + authed.DELETE("/contacts/:id", ctrl.DeleteFriend) + authed.PUT("/contacts/:id/remark", ctrl.UpdateRemark) + authed.GET("/contacts/requests", ctrl.GetPendingRequests) + + // 好友分组 + authed.GET("/contacts/groups", ctrl.GetGroups) + authed.POST("/contacts/groups", ctrl.CreateGroup) + authed.PUT("/contacts/groups/:id", ctrl.UpdateGroup) + authed.DELETE("/contacts/groups/:id", ctrl.DeleteGroup) + authed.PUT("/contacts/:id/group", ctrl.MoveToGroup) + + // 黑名单 + authed.POST("/contacts/block", ctrl.BlockUser) + authed.DELETE("/contacts/block/:user_id", ctrl.UnblockUser) + authed.GET("/contacts/block", ctrl.GetBlockList) + + // 搜索与推荐 + authed.GET("/users/search", ctrl.SearchUsers) + authed.GET("/contacts/recommend", ctrl.GetRecommendFriends) + } +} diff --git a/backend/go-service/app/provider/provider.go b/backend/go-service/app/provider/provider.go index f287489..63e2289 100644 --- a/backend/go-service/app/provider/provider.go +++ b/backend/go-service/app/provider/provider.go @@ -6,6 +6,7 @@ import ( adminController "github.com/echochat/backend/app/admin/controller" authController "github.com/echochat/backend/app/auth/controller" "github.com/echochat/backend/app/auth/service" + contactController "github.com/echochat/backend/app/contact/controller" wsApp "github.com/echochat/backend/app/ws" "github.com/echochat/backend/config" "github.com/echochat/backend/pkg/db" @@ -27,6 +28,7 @@ type App struct { WSHandler *wsApp.Handler // WebSocket 连接处理器 Hub *ws.Hub // WebSocket Hub 连接管理 PubSub *ws.PubSub // Redis Pub/Sub 消息路由 + ContactController *contactController.ContactController // 联系人控制器 } // NewApp 创建应用实例 @@ -41,6 +43,7 @@ func NewApp( wsHandler *wsApp.Handler, hub *ws.Hub, pubsub *ws.PubSub, + contactCtrl *contactController.ContactController, ) *App { return &App{ Config: cfg, @@ -53,6 +56,7 @@ func NewApp( WSHandler: wsHandler, Hub: hub, PubSub: pubsub, + ContactController: contactCtrl, } } diff --git a/backend/go-service/app/provider/wire.go b/backend/go-service/app/provider/wire.go index 15eeea7..9e88c87 100644 --- a/backend/go-service/app/provider/wire.go +++ b/backend/go-service/app/provider/wire.go @@ -6,6 +6,7 @@ package provider import ( "github.com/echochat/backend/app/admin" "github.com/echochat/backend/app/auth" + "github.com/echochat/backend/app/contact" wsApp "github.com/echochat/backend/app/ws" "github.com/echochat/backend/config" "github.com/google/wire" @@ -18,6 +19,7 @@ func InitializeApp(cfg *config.Config) (*App, error) { auth.AuthSet, admin.AdminSet, wsApp.WSSet, + contact.ContactSet, ) return nil, nil } diff --git a/backend/go-service/app/provider/wire_gen.go b/backend/go-service/app/provider/wire_gen.go index 4c13c32..ee6a610 100644 --- a/backend/go-service/app/provider/wire_gen.go +++ b/backend/go-service/app/provider/wire_gen.go @@ -13,6 +13,9 @@ import ( "github.com/echochat/backend/app/auth/controller" "github.com/echochat/backend/app/auth/dao" "github.com/echochat/backend/app/auth/service" + controller3 "github.com/echochat/backend/app/contact/controller" + dao3 "github.com/echochat/backend/app/contact/dao" + service3 "github.com/echochat/backend/app/contact/service" "github.com/echochat/backend/app/ws" "github.com/echochat/backend/config" "github.com/echochat/backend/pkg/db" @@ -45,6 +48,10 @@ func InitializeApp(cfg *config.Config) (*App, error) { hub := ws.ProvideHub() pubSub := ws.ProvidePubSub(client, hub) handler := ws.ProvideWSHandler(hub, pubSub, jwtConfig) - app := NewApp(cfg, gormDB, client, authService, authController, adminAuthController, userManageController, handler, hub, pubSub) + friendshipDAO := dao3.NewFriendshipDAO(gormDB) + friendGroupDAO := dao3.NewFriendGroupDAO(gormDB) + contactService := service3.NewContactService(friendshipDAO, friendGroupDAO, pubSub) + contactController := controller3.NewContactController(contactService) + app := NewApp(cfg, gormDB, client, authService, authController, adminAuthController, userManageController, handler, hub, pubSub, contactController) return app, nil } diff --git a/backend/go-service/router/router.go b/backend/go-service/router/router.go index 6a28357..6be4369 100644 --- a/backend/go-service/router/router.go +++ b/backend/go-service/router/router.go @@ -8,6 +8,7 @@ import ( "github.com/echochat/backend/app/admin" "github.com/echochat/backend/app/auth" + "github.com/echochat/backend/app/contact" "github.com/echochat/backend/app/provider" wsApp "github.com/echochat/backend/app/ws" "github.com/echochat/backend/pkg/middleware" @@ -34,6 +35,7 @@ func Setup(engine *gin.Engine, app *provider.App) { auth.RegisterRoutes(engine, app.AuthController, app.AdminAuthController, jwtAuth) admin.RegisterRoutes(engine, app.UserManageController, jwtAuth) wsApp.RegisterRoutes(engine, app.WSHandler) + contact.RegisterRoutes(engine, app.ContactController, jwtAuth) // [未来] im.RegisterRoutes(engine, app.ImController, jwtAuth) // [未来] meeting.RegisterRoutes(engine, app.MeetingController, jwtAuth)