Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# GoChat Docker Compose Port Configuration
# Copy this file to .env and customize ports to avoid conflicts

# Infrastructure Services
POSTGRES_PORT=5432
RABBITMQ_PORT=5672
RABBITMQ_MGMT_PORT=15672
MINIO_API_PORT=9000
MINIO_CONSOLE_PORT=9001

# Application Services
API_PORT=7070
WS_PORT=7000
TCP_PORT=7001
TCP_PORT2=7002
SITE_PORT=8080

# Monitoring Services
PROMETHEUS_PORT=19090
GRAFANA_PORT=3000
JAEGER_UI_PORT=16686
JAEGER_OTLP_GRPC=4317
JAEGER_OTLP_HTTP=4318

# Metrics Ports
LOGIC_METRICS_PORT=9091
CONNECT_WS_METRICS_PORT=9092
CONNECT_TCP_METRICS_PORT=9093
TASK_METRICS_PORT=9094
7 changes: 0 additions & 7 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,6 @@ jobs:
- name: Install dependencies
run: go mod download

- name: Check code formatting
run: |
if [ -n "$(go fmt ./...)" ]; then
echo "Code is not formatted. Run 'make fmt' to fix."
exit 1
fi

- name: Run go vet
run: go vet ./...

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.idea
vendor/
loadtest/reports/

# Environment configuration (each developer can customize ports)
.env
172 changes: 172 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,177 @@
# GoChat Multi-Container Deployment - Change Log

## 2026-01-26

### Session: Fixed Messages Not Displaying Bug

**Bug Fix: Messages Not Displaying After Send**
- Fixed critical bug where sent messages were not appearing in chat window in real-time
- Root cause: WebSocket port mismatch between frontend and Docker configuration

**Root Cause Analysis:**
1. Frontend hardcodes WebSocket URL as `ws://127.0.0.1:7000/ws`
2. `.env` file had `WS_PORT=17000`, mapping container port 7000 to host port 17000
3. Frontend could not connect to WebSocket, so real-time message delivery failed
4. Messages were saved to database successfully, but not pushed via WebSocket

**Fixes Applied:**

**.env**
- Changed `WS_PORT=17000` to `WS_PORT=7000` to match frontend expectation

**api/rpc/rpc.go** (Additional improvement)
- Added proper error handling for all RPC client functions
- Now returns `FailReplyCode` when RPC calls fail instead of silently succeeding
- Affected functions: `Login`, `Register`, `GetUserNameByUserId`, `CheckAuth`, `Logout`, `Push`, `PushRoom`, `Count`, `GetRoomInfo`, `GetSingleChatHistory`, `GetRoomHistory`

---

### Session: Added Image Message Support

**Feature: Image Message Uploads and Delivery**
- Users can now upload images and send image messages in single chat and room chat
- Images are stored in MinIO object storage with automatic bucket creation
- Supports JPEG, PNG, GIF, and WebP formats up to 10MB

**docker-compose.yml**
- Added MinIO service (minio/minio:latest) on 172.28.0.14
- Added ports 9000 (API) and 9001 (console) for MinIO
- Added `minio-data` volume for persistent storage
- Updated API service to depend on MinIO and added minio extra_host

**config/config.go**
- Added `CommonMinIO` struct with endpoint, accessKeyId, secretAccessKey, bucketName, useSSL
- Added `ContentTypeText`, `ContentTypeImage`, and `MaxImageSizeBytes` constants
- Updated `Common` struct to include `CommonMinIO`

**config/{dev,staging,prod}/common.toml**
- Added `[common-minio]` section with MinIO connection settings

**db/db.go**
- Added `ContentType` field to Message struct for database migration

**logic/dao/message.go**
- Added `ContentType` field to Message struct (default: "text")

**proto/logic.go**
- Added `ContentType` field to `Send`, `SendTcp`, and `MessageItem` structs

**api/handler/upload.go** (new file)
- Created `InitMinioClient()` to initialize MinIO client and create bucket
- Created `UploadImage()` handler for multipart form image uploads
- Validates file size (max 10MB) and content type (jpeg/png/gif/webp)
- Generates unique filenames with date-based paths (YYYY/MM/uuid.ext)

**api/handler/push.go**
- Added `ContentType` field to `FormPush` and `FormRoom` structs
- Updated `Push()` and `PushRoom()` to pass contentType to RPC

**api/router/router.go**
- Added route `POST /push/uploadImage` for image uploads

**api/chat.go**
- Added MinIO client initialization on API service startup

**logic/rpc.go**
- Updated `Push()` to save ContentType to database
- Updated `PushRoom()` to save ContentType to database
- Updated `GetSingleChatHistory()` to return ContentType in response
- Updated `GetRoomHistory()` to return ContentType in response

