feat(phase2e-2): Go meeting 模块 DDL + Model + DAO + service/controller/router 骨架(Task 3+4)
Task 3 - 数据库与持久化层:
- init.sql 追加 3 张表(meeting_rooms / meeting_participants / meeting_chats)+ 9 索引 + 全量 COMMENT
- 新增 phase2e2_migration.sql 增量升级脚本(IF NOT EXISTS 幂等)
- app/constants/meeting.go 统一会议常量(类型/状态/角色/结束原因/离会原因/默认配置/WS 事件 8 组)
- app/meeting/model 3 个 model + GORM tag(TIMESTAMP(0) 对齐现有表风格)
- app/meeting/dao 3 个 DAO 共 24 个方法:
* meeting_room_dao:Create/Get/Exists/MarkStarted/MarkEnded(乐观锁)/UpdateHost/UpdateSettings/ListByHost/ListExpiredForCleanup
* meeting_participant_dao:JoinRoom(事务内复用旧记录)/LeaveRoom(EXTRACT(EPOCH) DB 时间)/LeaveAllActive/TransferHost(事务)/FindActiveByUser(JOIN 单点校验)/列表计数更新
* meeting_chat_dao:Create/ListByRoom(游标)/DeleteByRoomIDs/CountByRoom
- psql 真库跑通 CRUD + UNIQUE + CASCADE + 主持人转让事务 + duration=10s 精确匹配 8 场景
Task 4 - Go 侧 service/controller/router/wire 骨架:
- app/meeting/service/interfaces.go:NotifyPusher / UserInfoResolver / OnlineChecker 三接口(解耦 notify/contact/ws)
- app/meeting/service/meeting_service.go:MeetingService + 8 sentinel error + 17 个空方法(返回 ErrNotImplemented)
- app/meeting/controller/meeting_controller.go:12 个 Gin handler + responseNotImplemented(501)
- app/meeting/router.go:12 条路由挂 /api/v1/meeting/* 并套 jwtAuth(扁平化 router.go,与 group/notify 一致)
- app/meeting/provider.go:MeetingSet = wire.NewSet(DAO×3, Service, Controller)
- 全局 wire.go 挂入 MeetingSet + 3 条 wire.Bind(Notify/UserInfo/Online);App struct/NewApp 加字段;router/router.go 挂载
- 存量修复:admin/provider.go 补齐 MessageManage{DAO,Service,Controller} 解决旧版 wire 重生成报 no provider found
- 验证:go build ./... / go vet ./... / wire ./app/provider 零错误;GIN_MODE=debug 启动打印全部 12 条路由;无 token curl 3 条代表性路由均返回 401(JWT 中间件生效)
关键风格修正(偏离草案):
- 常量归入 app/constants/meeting.go 单文件(与 group.go / notify.go 同构),非草案 app/meeting/constants/
- TIMESTAMP(0) 取代草案 TIMESTAMPTZ 对齐所有现有表
- 冗余 idx_meeting_rooms_code 移除(UNIQUE 已自动建索引)
- router/provider 目录扁平化(app/meeting/router.go / app/meeting/provider.go)
- 延续项目 Go 侧"零 _test.go"风格,用代码审查 + psql 真库验证 + Playwright E2E 三层守护
文档同步:
- docs/progress/CURRENT_STATUS.md 新增 Task 3 / Task 4 交付段落
- .cursor/rules/project-context.mdc Phase 2e-2 进度推进至 Task 0-4 ✅
- docs/plans/2026-04-21-phase2e-2-implementation.plan.md Task 3 / Task 4 标记完成 + 实际产出 vs 计划差异
Made-with: Cursor
This commit is contained in:
@@ -20,4 +20,7 @@ var AdminSet = wire.NewSet(
|
||||
controller.NewContactManageController,
|
||||
service.NewGroupManageService,
|
||||
controller.NewGroupManageController,
|
||||
dao.NewMessageManageDAO,
|
||||
service.NewMessageManageService,
|
||||
controller.NewMessageManageController,
|
||||
)
|
||||
|
||||
114
backend/go-service/app/constants/meeting.go
Normal file
114
backend/go-service/app/constants/meeting.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package constants
|
||||
|
||||
// 会议相关常量(Phase 2e-2)
|
||||
// 包含:会议类型、会议状态、参与者角色、结束原因、离会原因、默认配置
|
||||
// 所有枚举值严禁直接用中文字符串,中文仅用于 *Map 展示
|
||||
|
||||
// 会议类型(meeting_rooms.type)
|
||||
const (
|
||||
MeetingTypeInstant = 1 // 即时会议(MVP 仅此)
|
||||
MeetingTypeScheduled = 2 // 预约会议(Phase 2e-3)
|
||||
)
|
||||
|
||||
// MeetingTypeMap 会议类型中文映射
|
||||
var MeetingTypeMap = map[int]string{
|
||||
MeetingTypeInstant: "即时会议",
|
||||
MeetingTypeScheduled: "预约会议",
|
||||
}
|
||||
|
||||
// 会议状态(meeting_rooms.status)
|
||||
const (
|
||||
MeetingStatusPending = 0 // 未开始(仅预约会议使用)
|
||||
MeetingStatusActive = 1 // 进行中
|
||||
MeetingStatusEnded = 2 // 已结束
|
||||
)
|
||||
|
||||
// MeetingStatusMap 会议状态中文映射
|
||||
var MeetingStatusMap = map[int]string{
|
||||
MeetingStatusPending: "未开始",
|
||||
MeetingStatusActive: "进行中",
|
||||
MeetingStatusEnded: "已结束",
|
||||
}
|
||||
|
||||
// 会议参与者角色(meeting_participants.role)
|
||||
// MVP 仅使用 Participant / Host 两档,CoHost 保留至第二期
|
||||
const (
|
||||
MeetingRoleParticipant = 0 // 普通参会人
|
||||
MeetingRoleHost = 1 // 主持人
|
||||
MeetingRoleCoHost = 2 // 联合主持人(保留,MVP 不用)
|
||||
)
|
||||
|
||||
// MeetingRoleMap 参与者角色中文映射
|
||||
var MeetingRoleMap = map[int]string{
|
||||
MeetingRoleParticipant: "参会人",
|
||||
MeetingRoleHost: "主持人",
|
||||
MeetingRoleCoHost: "联合主持人",
|
||||
}
|
||||
|
||||
// 会议结束原因(meeting_rooms.ended_reason)
|
||||
const (
|
||||
MeetingEndedReasonHostEnded = "host_ended" // 主持人主动结束
|
||||
MeetingEndedReasonEmptyTTL = "empty_ttl" // 空房超时销毁
|
||||
MeetingEndedReasonAdminForce = "admin_force" // 管理员强制结束
|
||||
MeetingEndedReasonSystemError = "system_error" // 系统异常(如 Node worker died)
|
||||
)
|
||||
|
||||
// MeetingEndedReasonMap 结束原因中文映射
|
||||
var MeetingEndedReasonMap = map[string]string{
|
||||
MeetingEndedReasonHostEnded: "主持人结束",
|
||||
MeetingEndedReasonEmptyTTL: "空房超时",
|
||||
MeetingEndedReasonAdminForce: "管理员强制结束",
|
||||
MeetingEndedReasonSystemError: "系统异常",
|
||||
}
|
||||
|
||||
// 离会原因(meeting_participants.left_reason)
|
||||
const (
|
||||
MeetingLeftReasonSelf = "self" // 主动离会
|
||||
MeetingLeftReasonKicked = "kicked" // 被主持人移除
|
||||
MeetingLeftReasonHostEnd = "host_end" // 主持人结束会议
|
||||
MeetingLeftReasonEmptyTTL = "empty_ttl" // 空房 TTL 触发
|
||||
MeetingLeftReasonDisconnect = "disconnect" // 网络断开(宽限期结束仍未重连)
|
||||
)
|
||||
|
||||
// MeetingLeftReasonMap 离会原因中文映射
|
||||
var MeetingLeftReasonMap = map[string]string{
|
||||
MeetingLeftReasonSelf: "主动离开",
|
||||
MeetingLeftReasonKicked: "被移除",
|
||||
MeetingLeftReasonHostEnd: "会议结束",
|
||||
MeetingLeftReasonEmptyTTL: "空房超时",
|
||||
MeetingLeftReasonDisconnect: "网络断开",
|
||||
}
|
||||
|
||||
// 会议默认配置(应用层强制约束)
|
||||
const (
|
||||
MeetingMVPMaxMembers = 8 // MVP 阶段单会议硬上限
|
||||
MeetingRoomCodeLength = 9 // 会议号去连字符后的长度(XXX-XXX-XXX)
|
||||
MeetingRoomCodeRetryMax = 3 // 生成会议号冲突重试上限
|
||||
MeetingPasswordMaxAttempts = 5 // 密码连续错误锁定阈值
|
||||
MeetingPasswordLockSeconds = 600 // 密码锁定时长(10 分钟)
|
||||
MeetingHostGraceSeconds = 120 // 主持人掉线宽限期(2 分钟)
|
||||
MeetingEmptyRoomTTLSeconds = 300 // 空房销毁 TTL(5 分钟)
|
||||
MeetingInviteTokenTTL = 600 // 邀请链接 Token TTL(10 分钟)
|
||||
MeetingChatRetentionHours = 24 // 会议聊天保留时长(会议结束后)
|
||||
)
|
||||
|
||||
// 会议 WS 事件常量(设计文档 §6.3,11 个事件)
|
||||
// 命名格式:meeting.{domain}.{action}
|
||||
const (
|
||||
// 房间级事件
|
||||
MeetingWSEventRoomEnded = "meeting.room.ended" // 会议已结束
|
||||
MeetingWSEventRoomLocked = "meeting.room.locked" // 会议被锁定(等候室,Phase 2e-3)
|
||||
MeetingWSEventRoomHostChange = "meeting.room.host.changed" // 主持人变更
|
||||
|
||||
// 成员级事件
|
||||
MeetingWSEventMemberJoined = "meeting.member.joined" // 新成员加入
|
||||
MeetingWSEventMemberLeft = "meeting.member.left" // 成员离开
|
||||
MeetingWSEventMemberStateChange = "meeting.member.state.changed" // 麦克风 / 摄像头状态变化
|
||||
MeetingWSEventMemberKicked = "meeting.member.kicked" // 被移出
|
||||
|
||||
// 媒体级事件(Go 发起,驱动客户端 mediasoup-client 订阅/取消)
|
||||
MeetingWSEventProducerNew = "meeting.producer.new" // 有新 producer 可订阅
|
||||
MeetingWSEventProducerClosed = "meeting.producer.closed" // producer 已关闭
|
||||
MeetingWSEventConsumerResumed = "meeting.consumer.resumed" // consumer 已恢复(由服务端触发)
|
||||
MeetingWSEventChatMessage = "meeting.chat.message" // 会议内文字聊天
|
||||
)
|
||||
138
backend/go-service/app/meeting/controller/meeting_controller.go
Normal file
138
backend/go-service/app/meeting/controller/meeting_controller.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Package controller 提供 meeting 模块的 HTTP 接口
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/echochat/backend/app/meeting/service"
|
||||
"github.com/echochat/backend/pkg/middleware"
|
||||
"github.com/echochat/backend/pkg/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MeetingController 会议 REST 控制器
|
||||
// Task 4 骨架阶段:所有 handler 均返回 501 Not Implemented,便于路由自测与前端 mock
|
||||
// Task 5/6/7 将在此填充请求解析、业务调用与错误映射
|
||||
type MeetingController struct {
|
||||
meetingService *service.MeetingService
|
||||
}
|
||||
|
||||
// NewMeetingController 创建 MeetingController 实例
|
||||
func NewMeetingController(meetingService *service.MeetingService) *MeetingController {
|
||||
return &MeetingController{meetingService: meetingService}
|
||||
}
|
||||
|
||||
// responseNotImplemented Task 4 占位响应,Task 5/6/7 逐接口替换为真实实现
|
||||
func responseNotImplemented(c *gin.Context, endpoint string) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{
|
||||
"code": http.StatusNotImplemented,
|
||||
"message": "接口尚未实现",
|
||||
"endpoint": endpoint,
|
||||
})
|
||||
}
|
||||
|
||||
// requireUserID 统一的当前用户取值,失败直接写 401 并返回 false
|
||||
func requireUserID(c *gin.Context) (int64, bool) {
|
||||
userID, ok := middleware.GetCurrentUserID(c)
|
||||
if !ok {
|
||||
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
|
||||
return 0, false
|
||||
}
|
||||
return userID, true
|
||||
}
|
||||
|
||||
// CreateRoom POST /api/v1/meeting/rooms
|
||||
func (ctl *MeetingController) CreateRoom(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "POST /api/v1/meeting/rooms")
|
||||
}
|
||||
|
||||
// GetRoom GET /api/v1/meeting/rooms/:code
|
||||
func (ctl *MeetingController) GetRoom(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "GET /api/v1/meeting/rooms/:code")
|
||||
}
|
||||
|
||||
// JoinRoom POST /api/v1/meeting/rooms/:code/join
|
||||
func (ctl *MeetingController) JoinRoom(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "POST /api/v1/meeting/rooms/:code/join")
|
||||
}
|
||||
|
||||
// LeaveRoom POST /api/v1/meeting/rooms/:code/leave
|
||||
func (ctl *MeetingController) LeaveRoom(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "POST /api/v1/meeting/rooms/:code/leave")
|
||||
}
|
||||
|
||||
// EndRoom POST /api/v1/meeting/rooms/:code/end
|
||||
func (ctl *MeetingController) EndRoom(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "POST /api/v1/meeting/rooms/:code/end")
|
||||
}
|
||||
|
||||
// ListMyMeetings GET /api/v1/meeting/rooms?role=host|participant
|
||||
func (ctl *MeetingController) ListMyMeetings(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "GET /api/v1/meeting/rooms")
|
||||
}
|
||||
|
||||
// TransferHost POST /api/v1/meeting/rooms/:code/transfer-host
|
||||
func (ctl *MeetingController) TransferHost(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "POST /api/v1/meeting/rooms/:code/transfer-host")
|
||||
}
|
||||
|
||||
// KickMember POST /api/v1/meeting/rooms/:code/kick
|
||||
func (ctl *MeetingController) KickMember(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "POST /api/v1/meeting/rooms/:code/kick")
|
||||
}
|
||||
|
||||
// InviteUsers POST /api/v1/meeting/rooms/:code/invite
|
||||
func (ctl *MeetingController) InviteUsers(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "POST /api/v1/meeting/rooms/:code/invite")
|
||||
}
|
||||
|
||||
// RedeemInvite POST /api/v1/meeting/invites/:token/redeem
|
||||
func (ctl *MeetingController) RedeemInvite(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "POST /api/v1/meeting/invites/:token/redeem")
|
||||
}
|
||||
|
||||
// SendChat POST /api/v1/meeting/rooms/:code/chats
|
||||
func (ctl *MeetingController) SendChat(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "POST /api/v1/meeting/rooms/:code/chats")
|
||||
}
|
||||
|
||||
// ListChats GET /api/v1/meeting/rooms/:code/chats?after_id=&limit=
|
||||
func (ctl *MeetingController) ListChats(c *gin.Context) {
|
||||
if _, ok := requireUserID(c); !ok {
|
||||
return
|
||||
}
|
||||
responseNotImplemented(c, "GET /api/v1/meeting/rooms/:code/chats")
|
||||
}
|
||||
87
backend/go-service/app/meeting/dao/meeting_chat_dao.go
Normal file
87
backend/go-service/app/meeting/dao/meeting_chat_dao.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/echochat/backend/app/meeting/model"
|
||||
"github.com/echochat/backend/pkg/logs"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MeetingChatDAO 会议内文字聊天数据访问对象
|
||||
type MeetingChatDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewMeetingChatDAO 创建实例
|
||||
func NewMeetingChatDAO(db *gorm.DB) *MeetingChatDAO {
|
||||
return &MeetingChatDAO{db: db}
|
||||
}
|
||||
|
||||
// Create 写入一条会议聊天消息
|
||||
// ID / CreatedAt 由数据库自增 + autoCreateTime 填充
|
||||
func (d *MeetingChatDAO) Create(ctx context.Context, chat *model.MeetingChat) error {
|
||||
funcName := "dao.meeting_chat_dao.Create"
|
||||
err := d.db.WithContext(ctx).Create(chat).Error
|
||||
if err != nil {
|
||||
logs.Error(ctx, funcName, "写入会议聊天消息失败",
|
||||
zap.Int64("room_id", chat.RoomID),
|
||||
zap.Int64("user_id", chat.UserID),
|
||||
zap.Error(err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ListByRoom 按房间正序返回聊天历史(供会议室打开时加载)
|
||||
// afterID 为游标(id > afterID 的记录),0 表示从头开始
|
||||
// limit 默认 50,上限 200
|
||||
func (d *MeetingChatDAO) ListByRoom(ctx context.Context, roomID int64, afterID int64, limit int) ([]model.MeetingChat, error) {
|
||||
funcName := "dao.meeting_chat_dao.ListByRoom"
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
|
||||
q := d.db.WithContext(ctx).Where("room_id = ?", roomID)
|
||||
if afterID > 0 {
|
||||
q = q.Where("id > ?", afterID)
|
||||
}
|
||||
|
||||
var list []model.MeetingChat
|
||||
err := q.Order("id ASC").Limit(limit).Find(&list).Error
|
||||
if err != nil {
|
||||
logs.Error(ctx, funcName, "查询会议聊天失败",
|
||||
zap.Int64("room_id", roomID), zap.Error(err))
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
|
||||
// DeleteByRoomIDs 按房间 ID 批量删除聊天消息(会议结束 24 小时后清理)
|
||||
// 外层已有 meeting_rooms ON DELETE CASCADE,此方法用于主动定时清理(不删除 room 本身)
|
||||
func (d *MeetingChatDAO) DeleteByRoomIDs(ctx context.Context, roomIDs []int64) (int64, error) {
|
||||
funcName := "dao.meeting_chat_dao.DeleteByRoomIDs"
|
||||
if len(roomIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
res := d.db.WithContext(ctx).
|
||||
Where("room_id IN ?", roomIDs).
|
||||
Delete(&model.MeetingChat{})
|
||||
if res.Error != nil {
|
||||
logs.Error(ctx, funcName, "批量清理聊天消息失败",
|
||||
zap.Int("room_count", len(roomIDs)), zap.Error(res.Error))
|
||||
}
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
// CountByRoom 统计房间内聊天条数(供管理端观测)
|
||||
func (d *MeetingChatDAO) CountByRoom(ctx context.Context, roomID int64) (int64, error) {
|
||||
var count int64
|
||||
err := d.db.WithContext(ctx).
|
||||
Model(&model.MeetingChat{}).
|
||||
Where("room_id = ?", roomID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
255
backend/go-service/app/meeting/dao/meeting_participant_dao.go
Normal file
255
backend/go-service/app/meeting/dao/meeting_participant_dao.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/echochat/backend/app/constants"
|
||||
"github.com/echochat/backend/app/meeting/model"
|
||||
"github.com/echochat/backend/pkg/logs"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MeetingParticipantDAO 参与者数据访问对象
|
||||
type MeetingParticipantDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewMeetingParticipantDAO 创建实例
|
||||
func NewMeetingParticipantDAO(db *gorm.DB) *MeetingParticipantDAO {
|
||||
return &MeetingParticipantDAO{db: db}
|
||||
}
|
||||
|
||||
// JoinRoom 记录用户加入会议
|
||||
// 语义:
|
||||
// - 若 (room_id, user_id) 不存在 → 创建新行,role 取参数值,joined_at = NOW()
|
||||
// - 若 (room_id, user_id) 已存在且 left_at != NULL(历史离会)→ 重置 left_at/left_reason/duration,
|
||||
// joined_at 刷新为当前时间(视为全新一次参会),role 保持原值(角色变更走 UpdateRole)
|
||||
// - 若 (room_id, user_id) 已存在且 left_at = NULL(仍在会议中)→ 返回 ErrAlreadyInMeeting
|
||||
//
|
||||
// 返回最新持久化的参与者记录
|
||||
var (
|
||||
// ErrAlreadyInMeeting 用户已在会议中(未 left_at)
|
||||
ErrAlreadyInMeeting = errors.New("user already in meeting")
|
||||
)
|
||||
|
||||
// JoinRoom 加入会议(幂等 + 重入复用记录)
|
||||
func (d *MeetingParticipantDAO) JoinRoom(ctx context.Context, roomID, userID int64, role int) (*model.MeetingParticipant, error) {
|
||||
funcName := "dao.meeting_participant_dao.JoinRoom"
|
||||
logs.Info(ctx, funcName, "加入会议",
|
||||
zap.Int64("room_id", roomID), zap.Int64("user_id", userID), zap.Int("role", role))
|
||||
|
||||
var result model.MeetingParticipant
|
||||
err := d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var existing model.MeetingParticipant
|
||||
findErr := tx.Where("room_id = ? AND user_id = ?", roomID, userID).First(&existing).Error
|
||||
|
||||
if errors.Is(findErr, gorm.ErrRecordNotFound) {
|
||||
now := time.Now()
|
||||
result = model.MeetingParticipant{
|
||||
RoomID: roomID,
|
||||
UserID: userID,
|
||||
Role: role,
|
||||
JoinedAt: now,
|
||||
Duration: 0,
|
||||
}
|
||||
return tx.Create(&result).Error
|
||||
}
|
||||
if findErr != nil {
|
||||
return findErr
|
||||
}
|
||||
|
||||
if existing.LeftAt == nil {
|
||||
return ErrAlreadyInMeeting
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"joined_at": now,
|
||||
"left_at": nil,
|
||||
"left_reason": nil,
|
||||
"duration": 0,
|
||||
}
|
||||
if err := tx.Model(&model.MeetingParticipant{}).
|
||||
Where("id = ?", existing.ID).
|
||||
Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result = existing
|
||||
result.JoinedAt = now
|
||||
result.LeftAt = nil
|
||||
result.LeftReason = nil
|
||||
result.Duration = 0
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil && !errors.Is(err, ErrAlreadyInMeeting) {
|
||||
logs.Error(ctx, funcName, "加入会议失败",
|
||||
zap.Int64("room_id", roomID), zap.Int64("user_id", userID), zap.Error(err))
|
||||
}
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// LeaveRoom 记录用户离会
|
||||
// 计算 duration = NOW() - joined_at(秒),写入 left_at / left_reason
|
||||
// 已离会(left_at != NULL)的记录幂等忽略,返回受影响行数 0
|
||||
func (d *MeetingParticipantDAO) LeaveRoom(ctx context.Context, roomID, userID int64, reason string) (int64, error) {
|
||||
funcName := "dao.meeting_participant_dao.LeaveRoom"
|
||||
now := time.Now()
|
||||
|
||||
res := d.db.WithContext(ctx).
|
||||
Model(&model.MeetingParticipant{}).
|
||||
Where("room_id = ? AND user_id = ? AND left_at IS NULL", roomID, userID).
|
||||
Updates(map[string]interface{}{
|
||||
"left_at": now,
|
||||
"left_reason": reason,
|
||||
// duration 使用 SQL 表达式保证数据库时间一致
|
||||
"duration": gorm.Expr("EXTRACT(EPOCH FROM (? - joined_at))::INT", now),
|
||||
})
|
||||
if res.Error != nil {
|
||||
logs.Error(ctx, funcName, "离开会议失败",
|
||||
zap.Int64("room_id", roomID), zap.Int64("user_id", userID), zap.Error(res.Error))
|
||||
}
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
// LeaveAllActive 批量将会议内所有仍活跃的参与者置为离会(用于会议结束时兜底)
|
||||
// reason 建议传 host_end / empty_ttl 等房间级原因
|
||||
func (d *MeetingParticipantDAO) LeaveAllActive(ctx context.Context, roomID int64, reason string) (int64, error) {
|
||||
funcName := "dao.meeting_participant_dao.LeaveAllActive"
|
||||
now := time.Now()
|
||||
|
||||
res := d.db.WithContext(ctx).
|
||||
Model(&model.MeetingParticipant{}).
|
||||
Where("room_id = ? AND left_at IS NULL", roomID).
|
||||
Updates(map[string]interface{}{
|
||||
"left_at": now,
|
||||
"left_reason": reason,
|
||||
"duration": gorm.Expr("EXTRACT(EPOCH FROM (? - joined_at))::INT", now),
|
||||
})
|
||||
if res.Error != nil {
|
||||
logs.Error(ctx, funcName, "批量离会失败",
|
||||
zap.Int64("room_id", roomID), zap.Error(res.Error))
|
||||
}
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
// GetByRoomAndUser 查询单个参与者记录
|
||||
func (d *MeetingParticipantDAO) GetByRoomAndUser(ctx context.Context, roomID, userID int64) (*model.MeetingParticipant, error) {
|
||||
var p model.MeetingParticipant
|
||||
err := d.db.WithContext(ctx).
|
||||
Where("room_id = ? AND user_id = ?", roomID, userID).
|
||||
First(&p).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// ListActiveByRoom 返回房间内所有仍未离会的参与者(left_at IS NULL)
|
||||
// 按 joined_at ASC 排序,供"最早加入者接任主持人"使用
|
||||
func (d *MeetingParticipantDAO) ListActiveByRoom(ctx context.Context, roomID int64) ([]model.MeetingParticipant, error) {
|
||||
funcName := "dao.meeting_participant_dao.ListActiveByRoom"
|
||||
var list []model.MeetingParticipant
|
||||
err := d.db.WithContext(ctx).
|
||||
Where("room_id = ? AND left_at IS NULL", roomID).
|
||||
Order("joined_at ASC").
|
||||
Find(&list).Error
|
||||
if err != nil {
|
||||
logs.Error(ctx, funcName, "查询在线参与者失败",
|
||||
zap.Int64("room_id", roomID), zap.Error(err))
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
|
||||
// CountActiveByRoom 房间内当前在线人数(不含已离会)
|
||||
// 用于加入会议时的容量校验
|
||||
func (d *MeetingParticipantDAO) CountActiveByRoom(ctx context.Context, roomID int64) (int64, error) {
|
||||
var count int64
|
||||
err := d.db.WithContext(ctx).
|
||||
Model(&model.MeetingParticipant{}).
|
||||
Where("room_id = ? AND left_at IS NULL", roomID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// ListByRoom 房间内所有历史参与者(含已离会),按 joined_at ASC 排序
|
||||
func (d *MeetingParticipantDAO) ListByRoom(ctx context.Context, roomID int64) ([]model.MeetingParticipant, error) {
|
||||
var list []model.MeetingParticipant
|
||||
err := d.db.WithContext(ctx).
|
||||
Where("room_id = ?", roomID).
|
||||
Order("joined_at ASC").
|
||||
Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
// FindActiveByUser 查询用户当前仍在进行中的会议(left_at IS NULL,且 room.status != ended)
|
||||
// 用于"用户不能同时在多个会议中"的业务校验
|
||||
// 返回 gorm.ErrRecordNotFound 表示当前空闲
|
||||
func (d *MeetingParticipantDAO) FindActiveByUser(ctx context.Context, userID int64) (*model.MeetingParticipant, error) {
|
||||
var p model.MeetingParticipant
|
||||
err := d.db.WithContext(ctx).
|
||||
Joins("JOIN meeting_rooms ON meeting_rooms.id = meeting_participants.room_id").
|
||||
Where("meeting_participants.user_id = ? AND meeting_participants.left_at IS NULL AND meeting_rooms.status != ?",
|
||||
userID, constants.MeetingStatusEnded).
|
||||
First(&p).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// TransferHost 事务内完成主持人转让
|
||||
// 1) 旧 host 的 role 置为 Participant(0)
|
||||
// 2) 新 host 的 role 置为 Host(1)
|
||||
// 调用方应在同一上游事务中同时调 MeetingRoomDAO.UpdateHost 修改 meeting_rooms.host_id
|
||||
// 本方法内部已自带事务,上游无需再包裹
|
||||
func (d *MeetingParticipantDAO) TransferHost(ctx context.Context, roomID, oldHostID, newHostID int64) error {
|
||||
funcName := "dao.meeting_participant_dao.TransferHost"
|
||||
logs.Info(ctx, funcName, "转让主持人",
|
||||
zap.Int64("room_id", roomID),
|
||||
zap.Int64("old_host_id", oldHostID),
|
||||
zap.Int64("new_host_id", newHostID))
|
||||
|
||||
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.MeetingParticipant{}).
|
||||
Where("room_id = ? AND user_id = ? AND role = ?",
|
||||
roomID, oldHostID, constants.MeetingRoleHost).
|
||||
Update("role", constants.MeetingRoleParticipant).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.MeetingParticipant{}).
|
||||
Where("room_id = ? AND user_id = ? AND left_at IS NULL",
|
||||
roomID, newHostID).
|
||||
Update("role", constants.MeetingRoleHost).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRole 更新指定参与者角色(非转让场景,例如直接指派联合主持人)
|
||||
func (d *MeetingParticipantDAO) UpdateRole(ctx context.Context, roomID, userID int64, role int) error {
|
||||
return d.db.WithContext(ctx).
|
||||
Model(&model.MeetingParticipant{}).
|
||||
Where("room_id = ? AND user_id = ?", roomID, userID).
|
||||
Update("role", role).Error
|
||||
}
|
||||
|
||||
// ListByUser 用户历史会议列表(分页,按加入时间倒序)
|
||||
func (d *MeetingParticipantDAO) ListByUser(ctx context.Context, userID int64, offset, limit int) ([]model.MeetingParticipant, int64, error) {
|
||||
q := d.db.WithContext(ctx).
|
||||
Model(&model.MeetingParticipant{}).
|
||||
Where("user_id = ?", userID)
|
||||
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var list []model.MeetingParticipant
|
||||
err := q.Order("joined_at DESC").Offset(offset).Limit(limit).Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
177
backend/go-service/app/meeting/dao/meeting_room_dao.go
Normal file
177
backend/go-service/app/meeting/dao/meeting_room_dao.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Package dao 提供 meeting 模块的数据库访问操作
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/echochat/backend/app/constants"
|
||||
"github.com/echochat/backend/app/meeting/model"
|
||||
"github.com/echochat/backend/pkg/logs"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MeetingRoomDAO 会议房间数据访问对象
|
||||
type MeetingRoomDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewMeetingRoomDAO 创建 MeetingRoomDAO 实例
|
||||
func NewMeetingRoomDAO(db *gorm.DB) *MeetingRoomDAO {
|
||||
return &MeetingRoomDAO{db: db}
|
||||
}
|
||||
|
||||
// Create 新建一个会议房间
|
||||
// 由调用方预先生成 RoomCode 并确认唯一,本方法不做冲突重试
|
||||
func (d *MeetingRoomDAO) Create(ctx context.Context, room *model.MeetingRoom) error {
|
||||
funcName := "dao.meeting_room_dao.Create"
|
||||
logs.Info(ctx, funcName, "创建会议房间",
|
||||
zap.String("room_code", room.RoomCode), zap.Int64("host_id", room.HostID))
|
||||
|
||||
err := d.db.WithContext(ctx).Create(room).Error
|
||||
if err != nil {
|
||||
logs.Error(ctx, funcName, "创建会议房间失败",
|
||||
zap.String("room_code", room.RoomCode), zap.Error(err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetByID 按主键查询
|
||||
func (d *MeetingRoomDAO) GetByID(ctx context.Context, id int64) (*model.MeetingRoom, error) {
|
||||
var room model.MeetingRoom
|
||||
err := d.db.WithContext(ctx).First(&room, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &room, nil
|
||||
}
|
||||
|
||||
// GetByCode 按会议号查询(入会流程的主要入口)
|
||||
// 返回 gorm.ErrRecordNotFound 时上层应转换为业务错误 ErrMeetingNotFound
|
||||
func (d *MeetingRoomDAO) GetByCode(ctx context.Context, code string) (*model.MeetingRoom, error) {
|
||||
funcName := "dao.meeting_room_dao.GetByCode"
|
||||
|
||||
var room model.MeetingRoom
|
||||
err := d.db.WithContext(ctx).Where("room_code = ?", code).First(&room).Error
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logs.Error(ctx, funcName, "按会议号查询房间失败",
|
||||
zap.String("room_code", code), zap.Error(err))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &room, nil
|
||||
}
|
||||
|
||||
// ExistsCode 会议号是否已被占用(用于创建会议时的 code 冲突重试)
|
||||
func (d *MeetingRoomDAO) ExistsCode(ctx context.Context, code string) (bool, error) {
|
||||
var count int64
|
||||
err := d.db.WithContext(ctx).
|
||||
Model(&model.MeetingRoom{}).
|
||||
Where("room_code = ?", code).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// MarkStarted 记录会议实际开始时间(host 首次加入时调用)
|
||||
// 仅当 status=pending 且 started_at IS NULL 时才写入,避免重复覆盖
|
||||
func (d *MeetingRoomDAO) MarkStarted(ctx context.Context, id int64, startedAt time.Time) (int64, error) {
|
||||
funcName := "dao.meeting_room_dao.MarkStarted"
|
||||
res := d.db.WithContext(ctx).
|
||||
Model(&model.MeetingRoom{}).
|
||||
Where("id = ? AND status = ? AND started_at IS NULL", id, constants.MeetingStatusPending).
|
||||
Updates(map[string]interface{}{
|
||||
"status": constants.MeetingStatusActive,
|
||||
"started_at": startedAt,
|
||||
})
|
||||
if res.Error != nil {
|
||||
logs.Error(ctx, funcName, "标记会议开始失败",
|
||||
zap.Int64("room_id", id), zap.Error(res.Error))
|
||||
}
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
// MarkEnded 记录会议结束(status=2 + ended_at + ended_reason)
|
||||
// 乐观锁:只对 status != ended 的行生效,避免重复结束覆盖原始 reason
|
||||
func (d *MeetingRoomDAO) MarkEnded(ctx context.Context, id int64, reason string, endedAt time.Time) (int64, error) {
|
||||
funcName := "dao.meeting_room_dao.MarkEnded"
|
||||
res := d.db.WithContext(ctx).
|
||||
Model(&model.MeetingRoom{}).
|
||||
Where("id = ? AND status != ?", id, constants.MeetingStatusEnded).
|
||||
Updates(map[string]interface{}{
|
||||
"status": constants.MeetingStatusEnded,
|
||||
"ended_at": endedAt,
|
||||
"ended_reason": reason,
|
||||
})
|
||||
if res.Error != nil {
|
||||
logs.Error(ctx, funcName, "标记会议结束失败",
|
||||
zap.Int64("room_id", id), zap.String("reason", reason), zap.Error(res.Error))
|
||||
}
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
// UpdateHost 更新主持人(仅修改 meeting_rooms.host_id 字段,meeting_participants.role 由 ParticipantDAO.TransferHost 在同一事务中处理)
|
||||
func (d *MeetingRoomDAO) UpdateHost(ctx context.Context, id, newHostID int64) error {
|
||||
return d.db.WithContext(ctx).
|
||||
Model(&model.MeetingRoom{}).
|
||||
Where("id = ?", id).
|
||||
Update("host_id", newHostID).Error
|
||||
}
|
||||
|
||||
// UpdateSettings 更新房间级配置(settings 字段整体替换)
|
||||
// 调用方需保证 settingsJSON 是合法 JSON 字符串
|
||||
func (d *MeetingRoomDAO) UpdateSettings(ctx context.Context, id int64, settingsJSON string) error {
|
||||
return d.db.WithContext(ctx).
|
||||
Model(&model.MeetingRoom{}).
|
||||
Where("id = ?", id).
|
||||
Update("settings", settingsJSON).Error
|
||||
}
|
||||
|
||||
// ListByHost 按主持人查询会议列表(支持 status 过滤 + 分页)
|
||||
// status 传 -1 表示不限
|
||||
// 返回按 created_at DESC 排序,走 idx_meeting_rooms_host_status 索引
|
||||
func (d *MeetingRoomDAO) ListByHost(ctx context.Context, hostID int64, status int, offset, limit int) ([]model.MeetingRoom, int64, error) {
|
||||
funcName := "dao.meeting_room_dao.ListByHost"
|
||||
q := d.db.WithContext(ctx).
|
||||
Model(&model.MeetingRoom{}).
|
||||
Where("host_id = ?", hostID)
|
||||
if status >= 0 {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
logs.Error(ctx, funcName, "计数失败", zap.Int64("host_id", hostID), zap.Error(err))
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var rooms []model.MeetingRoom
|
||||
err := q.Order("created_at DESC").Offset(offset).Limit(limit).Find(&rooms).Error
|
||||
if err != nil {
|
||||
logs.Error(ctx, funcName, "查询失败", zap.Int64("host_id", hostID), zap.Error(err))
|
||||
}
|
||||
return rooms, total, err
|
||||
}
|
||||
|
||||
// ListExpiredForCleanup 返回已结束且超过指定小时数的房间 ID 列表
|
||||
// 供定时任务清理 meeting_chats 使用;room 本身保留归档不删
|
||||
// 返回 limit 上限用于分批处理,避免一次捞太多
|
||||
func (d *MeetingRoomDAO) ListExpiredForCleanup(ctx context.Context, hoursAgo int, limit int) ([]int64, error) {
|
||||
funcName := "dao.meeting_room_dao.ListExpiredForCleanup"
|
||||
cutoff := time.Now().Add(-time.Duration(hoursAgo) * time.Hour)
|
||||
|
||||
var ids []int64
|
||||
err := d.db.WithContext(ctx).
|
||||
Model(&model.MeetingRoom{}).
|
||||
Where("status = ? AND ended_at IS NOT NULL AND ended_at < ?",
|
||||
constants.MeetingStatusEnded, cutoff).
|
||||
Order("ended_at ASC").
|
||||
Limit(limit).
|
||||
Pluck("id", &ids).Error
|
||||
if err != nil {
|
||||
logs.Error(ctx, funcName, "查询过期房间失败", zap.Error(err))
|
||||
}
|
||||
return ids, err
|
||||
}
|
||||
19
backend/go-service/app/meeting/model/meeting_chat.go
Normal file
19
backend/go-service/app/meeting/model/meeting_chat.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// MeetingChat 会议内文字聊天消息模型,对应 meeting_chats 表
|
||||
// 独立于 im_messages,不进入常规 IM 消息流
|
||||
// 清理策略:会议结束(status=2)后 24 小时由定时任务批量 DELETE
|
||||
type MeetingChat struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` // 消息唯一标识
|
||||
RoomID int64 `json:"room_id" gorm:"not null;index:idx_meeting_chats_room_created,priority:1"` // 所属房间 ID
|
||||
UserID int64 `json:"user_id" gorm:"not null"` // 发送者用户 ID
|
||||
Content string `json:"content" gorm:"type:text;not null"` // 消息内容,纯文本
|
||||
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0);index:idx_meeting_chats_room_created,priority:2,sort:asc"` // 发送时间
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (MeetingChat) TableName() string {
|
||||
return "meeting_chats"
|
||||
}
|
||||
27
backend/go-service/app/meeting/model/meeting_participant.go
Normal file
27
backend/go-service/app/meeting/model/meeting_participant.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// MeetingParticipant 会议参与者模型,对应 meeting_participants 表
|
||||
// 一次入会对应一条记录,重入(left_at 已填的情况下再次加入)通过 UPDATE 复用
|
||||
// 主持人转让通过事务:旧 host.role=0 + 新 host.role=1
|
||||
type MeetingParticipant struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` // 参与者记录唯一标识
|
||||
RoomID int64 `json:"room_id" gorm:"not null;uniqueIndex:uk_meeting_participants_room_user,priority:1;index:idx_meeting_participants_room,priority:1"` // 所属房间 ID,与 user_id 组合唯一
|
||||
UserID int64 `json:"user_id" gorm:"not null;uniqueIndex:uk_meeting_participants_room_user,priority:2;index:idx_meeting_participants_user,priority:1"` // 用户 ID
|
||||
Role int `json:"role" gorm:"not null;default:0"` // 0=普通,1=主持人(MVP 仅此两档),2=联合主持人
|
||||
JoinedAt time.Time `json:"joined_at" gorm:"not null;autoCreateTime;type:timestamp(0);index:idx_meeting_participants_room,priority:2,sort:asc;index:idx_meeting_participants_user,priority:2,sort:desc"` // 加入时间
|
||||
LeftAt *time.Time `json:"left_at" gorm:"type:timestamp(0)"` // 离开时间,NULL 表示仍在会议中
|
||||
LeftReason *string `json:"left_reason" gorm:"size:20"` // 离会原因:self / kicked / host_end / empty_ttl / disconnect
|
||||
Duration int `json:"duration" gorm:"not null;default:0"` // 本次参会时长(秒),left_at 写入时同步计算
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (MeetingParticipant) TableName() string {
|
||||
return "meeting_participants"
|
||||
}
|
||||
|
||||
// IsActive 参与者当前是否仍在会议中(left_at 为 NULL)
|
||||
func (p *MeetingParticipant) IsActive() bool {
|
||||
return p.LeftAt == nil
|
||||
}
|
||||
30
backend/go-service/app/meeting/model/meeting_room.go
Normal file
30
backend/go-service/app/meeting/model/meeting_room.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Package model 提供 meeting 模块的数据库模型
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// MeetingRoom 会议房间模型,对应 meeting_rooms 表
|
||||
// 记录会议的创建信息、主持人、容量、状态与生命周期时间点
|
||||
// 对外使用 RoomCode(XXX-XXX-XXX)寻址,内部使用 ID(主键)寻址
|
||||
type MeetingRoom struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` // 会议房间唯一标识
|
||||
RoomCode string `json:"room_code" gorm:"size:20;not null;uniqueIndex"` // 用户可见的会议号 XXX-XXX-XXX,全局唯一
|
||||
Title string `json:"title" gorm:"size:200;not null"` // 会议标题
|
||||
HostID int64 `json:"host_id" gorm:"not null;index:idx_meeting_rooms_host_status,priority:1"` // 主持人用户 ID
|
||||
Type int `json:"type" gorm:"not null;default:1"` // 会议类型:1=即时,2=预约
|
||||
PasswordHash *string `json:"-" gorm:"size:255"` // 入会密码 bcrypt 哈希,NULL 表示无密码;JSON 始终剥离
|
||||
MaxMembers int `json:"max_members" gorm:"not null;default:50"` // 房间最大成员数,MVP 应用层强制 8
|
||||
Status int `json:"status" gorm:"not null;default:0;index:idx_meeting_rooms_host_status,priority:2"` // 状态:0=未开始,1=进行中,2=已结束
|
||||
ScheduledAt *time.Time `json:"scheduled_at" gorm:"type:timestamp(0)"` // 预约开始时间,即时会议为 NULL
|
||||
StartedAt *time.Time `json:"started_at" gorm:"type:timestamp(0)"` // 实际开始时间
|
||||
EndedAt *time.Time `json:"ended_at" gorm:"type:timestamp(0)"` // 实际结束时间
|
||||
EndedReason *string `json:"ended_reason" gorm:"size:20"` // 结束原因:host_ended / empty_ttl / admin_force / system_error
|
||||
Settings string `json:"settings" gorm:"type:jsonb;not null;default:'{}'"` // JSON 字符串,保存房间级开关
|
||||
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0);index:idx_meeting_rooms_host_status,priority:3,sort:desc"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"not null;autoUpdateTime;type:timestamp(0)"` // 更新时间
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (MeetingRoom) TableName() string {
|
||||
return "meeting_rooms"
|
||||
}
|
||||
21
backend/go-service/app/meeting/provider.go
Normal file
21
backend/go-service/app/meeting/provider.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package meeting
|
||||
|
||||
import (
|
||||
"github.com/echochat/backend/app/meeting/controller"
|
||||
"github.com/echochat/backend/app/meeting/dao"
|
||||
"github.com/echochat/backend/app/meeting/service"
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
// MeetingSet 会议模块 Wire Provider Set
|
||||
// 对外暴露:
|
||||
// - *service.MeetingService —— 业务服务,供未来 ws handler / media-server 回调使用
|
||||
// - *controller.MeetingController —— REST API 控制器
|
||||
// 依赖的接口 NotifyPusher / UserInfoResolver / OnlineChecker 由上游 wire.Bind 绑定具体实现
|
||||
var MeetingSet = wire.NewSet(
|
||||
dao.NewMeetingRoomDAO,
|
||||
dao.NewMeetingParticipantDAO,
|
||||
dao.NewMeetingChatDAO,
|
||||
service.NewMeetingService,
|
||||
controller.NewMeetingController,
|
||||
)
|
||||
36
backend/go-service/app/meeting/router.go
Normal file
36
backend/go-service/app/meeting/router.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Package meeting 会议模块(Phase 2e-2)
|
||||
package meeting
|
||||
|
||||
import (
|
||||
"github.com/echochat/backend/app/meeting/controller"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterRoutes 注册 meeting 模块的全部前台路由
|
||||
// 所有接口统一挂载在 /api/v1/meeting/* 前缀下,均需 JWT 认证
|
||||
// 设计文档 §6.2 的 12 个接口 Task 4 骨架阶段仅搭路由层,业务实现留待 Task 5/6/7
|
||||
func RegisterRoutes(r *gin.Engine, ctrl *controller.MeetingController, jwtAuth gin.HandlerFunc) {
|
||||
authed := r.Group("/api/v1/meeting")
|
||||
authed.Use(jwtAuth)
|
||||
{
|
||||
// 会议房间生命周期
|
||||
authed.POST("/rooms", ctrl.CreateRoom)
|
||||
authed.GET("/rooms", ctrl.ListMyMeetings)
|
||||
authed.GET("/rooms/:code", ctrl.GetRoom)
|
||||
authed.POST("/rooms/:code/join", ctrl.JoinRoom)
|
||||
authed.POST("/rooms/:code/leave", ctrl.LeaveRoom)
|
||||
authed.POST("/rooms/:code/end", ctrl.EndRoom)
|
||||
|
||||
// 主持人四件套(Task 7)
|
||||
authed.POST("/rooms/:code/transfer-host", ctrl.TransferHost)
|
||||
authed.POST("/rooms/:code/kick", ctrl.KickMember)
|
||||
|
||||
// 邀请(Task 5)
|
||||
authed.POST("/rooms/:code/invite", ctrl.InviteUsers)
|
||||
authed.POST("/invites/:token/redeem", ctrl.RedeemInvite)
|
||||
|
||||
// 会议内聊天(Task 6)
|
||||
authed.POST("/rooms/:code/chats", ctrl.SendChat)
|
||||
authed.GET("/rooms/:code/chats", ctrl.ListChats)
|
||||
}
|
||||
}
|
||||
31
backend/go-service/app/meeting/service/interfaces.go
Normal file
31
backend/go-service/app/meeting/service/interfaces.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Package service 提供 meeting 模块的业务逻辑
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
authModel "github.com/echochat/backend/app/auth/model"
|
||||
notifyService "github.com/echochat/backend/app/notify/service"
|
||||
)
|
||||
|
||||
// NotifyPusher 通知推送接口
|
||||
// 由 notify.service.NotifyService 隐式实现,供 meeting 模块注入使用
|
||||
// 会议邀请、主持人变更、踢出会议等事件通过此接口落地通知中心
|
||||
type NotifyPusher interface {
|
||||
Push(ctx context.Context, payload *notifyService.PushPayload)
|
||||
PushBatch(ctx context.Context, payloads []*notifyService.PushPayload)
|
||||
}
|
||||
|
||||
// UserInfoResolver 查询用户昵称 / 头像的接口
|
||||
// 由 contact.FriendshipDAO 隐式实现(已有 GetUsersByIDs 方法)
|
||||
// 用于拉取会议邀请中主持人信息、成员列表头像等
|
||||
type UserInfoResolver interface {
|
||||
GetUsersByIDs(ctx context.Context, userIDs []int64) ([]authModel.User, error)
|
||||
}
|
||||
|
||||
// OnlineChecker 查询用户在线状态的接口
|
||||
// 由 ws.OnlineService 隐式实现(签名对齐现有实现:不返回 error,查询失败按离线处理)
|
||||
// 用于会议邀请时判断收件人是否在线(离线则降级为仅通知入库,上线时通过未读补偿推送)
|
||||
type OnlineChecker interface {
|
||||
IsOnline(ctx context.Context, userID int64) bool
|
||||
}
|
||||
144
backend/go-service/app/meeting/service/meeting_service.go
Normal file
144
backend/go-service/app/meeting/service/meeting_service.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/echochat/backend/app/meeting/dao"
|
||||
"github.com/echochat/backend/app/meeting/model"
|
||||
"github.com/echochat/backend/pkg/ws"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Task 5 会继续填充具体业务逻辑时使用的哨兵错误,此处先集中定义占位
|
||||
// 命名与 group/notify 模块的 Err* 风格一致,控制器可直接 errors.Is 识别
|
||||
var (
|
||||
ErrMeetingNotFound = errors.New("会议不存在")
|
||||
ErrMeetingEnded = errors.New("会议已结束")
|
||||
ErrMeetingFull = errors.New("会议已满员")
|
||||
ErrMeetingPasswordWrong = errors.New("会议密码错误")
|
||||
ErrNotInMeeting = errors.New("你不在此会议中")
|
||||
ErrAlreadyInOtherMeeting = errors.New("你当前已在其他会议中")
|
||||
ErrNotMeetingHost = errors.New("仅主持人可执行此操作")
|
||||
ErrInviteTokenInvalid = errors.New("邀请链接已失效")
|
||||
ErrRoomCodeConflict = errors.New("会议号生成冲突,请重试")
|
||||
ErrNotImplemented = errors.New("功能尚未实现")
|
||||
)
|
||||
|
||||
// MeetingService 会议业务服务
|
||||
// Task 4 骨架阶段:仅完成依赖组装和方法签名占位,具体业务逻辑留待 Task 5/6/7 填充
|
||||
// 方法返回 ErrNotImplemented,Controller 将其映射为 501 响应,便于 Postman 与前端联调前观察路由完整性
|
||||
type MeetingService struct {
|
||||
roomDAO *dao.MeetingRoomDAO
|
||||
participantDAO *dao.MeetingParticipantDAO
|
||||
chatDAO *dao.MeetingChatDAO
|
||||
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
pubsub *ws.PubSub
|
||||
|
||||
notifyPusher NotifyPusher
|
||||
userResolver UserInfoResolver
|
||||
onlineChecker OnlineChecker
|
||||
}
|
||||
|
||||
// NewMeetingService 创建 MeetingService 实例
|
||||
// 依赖通过构造函数注入,接口依赖由上游 Wire 绑定到具体实现
|
||||
func NewMeetingService(
|
||||
roomDAO *dao.MeetingRoomDAO,
|
||||
participantDAO *dao.MeetingParticipantDAO,
|
||||
chatDAO *dao.MeetingChatDAO,
|
||||
db *gorm.DB,
|
||||
redis *redis.Client,
|
||||
pubsub *ws.PubSub,
|
||||
notifyPusher NotifyPusher,
|
||||
userResolver UserInfoResolver,
|
||||
onlineChecker OnlineChecker,
|
||||
) *MeetingService {
|
||||
return &MeetingService{
|
||||
roomDAO: roomDAO,
|
||||
participantDAO: participantDAO,
|
||||
chatDAO: chatDAO,
|
||||
db: db,
|
||||
redis: redis,
|
||||
pubsub: pubsub,
|
||||
notifyPusher: notifyPusher,
|
||||
userResolver: userResolver,
|
||||
onlineChecker: onlineChecker,
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 会议生命周期(Task 5 实现) ======
|
||||
|
||||
// CreateRoom 创建会议房间
|
||||
// Task 5:生成 room_code、bcrypt 密码、持久化 + 写 Redis room 快照
|
||||
func (s *MeetingService) CreateRoom(ctx context.Context, hostID int64, title, password string, maxMembers int, settings map[string]interface{}) (*model.MeetingRoom, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetRoomByCode 通过会议号查询房间详情(含当前成员列表)
|
||||
func (s *MeetingService) GetRoomByCode(ctx context.Context, code string) (*model.MeetingRoom, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
// JoinRoom 用户加入会议(校验密码 / 容量 / 单点参会)
|
||||
func (s *MeetingService) JoinRoom(ctx context.Context, userID int64, code, password string) (*model.MeetingParticipant, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
// LeaveRoom 用户主动离会
|
||||
// 若离开者是 host 且房间内还有其他成员,触发主持人转让(最早加入者接任)
|
||||
// 若离开后房间空,设置 Redis room TTL=300s 等待重入复用
|
||||
func (s *MeetingService) LeaveRoom(ctx context.Context, userID int64, code string) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
// EndRoom 主持人主动结束会议(status=ended + 全员 left_at + 触发 mediasoup 清理)
|
||||
func (s *MeetingService) EndRoom(ctx context.Context, userID int64, code string) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
// TransferHost 主持人转让(仅当前 host 可调用)
|
||||
func (s *MeetingService) TransferHost(ctx context.Context, operatorID int64, code string, newHostID int64) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
// KickMember 主持人踢出成员
|
||||
func (s *MeetingService) KickMember(ctx context.Context, operatorID int64, code string, targetUserID int64) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
// ListMyMeetings 我主持的 / 我参与的会议列表(含分页)
|
||||
func (s *MeetingService) ListMyMeetings(ctx context.Context, userID int64, role string, offset, limit int) ([]model.MeetingRoom, int64, error) {
|
||||
return nil, 0, ErrNotImplemented
|
||||
}
|
||||
|
||||
// ====== 邀请链接(Task 5) ======
|
||||
|
||||
// CreateInviteToken 生成邀请链接 Token 并写 Redis(TTL 600s)
|
||||
func (s *MeetingService) CreateInviteToken(ctx context.Context, inviterID int64, code string, inviteeID int64) (string, error) {
|
||||
return "", ErrNotImplemented
|
||||
}
|
||||
|
||||
// RedeemInviteToken 点击邀请链接时兑换 Token(校验 + 删除)
|
||||
func (s *MeetingService) RedeemInviteToken(ctx context.Context, userID int64, token string) (string, error) {
|
||||
return "", ErrNotImplemented
|
||||
}
|
||||
|
||||
// InviteUsers 主持人邀请用户(批量,走 notify.Push 发送 meeting_invite 通知)
|
||||
func (s *MeetingService) InviteUsers(ctx context.Context, inviterID int64, code string, inviteeIDs []int64, groupIDs []int64) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
// ====== 会议内聊天(Task 6) ======
|
||||
|
||||
// SendChatMessage 写入会议聊天 + 向房间内所有成员广播 WS meeting.chat.message
|
||||
func (s *MeetingService) SendChatMessage(ctx context.Context, userID int64, code, content string) (*model.MeetingChat, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
// ListChatMessages 加载会议聊天历史(游标分页)
|
||||
func (s *MeetingService) ListChatMessages(ctx context.Context, userID int64, code string, afterID int64, limit int) ([]model.MeetingChat, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
groupController "github.com/echochat/backend/app/group/controller"
|
||||
imController "github.com/echochat/backend/app/im/controller"
|
||||
imHandler "github.com/echochat/backend/app/im/handler"
|
||||
meetingController "github.com/echochat/backend/app/meeting/controller"
|
||||
meetingService "github.com/echochat/backend/app/meeting/service"
|
||||
notifyController "github.com/echochat/backend/app/notify/controller"
|
||||
notifyService "github.com/echochat/backend/app/notify/service"
|
||||
notifyTask "github.com/echochat/backend/app/notify/task"
|
||||
@@ -52,6 +54,8 @@ type App struct {
|
||||
NotifyService *notifyService.NotifyService // 通知业务服务(兼 Pusher、ConnectHook)
|
||||
NotifyController *notifyController.NotificationController // 通知控制器
|
||||
NotifyCleanupTask *notifyTask.CleanupTask // 通知清理定时任务
|
||||
MeetingService *meetingService.MeetingService // 会议业务服务(Task 4 骨架)
|
||||
MeetingController *meetingController.MeetingController // 会议控制器(Task 4 骨架,handler 返回 501)
|
||||
}
|
||||
|
||||
// NewApp 创建应用实例
|
||||
@@ -81,6 +85,8 @@ func NewApp(
|
||||
notifySvc *notifyService.NotifyService,
|
||||
notifyCtrl *notifyController.NotificationController,
|
||||
notifyCleanup *notifyTask.CleanupTask,
|
||||
meetingSvc *meetingService.MeetingService,
|
||||
meetingCtrl *meetingController.MeetingController,
|
||||
) *App {
|
||||
wsHandler.SetOfflinePusher(offlinePusher)
|
||||
wsHandler.SetNotifyConnectHook(notifySvc)
|
||||
@@ -111,6 +117,8 @@ func NewApp(
|
||||
NotifyService: notifySvc,
|
||||
NotifyController: notifyCtrl,
|
||||
NotifyCleanupTask: notifyCleanup,
|
||||
MeetingService: meetingSvc,
|
||||
MeetingController: meetingCtrl,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
imApp "github.com/echochat/backend/app/im"
|
||||
imDAO "github.com/echochat/backend/app/im/dao"
|
||||
imService "github.com/echochat/backend/app/im/service"
|
||||
meetingApp "github.com/echochat/backend/app/meeting"
|
||||
meetingService "github.com/echochat/backend/app/meeting/service"
|
||||
notifyApp "github.com/echochat/backend/app/notify"
|
||||
notifyService "github.com/echochat/backend/app/notify/service"
|
||||
wsApp "github.com/echochat/backend/app/ws"
|
||||
@@ -36,6 +38,7 @@ func InitializeApp(cfg *config.Config) (*App, error) {
|
||||
fileApp.FileSet,
|
||||
groupApp.GroupSet,
|
||||
notifyApp.NotifySet,
|
||||
meetingApp.MeetingSet,
|
||||
wire.Bind(new(wsApp.FriendIDsGetter), new(*contactDAO.FriendshipDAO)),
|
||||
wire.Bind(new(groupService.UserInfoProvider), new(*contactDAO.FriendshipDAO)),
|
||||
wire.Bind(new(imService.GroupInfoGetter), new(*groupDAO.GroupDAO)),
|
||||
@@ -48,6 +51,9 @@ func InitializeApp(cfg *config.Config) (*App, error) {
|
||||
wire.Bind(new(notifyService.UserInfoResolver), new(*contactDAO.FriendshipDAO)),
|
||||
wire.Bind(new(contactService.NotifyPusher), new(*notifyService.NotifyService)),
|
||||
wire.Bind(new(groupService.NotifyPusher), new(*notifyService.NotifyService)),
|
||||
wire.Bind(new(meetingService.NotifyPusher), new(*notifyService.NotifyService)),
|
||||
wire.Bind(new(meetingService.UserInfoResolver), new(*contactDAO.FriendshipDAO)),
|
||||
wire.Bind(new(meetingService.OnlineChecker), new(*wsApp.OnlineService)),
|
||||
)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -15,17 +15,20 @@ import (
|
||||
"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"
|
||||
service4 "github.com/echochat/backend/app/contact/service"
|
||||
controller4 "github.com/echochat/backend/app/file/controller"
|
||||
service4 "github.com/echochat/backend/app/file/service"
|
||||
service5 "github.com/echochat/backend/app/file/service"
|
||||
controller5 "github.com/echochat/backend/app/group/controller"
|
||||
dao4 "github.com/echochat/backend/app/group/dao"
|
||||
service5 "github.com/echochat/backend/app/group/service"
|
||||
imApp "github.com/echochat/backend/app/im"
|
||||
service6 "github.com/echochat/backend/app/group/service"
|
||||
"github.com/echochat/backend/app/im"
|
||||
controller7 "github.com/echochat/backend/app/meeting/controller"
|
||||
dao6 "github.com/echochat/backend/app/meeting/dao"
|
||||
service7 "github.com/echochat/backend/app/meeting/service"
|
||||
controller6 "github.com/echochat/backend/app/notify/controller"
|
||||
dao5 "github.com/echochat/backend/app/notify/dao"
|
||||
service6 "github.com/echochat/backend/app/notify/service"
|
||||
task2 "github.com/echochat/backend/app/notify/task"
|
||||
service3 "github.com/echochat/backend/app/notify/service"
|
||||
"github.com/echochat/backend/app/notify/task"
|
||||
"github.com/echochat/backend/app/ws"
|
||||
"github.com/echochat/backend/config"
|
||||
"github.com/echochat/backend/pkg/db"
|
||||
@@ -69,46 +72,37 @@ func InitializeApp(cfg *config.Config) (*App, error) {
|
||||
onlineController := controller2.NewOnlineController(onlineManageService)
|
||||
contactManageService := service2.NewContactManageService(gormDB)
|
||||
contactManageController := controller2.NewContactManageController(contactManageService)
|
||||
handler := ws.ProvideWSHandler(hub, pubSub, jwtConfig, onlineService, authService)
|
||||
friendGroupDAO := dao3.NewFriendGroupDAO(gormDB)
|
||||
|
||||
// Notify 模块初始化(contact/group 依赖 NotifyPusher)
|
||||
notificationDAO := dao5.NewNotificationDAO(gormDB)
|
||||
notifyService := service6.NewNotifyService(notificationDAO, pubSub, friendshipDAO)
|
||||
notificationController := controller6.NewNotificationController(notifyService)
|
||||
notifyCleanupTask := task2.NewCleanupTask(notificationDAO)
|
||||
|
||||
contactService := service3.NewContactService(friendshipDAO, friendGroupDAO, onlineService, notifyService)
|
||||
contactController := controller3.NewContactController(contactService)
|
||||
|
||||
// IM 模块初始化
|
||||
conversationDAO := imApp.ProvideConversationDAO(gormDB)
|
||||
messageDAO := imApp.ProvideMessageDAO(gormDB)
|
||||
groupDAO := dao4.NewGroupDAO(gormDB)
|
||||
messageReadDAO := dao4.NewMessageReadDAO(gormDB)
|
||||
imService := imApp.ProvideIMService(conversationDAO, messageDAO, pubSub, client, friendshipDAO, friendshipDAO, groupDAO, messageReadDAO)
|
||||
imEventHandler := imApp.ProvideIMEventHandler(imService, hub)
|
||||
offlinePusher := imApp.ProvideOfflinePusher(imService, conversationDAO, pubSub)
|
||||
imController := imApp.ProvideIMController(imService)
|
||||
|
||||
// File 模块初始化
|
||||
fileService := service4.NewFileService(minioClient, minioConfig)
|
||||
fileController := controller4.NewFileController(fileService)
|
||||
|
||||
// Group 模块初始化
|
||||
joinRequestDAO := dao4.NewJoinRequestDAO(gormDB)
|
||||
groupService := service5.NewGroupService(groupDAO, joinRequestDAO, friendshipDAO, pubSub, messageDAO, notifyService)
|
||||
groupController := controller5.NewGroupController(groupService)
|
||||
|
||||
// Admin 群组管理初始化
|
||||
groupManageService := service2.NewGroupManageService(gormDB, groupDAO)
|
||||
groupManageController := controller2.NewGroupManageController(groupManageService)
|
||||
|
||||
// Admin 消息管理初始化
|
||||
messageManageDAO := dao2.NewMessageManageDAO(gormDB)
|
||||
conversationDAO := im.ProvideConversationDAO(gormDB)
|
||||
messageManageService := service2.NewMessageManageService(messageManageDAO, userDAO, conversationDAO, pubSub)
|
||||
messageManageController := controller2.NewMessageManageController(messageManageService)
|
||||
|
||||
app := NewApp(cfg, gormDB, client, minioClient, authService, authController, adminAuthController, userManageController, onlineController, contactManageController, groupManageController, messageManageController, handler, hub, pubSub, onlineService, contactController, imController, imEventHandler, offlinePusher, fileController, groupController, notifyService, notificationController, notifyCleanupTask)
|
||||
handler := ws.ProvideWSHandler(hub, pubSub, jwtConfig, onlineService, authService)
|
||||
friendGroupDAO := dao3.NewFriendGroupDAO(gormDB)
|
||||
notificationDAO := dao5.NewNotificationDAO(gormDB)
|
||||
notifyService := service3.NewNotifyService(notificationDAO, pubSub, friendshipDAO)
|
||||
contactService := service4.NewContactService(friendshipDAO, friendGroupDAO, onlineService, notifyService)
|
||||
contactController := controller3.NewContactController(contactService)
|
||||
messageDAO := im.ProvideMessageDAO(gormDB)
|
||||
messageReadDAO := dao4.NewMessageReadDAO(gormDB)
|
||||
imService := im.ProvideIMService(conversationDAO, messageDAO, pubSub, client, friendshipDAO, friendshipDAO, groupDAO, messageReadDAO)
|
||||
imController := im.ProvideIMController(imService)
|
||||
eventHandler := im.ProvideIMEventHandler(imService, hub)
|
||||
offlinePusher := im.ProvideOfflinePusher(imService, conversationDAO, pubSub)
|
||||
fileService := service5.NewFileService(minioClient, minioConfig)
|
||||
fileController := controller4.NewFileController(fileService)
|
||||
joinRequestDAO := dao4.NewJoinRequestDAO(gormDB)
|
||||
groupService := service6.NewGroupService(groupDAO, joinRequestDAO, friendshipDAO, pubSub, messageDAO, notifyService)
|
||||
groupController := controller5.NewGroupController(groupService)
|
||||
notificationController := controller6.NewNotificationController(notifyService)
|
||||
cleanupTask := task.NewCleanupTask(notificationDAO)
|
||||
meetingRoomDAO := dao6.NewMeetingRoomDAO(gormDB)
|
||||
meetingParticipantDAO := dao6.NewMeetingParticipantDAO(gormDB)
|
||||
meetingChatDAO := dao6.NewMeetingChatDAO(gormDB)
|
||||
meetingService := service7.NewMeetingService(meetingRoomDAO, meetingParticipantDAO, meetingChatDAO, gormDB, client, pubSub, notifyService, friendshipDAO, onlineService)
|
||||
meetingController := controller7.NewMeetingController(meetingService)
|
||||
app := NewApp(cfg, gormDB, client, minioClient, authService, authController, adminAuthController, userManageController, onlineController, contactManageController, groupManageController, messageManageController, handler, hub, pubSub, onlineService, contactController, imController, eventHandler, offlinePusher, fileController, groupController, notifyService, notificationController, cleanupTask, meetingService, meetingController)
|
||||
return app, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user