**go.mod**
- Added `github.com/minio/minio-go/v7` dependency

**tests/helpers/api_client.go**
- Added `PushWithContentType()` for sending messages with content type
- Added `PushRoomWithContentType()` for room messages with content type

**tests/integration/image_message_test.go** (new file)
- Test image upload success
- Test invalid image type rejection
- Test oversized image rejection
- Test sending image message (single chat)
- Test sending image message (room chat)
- Test image message appears in history with correct contentType

**scripts/feat-image-message.sh** (new file)
- Functional test script for image message feature
- Tests user registration, image upload, message sending, and history retrieval

---

## 2026-01-25

### Session: Added Message Persistence and History API

**Feature: Chat Message Persistence**
- Messages are now persisted to SQLite database before delivery
- Supports both single chat and room messages
- Synchronous persistence ensures messages are saved reliably

**logic/dao/message.go** (new file)
- Created `Message` model with fields: Id, FromUserId, FromUserName, ToUserId, ToUserName, RoomId, MessageType, Content, CreateTime
- Added `Add()` method for inserting messages
- Added `GetSingleChatHistory()` for retrieving chat history between two users
- Added `GetRoomHistory()` for retrieving room message history

**db/db.go**
- Migrated from SQLite to PostgreSQL
- Added `User` struct for auto-migration
- Added `Message` struct for auto-migration
- Added auto-migration for user and message tables on database initialization
- Added connection pool configuration (maxIdleConns, maxOpenConns, connMaxLifetime)

**config/config.go**
- Added `CommonPostgreSQL` struct with database connection settings

**config/{dev,staging,prod}/common.toml**
- Added `[common-postgresql]` section with host, port, user, password, dbname, sslmode, connection pool settings

**docker-compose.yml**
- Added PostgreSQL service (postgres:15-alpine) on 172.28.0.13
- Added `postgres-data` volume for data persistence
- Updated logic service to depend on postgres

**logic/rpc.go**
- Modified `Push()` to persist single chat messages before publishing to RabbitMQ
- Modified `PushRoom()` to persist room messages before publishing to RabbitMQ
- Added `GetSingleChatHistory()` RPC method for retrieving single chat history
- Added `GetRoomHistory()` RPC method for retrieving room message history

**proto/logic.go**
- Added `GetSingleChatHistoryRequest` with CurrentUserId, OtherUserId, Limit, Offset
- Added `GetRoomHistoryRequest` with RoomId, Limit, Offset
- Added `MessageItem` for representing messages in responses
- Added `GetMessageHistoryResponse` with Code and Messages array

**api/handler/message.go** (new file)
- Created `GetSingleChatHistory()` HTTP handler
- Created `GetRoomHistory()` HTTP handler

**api/rpc/rpc.go**
- Added `GetSingleChatHistory()` RPC client method
- Added `GetRoomHistory()` RPC client method

**api/router/router.go**
- Added route `POST /push/history/single` for single chat history
- Added route `POST /push/history/room` for room chat history

**tests/helpers/api_client.go**
- Added `GetSingleChatHistory()` test client method
- Added `GetRoomHistory()` test client method

**tests/integration/message_history_test.go** (new file)
- Added integration tests for single chat history retrieval
- Added integration tests for room chat history retrieval
- Added tests for pagination and invalid token handling

**scripts/perf-optimization.sh** (new file)
- Created functional test script for message persistence feature
- Tests user registration, message sending, and history retrieval
- Validates both single chat and room message persistence

---

## 2025-12-30

### Session 3: Added CI/CD Pipeline
Expand Down
6 changes: 6 additions & 0 deletions api/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"syscall"
"time"

"gochat/api/handler"
"gochat/api/router"
"gochat/api/rpc"
"gochat/config"
Expand Down Expand Up @@ -53,6 +54,11 @@ func (c *Chat) Run() {
//init rpc client
rpc.InitLogicRpcClient()

// Initialize MinIO client for image uploads
if err := handler.InitMinioClient(); err != nil {
logrus.Warnf("Failed to initialize MinIO client: %v", err)
}

r := router.Register()
runMode := config.GetGinRunMode()
logrus.Info("server start , now run mode is ", runMode)
Expand Down
82 changes: 82 additions & 0 deletions api/handler/message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Message history handlers
*/
package handler

import (
"gochat/api/ctxutil"
"gochat/api/rpc"
"gochat/proto"
"gochat/tools"

"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)

type FormSingleChatHistory struct {
AuthToken string `form:"authToken" json:"authToken" binding:"required"`
OtherUserId int `form:"otherUserId" json:"otherUserId" binding:"required"`
Limit int `form:"limit" json:"limit"`
Offset int `form:"offset" json:"offset"`
}

// GetSingleChatHistory retrieves message history between the current user and another user
func GetSingleChatHistory(c *gin.Context) {
var form FormSingleChatHistory
if err := c.ShouldBindBodyWith(&form, binding.JSON); err != nil {
tools.FailWithMsg(c, err.Error())
return
}

// Get current user from context (set by auth middleware)
currentUserId, _, ok := ctxutil.GetAuthFromContext(c)
if !ok {
tools.FailWithMsg(c, "auth info not found in context")
return
}

ctx := c.Request.Context()
req := &proto.GetSingleChatHistoryRequest{
CurrentUserId: currentUserId,
OtherUserId: form.OtherUserId,
Limit: form.Limit,
Offset: form.Offset,
}

code, messages := rpc.RpcLogicObj.GetSingleChatHistory(ctx, req)
if code == tools.CodeFail {
tools.FailWithMsg(c, "rpc get single chat history fail!")
return
}
tools.SuccessWithMsg(c, "ok", messages)
}

type FormRoomHistory struct {
AuthToken string `form:"authToken" json:"authToken" binding:"required"`
RoomId int `form:"roomId" json:"roomId" binding:"required"`
Limit int `form:"limit" json:"limit"`
Offset int `form:"offset" json:"offset"`
}

// GetRoomHistory retrieves message history for a room
func GetRoomHistory(c *gin.Context) {
var form FormRoomHistory
if err := c.ShouldBindBodyWith(&form, binding.JSON); err != nil {
tools.FailWithMsg(c, err.Error())
return
}

ctx := c.Request.Context()
req := &proto.GetRoomHistoryRequest{
RoomId: form.RoomId,
Limit: form.Limit,
Offset: form.Offset,
}

code, messages := rpc.RpcLogicObj.GetRoomHistory(ctx, req)
if code == tools.CodeFail {
tools.FailWithMsg(c, "rpc get room history fail!")
return
}
tools.SuccessWithMsg(c, "ok", messages)
}
26 changes: 19 additions & 7 deletions api/handler/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import (
)

type FormPush struct {
Msg string `form:"msg" json:"msg" binding:"required"`
ToUserId string `form:"toUserId" json:"toUserId" binding:"required"`
RoomId int `form:"roomId" json:"roomId" binding:"required"`
AuthToken string `form:"authToken" json:"authToken" binding:"required"`
Msg string `form:"msg" json:"msg" binding:"required"`
ToUserId string `form:"toUserId" json:"toUserId" binding:"required"`
RoomId int `form:"roomId" json:"roomId" binding:"required"`
AuthToken string `form:"authToken" json:"authToken" binding:"required"`
ContentType string `form:"contentType" json:"contentType"` // "text" or "image", defaults to "text"
}

func Push(c *gin.Context) {
Expand All @@ -48,6 +49,10 @@ func Push(c *gin.Context) {
return
}
roomId := formPush.RoomId
contentType := formPush.ContentType
if contentType == "" {
contentType = config.ContentTypeText
}
req := &proto.Send{
Msg: msg,
FromUserId: fromUserId,
Expand All @@ -56,6 +61,7 @@ func Push(c *gin.Context) {
ToUserName: toUserName,
RoomId: roomId,
Op: config.OpSingleSend,
ContentType: contentType,
}
code, rpcMsg := rpc.RpcLogicObj.Push(ctx, req)
if code == tools.CodeFail {
Expand All @@ -67,9 +73,10 @@ func Push(c *gin.Context) {
}

type FormRoom struct {
AuthToken string `form:"authToken" json:"authToken" binding:"required"`
Msg string `form:"msg" json:"msg" binding:"required"`
RoomId int `form:"roomId" json:"roomId" binding:"required"`
AuthToken string `form:"authToken" json:"authToken" binding:"required"`
Msg string `form:"msg" json:"msg" binding:"required"`
RoomId int `form:"roomId" json:"roomId" binding:"required"`
ContentType string `form:"contentType" json:"contentType"` // "text" or "image", defaults to "text"
}

func PushRoom(c *gin.Context) {
Expand All @@ -87,12 +94,17 @@ func PushRoom(c *gin.Context) {
tools.FailWithMsg(c, "auth info not found in context")
return
}
contentType := formRoom.ContentType
if contentType == "" {
contentType = config.ContentTypeText
}
req := &proto.Send{
Msg: msg,
FromUserId: fromUserId,
FromUserName: fromUserName,
RoomId: roomId,
Op: config.OpRoomSend,
ContentType: contentType,
}
code, msg := rpc.RpcLogicObj.PushRoom(ctx, req)
if code == tools.CodeFail {
Expand Down
Loading
Loading