You need to sign in or sign up before continuing.
Commits (14)
# README
### 1.go返回数组时格式问题
### 2.添加了头像
### 3.使用了`react-icons/md`
## unfinished
### 1.deleted function
module chatroom
go 1.24.3
require (
github.com/gin-gonic/gin v1.10.1
github.com/lib/pq v1.10.9
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
)
const (
host = "localhost"
port = 5432
user = "chat_room_user"
password = "secure_password"
dbname = "chat_room_db"
)
type Message struct {
MessageId int `json:"message_id"`
RoomId int `json:"room_id"`
Profile int `json:"profile"`
Sender string `json:"sender"`
Content string `json:"content"`
Time string `json:"time"`
}
type RoomPreviewInfo struct {
RoomId int `json:"roomId"`
RoomName string `json:"roomName"`
LastSender sql.NullString `json:"lastSender"`
LastContent sql.NullString `json:"lastContent"`
LastTime sql.NullTime `json:"lastTime"`
}
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
type RoomAddRes struct {
RoomId int `json:"room_id"`
}
var db *sql.DB
func main() {
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
var err error
db, err = sql.Open("postgres", psqlInfo)
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatal(err)
}
createTable()
var tableExists bool
err = db.QueryRow(`
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = 'rooms'
)
`).Scan(&tableExists)
if err != nil {
log.Fatal("Table check failed: ", err)
}
if !tableExists {
log.Println("Table 'rooms' not found, creating...")
createTable()
} else {
log.Println("Table 'rooms' already exists")
}
router := gin.Default()
router.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
router.POST("/room/add", AddNewRoom)
router.GET("/room/list", GetRoomList)
router.POST("/room/delete", DeleteRoom)
router.POST("/room/message/add", AddMessage)
router.GET("/room/message/list", GetMessageList)
router.PUT("/room/message/update", RoomMessageUpdate)
router.Run(":8080")
}
func AddNewRoom(c *gin.Context) {
var room RoomPreviewInfo
if err := c.ShouldBindJSON(&room); err != nil {
c.JSON(400, Response{Code: 400, Msg: "Invalid input", Data: nil})
return
}
var roomId int
err := db.QueryRow(
"INSERT INTO rooms (room_name) VALUES ($1) RETURNING room_id",
room.RoomName,
).Scan(&roomId)
if err != nil {
c.JSON(500, Response{Code: 500, Msg: "Failed to add room", Data: nil})
return
}
room.RoomId = roomId
c.JSON(200, RoomAddRes{
RoomId: room.RoomId,
})
log.Printf("New room added: %+v", room)
}
func GetRoomList(c *gin.Context) {
var total int
roomCountQuery := "SELECT COUNT(*) FROM rooms"
err := db.QueryRow(roomCountQuery).Scan(&total)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "总数统计失败:" + err.Error(),
})
return
}
query := `
SELECT room_id, room_name, last_sender, last_content, last_time
FROM rooms
ORDER BY last_time DESC NULLS LAST
`
args := []interface{}{}
rows, err := db.Query(query, args...)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "Falie while serch:" + err.Error(),
})
return
}
defer rows.Close()
var roomList []RoomPreviewInfo
for rows.Next() {
var roomItem RoomPreviewInfo
if err := rows.Scan(
&roomItem.RoomId,
&roomItem.RoomName,
&roomItem.LastSender,
&roomItem.LastContent,
&roomItem.LastTime,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "Faile while parsing" + err.Error(),
})
return
}
roomList = append(roomList, roomItem)
}
c.JSON(http.StatusOK, Response{
Code: 0,
Msg: "Success",
Data: roomList,
})
}
func DeleteRoom(c *gin.Context) {
roomId, err := strconv.Atoi((c.Query("roomTd")))
if err != nil || roomId <= 0 {
c.JSON(http.StatusOK, Response{Code: 400, Msg: "Invalid ID"})
return
}
result, err := db.Exec("DELETE FROM rooms WHERE roomId = $1", roomId)
if err != nil {
c.JSON(http.StatusOK, Response{Code: 500, Msg: err.Error()})
return
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
c.JSON(http.StatusOK, Response{Code: 400, Msg: "Room not found"})
return
}
c.JSON(http.StatusOK, Response{
Code: 0,
Msg: "Room deleted",
Data: nil,
})
}
func AddMessage(c *gin.Context) {
var message struct {
RoomId int `json:"roomId"`
ProfileId int `json:"profile"`
Sender string `json:"sender"`
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&message); err != nil {
c.JSON(http.StatusBadRequest, Response{Code: 400, Msg: "Invalid input: " + err.Error(), Data: nil})
return
}
// 写入评论
query := `
INSERT INTO messages (room_id, profile, sender, content, "time")
VALUES ($1, $2, $3, $4, NOW())
RETURNING message_id
`
var messageId int
err := db.QueryRow(query, message.RoomId, message.ProfileId, message.Sender, message.Content).Scan(&messageId)
if err != nil {
c.JSON(http.StatusInternalServerError, Response{Code: 500, Msg: "Failed to add message: " + err.Error(), Data: nil})
return
}
// 更新room
_, err = db.Exec(
"UPDATE rooms SET last_sender = $1, last_content = $2, last_time = NOW() WHERE room_id = $3",
message.Sender, message.Content, message.RoomId,
)
if err != nil {
log.Printf("Failed to update room preview: %v", err)
c.JSON(http.StatusInternalServerError, Response{Code: 500, Msg: "Failed to updata lastest message: " + err.Error(), Data: nil})
return
}
c.JSON(http.StatusOK, Response{
Code: 0,
Msg: "Message added successfully",
Data: gin.H{"messageId": messageId},
})
log.Printf("New message with ID %d added to room %d", messageId, message.RoomId)
}
func GetMessageList(c *gin.Context) {
roomId := c.Query("roomId")
if roomId == "" {
c.JSON(400, Response{Code: 400, Msg: "Room ID is required", Data: nil})
return
}
rows, err := db.Query("SELECT profile, sender, content, time FROM messages WHERE room_id = $1 ORDER BY time ASC", roomId)
if err != nil {
c.JSON(500, Response{Code: 500, Msg: "Failed to retrieve messages", Data: nil})
return
}
defer rows.Close()
var messages []Message
for rows.Next() {
var msg Message
if err := rows.Scan(&msg.Profile, &msg.Sender, &msg.Content, &msg.Time); err != nil {
c.JSON(500, Response{Code: 500, Msg: "Failed to scan message", Data: nil})
return
}
messages = append(messages, msg)
}
c.JSON(200, Response{
Code: 0,
Msg: "Messages retrieved successfully",
Data: messages,
})
}
func RoomMessageUpdate(c *gin.Context) {
// 处理更新房间消息的逻辑
}
func createTable() {
query := `
CREATE TABLE IF NOT EXISTS rooms (
room_id SERIAL PRIMARY KEY,
room_name VARCHAR(100) NOT NULL UNIQUE,
last_sender VARCHAR(100),
last_content TEXT,
last_time TIMESTAMP
);
CREATE TABLE IF NOT EXISTS messages (
message_id SERIAL PRIMARY KEY,
room_id INT NOT NULL,
profile INT NOT NULL,
sender VARCHAR(100) NOT NULL,
content TEXT NOT NULL,
"time" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (room_id) REFERENCES rooms(room_id)
);
`
_, err := db.Exec(query)
if err != nil {
log.Fatal("Failed to create tables: ", err)
}
log.Println("Tables created successfully")
}
module xlab.zju.edu.cn/git/1158csth/intern_project_frontend_backend
go 1.24.3
package handlers
import (
"fmt"
"net/http"
"path/filepath"
"github.com/gin-gonic/gin"
"your-app/models"
"your-app/services"
)
func SendMessage(c *gin.Context) {
conversationID := c.Param("conversationId")
// 处理多模态输入
text := c.PostForm("text")
file, err := c.FormFile("image")
var imageBase64 string
if file != nil {
// 保存图片并转换为Base64
dst := fmt.Sprintf("./uploads/%s", file.Filename)
if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
imageBase64 = services.ImageToBase64(dst)
}
// 保存用户消息到数据库
userMsg := models.Message{
ConversationID: conversationID,
Role: "user",
Type: models.Text,
Content: text,
}
if imageBase64 != "" {
userMsg.Type = models.Image
userMsg.Content = imageBase64
}
// 保存消息(伪代码)
db.Create(&userMsg)
// 调用LLM服务
llmResponse, err := services.CallLLMAPI(conversationID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "LLM服务调用失败"})
return
}
// 保存AI回复
aiMsg := models.Message{
ConversationID: conversationID,
Role: "assistant",
Type: models.Text,
Content: llmResponse,
}
db.Create(&aiMsg)
c.JSON(http.StatusOK, gin.H{
"userMessage": userMsg,
"aiMessage": aiMsg,
})
}
\ No newline at end of file
package models
import "time"
type MessageType string
const (
Text MessageType = "text"
Image MessageType = "image"
)
type Message struct {
ID uint `gorm:"primaryKey" json:"id"`
ConversationID uint `json:"conversation_id"`
Role string `json:"role"` // "user" or "assistant"
Type MessageType `json:"type"`
Content string `json:"content"` // 文本内容或图片Base64
CreatedAt time.Time `json:"created_at"`
}
\ No newline at end of file
package services
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"your-app/models"
)
func CallLLMAPI(conversationID string) (string, error) {
// 获取对话上下文
var messages []models.Message
db.Where("conversation_id = ?", conversationID).Find(&messages)
// 构造多模态请求
payload := map[string]interface{}{
"model": "gpt-4-vision-preview",
"messages": buildMessages(messages),
}
payloadBytes, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(payloadBytes))
req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
// 解析响应
var result map[string]interface{}
json.Unmarshal(body, &result)
choices := result["choices"].([]interface{})
message := choices[0].(map[string]interface{})["message"].(map[string]interface{})
return message["content"].(string), nil
}
// 构建多模态消息
func buildMessages(messages []models.Message) []map[string]interface{} {
var result []map[string]interface{}
for _, msg := range messages {
content := []interface{}{{"type": "text", "text": msg.Content}}
if msg.Type == models.Image {
content = append(content, map[string]interface{}{
"type": "image_url",
"image_url": map[string]string{
"url": fmt.Sprintf("data:image/jpeg;base64,%s", msg.Content),
},
})
}
result = append(result, map[string]interface{}{
"role": msg.Role,
"content": content,
})
}
return result
}
\ No newline at end of file
...@@ -10,7 +10,8 @@ ...@@ -10,7 +10,8 @@
"dependencies": { "dependencies": {
"next": "15.3.3", "next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"react-icons": "^5.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
...@@ -837,6 +838,15 @@ ...@@ -837,6 +838,15 @@
"react": "^19.1.0" "react": "^19.1.0"
} }
}, },
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.26.0", "version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
......
...@@ -9,14 +9,15 @@ ...@@ -9,14 +9,15 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"next": "15.3.3" "react-icons": "^5.5.0"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19" "@types/react-dom": "^19",
"typescript": "^5"
} }
} }
...@@ -72,4 +72,75 @@ h1, h2, h3, h4, h5, h6, ...@@ -72,4 +72,75 @@ h1, h2, h3, h4, h5, h6,
--font-size-base: 12px; --font-size-base: 12px;
--font-size-sm: 11px; --font-size-sm: 11px;
} }
}
.context-menu {
/* From Uiverse.io by andrew-demchenk0 */
.button {
--main-focus: #2d8cf0;
--font-color: #323232;
--bg-color-sub: #dedede;
--bg-color: #eee;
--main-color: #323232;
position: relative;
width: 150px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
border: 2px solid var(--main-color);
box-shadow: 4px 4px var(--main-color);
background-color: var(--bg-color);
border-radius: 10px;
overflow: hidden;
}
.button, .button__icon, .button__text {
transition: all 0.3s;
}
.button .button__text {
transform: translateX(33px);
color: var(--font-color);
font-weight: 600;
}
.button .button__icon {
position: absolute;
transform: translateX(109px);
height: 100%;
width: 39px;
background-color: var(--bg-color-sub);
display: flex;
align-items: center;
justify-content: center;
}
.button .svg {
width: 20px;
fill: var(--main-color);
}
.button:hover {
background: var(--bg-color);
}
.button:hover .button__text {
color: transparent;
}
.button:hover .button__icon {
width: 148px;
transform: translateX(0);
}
.button:active {
transform: translate(3px, 3px);
box-shadow: 0px 0px var(--main-color);
}
}
.root {
display: flex;
position: relative;
} }
\ No newline at end of file
import ChatRoom from "./pages/ChatRoom/ChatRoom"; 'use client';
import React, { useState } from 'react';
import { ChatRoom } from "./pages/ChatRoom/ChatRoom";
import { SetName } from './pages/SetName/SetName';
import "./globals.css"; import "./globals.css";
export default function Page() { export default function Page() {
const [userName, setUserName] = useState('');
const handleLogin = (name: string) => {
setUserName(name);
};
return ( return (
<div> <div className='root'>
<ChatRoom /> {!userName ? (
<SetName onLogin={handleLogin} />
) : (
<ChatRoom userName={userName} />
)}
</div> </div>
); );
} }
\ No newline at end of file
.chat-room { .chat-room {
display: flex; display: flex;
position: relative; position: fixed;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
box-sizing: border-box; box-sizing: border-box;
......
'use client'; 'use client';
import "./ChatRoom.css"; import "./ChatRoom.css";
import React, { Component } from "react"; import React, { useEffect, useState } from "react";
const backEnd:string = "http://localhost:8080";
// profile
const Profile = [ 'https://pic4.zhimg.com/v2-c5a0d0d57c1a85c6db56e918707f54a3_r.jpg', const Profile = [ 'https://pic4.zhimg.com/v2-c5a0d0d57c1a85c6db56e918707f54a3_r.jpg',
'https://pic2.zhimg.com/v2-c2e79191533fdc7fced2f658eef987c9_r.jpg', 'https://pic2.zhimg.com/v2-c2e79191533fdc7fced2f658eef987c9_r.jpg',
'https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg', 'https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg',
...@@ -14,13 +15,25 @@ const Profile = [ 'https://pic4.zhimg.com/v2-c5a0d0d57c1a85c6db56e918707f54a3_r. ...@@ -14,13 +15,25 @@ const Profile = [ 'https://pic4.zhimg.com/v2-c5a0d0d57c1a85c6db56e918707f54a3_r.
const RoomProfile = 'https://tse1-mm.cn.bing.net/th/id/OIP-C.0KyBJKAdIGi9SAQc_X62tQHaLr?cb=thvnextc2&rs=1&pid=ImgDetMain'; const RoomProfile = 'https://tse1-mm.cn.bing.net/th/id/OIP-C.0KyBJKAdIGi9SAQc_X62tQHaLr?cb=thvnextc2&rs=1&pid=ImgDetMain';
interface RoomEntryProps { interface RoomEntryProps {
roomId: number; // room id roomId: number;
roomName: string; // room name roomName: string;
lastSender: string; // the lasted User name lastSender: { String: string, Valid: boolean };
lastContent: string; // content lastContent: { String: string, Valid: boolean };
lastTime: number; // the lasted message time lastTime: { Time: string, Valid: boolean };
}
interface MessageProps {
roomId: number;
roomName: string;
messages: Array<{
profile: number;
sender: string;
content: string;
time: string;
}>;
} }
function RoomEntry (props: RoomEntryProps) {
function RoomEntry ({rooms, onRoomClick} : {rooms: RoomEntryProps[], onRoomClick: (roomId: number, roomName: string) => void}) {
return ( return (
<div className="chat-room-nav"> <div className="chat-room-nav">
<div className="sidebar-action"> <div className="sidebar-action">
...@@ -32,42 +45,65 @@ function RoomEntry (props: RoomEntryProps) { ...@@ -32,42 +45,65 @@ function RoomEntry (props: RoomEntryProps) {
</div> </div>
<div className="chat-list"> <div className="chat-list">
<div className="chat-item"> {rooms.map((room) => (
<img src={RoomProfile} alt="Avatar" className="avatar" /> <div className="chat-item" key={room.roomId} onClick={() => onRoomClick(room.roomId, room.roomName)}>
<div className="chat-info"> <img src={RoomProfile} alt="Avatar" className="avatar" />
<h3>{props.roomName}</h3> <div className="chat-info">
<span className="chat-message">{props.lastSender}: {props.lastContent}</span> <h3>{room.roomName}</h3>
<span className="chat-time">Time</span> <span className="chat-message">
</div> {room.lastSender.Valid ? room.lastSender.String : ''}:
</div> {room.lastContent.Valid ? room.lastContent.String : ''}</span>
<span className="chat-time">{room.lastTime.Valid ? formatTimeToHoursMinutes(room.lastTime.Time) : ''}</span>
</div>
</div>
))}
</div> </div>
</div> </div>
); );
// Button From Uiverse.io by njesenberger // Button From Uiverse.io by njesenberger
} }
function addNewRoom() { function formatTimeToHoursMinutes(isoString: string) {
const RoomNameInput = (document.getElementsByClassName("RoomNameInput")[0] as HTMLInputElement).value; const date = new Date(isoString);
if (RoomNameInput === "") { const hours = String(date.getHours()).padStart(2, '0'); // 确保两位数
alert("Please enter a room name."); const minutes = String(date.getMinutes()).padStart(2, '0'); // 确保两位数
return; return `${hours}:${minutes}`;
} }
const chatList = document.getElementsByClassName("chat-list")[0]; function InputRoomNameArea({ onAddNewRoom }: { onAddNewRoom: (roomName: string) => void}) {
const newRoom = document.createElement("div"); const [roomNameInput, setRoomNameInput] = useState("");
newRoom.className = "chat-item";
newRoom.innerHTML = `
<img src="${RoomProfile}" alt="Avatar" class="avatar" />
<div class="chat-info">
<h3>${RoomNameInput}</h3>
<span class="chat-message">New message</span>
<span class="chat-time">Time</span>
</div>
`;
chatList.appendChild(newRoom);
closeOpenDiv(); const handleAddNewRoom = () => {
onAddNewRoom(roomNameInput);
setRoomNameInput("");
closeOpenDiv();
}
return (
<div className="open">
<div className="roomName-input">
<h3>Please Enter the New Room Name</h3>
<input
type="text"
className="RoomNameInput"
placeholder="Start new chat"
value = {roomNameInput}
onChange={(e) => setRoomNameInput(e.target.value)}
onKeyUpCapture={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleAddNewRoom();
}
else if (e.key === 'Escape') {
closeOpenDiv();
}
}}
/>
<div className="button-container">
<button className="create-button" onClick={handleAddNewRoom}>Submit</button>
<button className="cancel-button" onClick={closeOpenDiv}>Cancel</button>
</div>
</div>
</div>
);
} }
function openOpenDiv() { function openOpenDiv() {
...@@ -85,19 +121,21 @@ function closeOpenDiv() { ...@@ -85,19 +121,21 @@ function closeOpenDiv() {
(document.getElementsByClassName("RoomNameInput")[0] as HTMLInputElement).value = ''; (document.getElementsByClassName("RoomNameInput")[0] as HTMLInputElement).value = '';
} }
function MessageItem (props: MessageProps & { onAddNewComment: (content: string) => void}) {
const [inputValue, setInputValue] = useState("");
// 单个聊天房间组件 if (props.roomId === 0) {
interface MessageProps { return <div className="message-item">Please select a room to chat.</div>;
roomId: number; // room id }
roomName: string; // room name
messages: Array<{ const handlerSend = () => {
profile: number; // profile index if (inputValue.trim() === '') {
sender: string; // sender name alert("Message can't be empty");
content: string; // message content return
time: number; // message time }
}>; props.onAddNewComment(inputValue);
} setInputValue('');
function MessageItem (props: MessageProps) { }
return ( return (
<div className="message-item"> <div className="message-item">
<div className="message-header"> <div className="message-header">
...@@ -111,7 +149,7 @@ function MessageItem (props: MessageProps) { ...@@ -111,7 +149,7 @@ function MessageItem (props: MessageProps) {
<div className="message-content"> <div className="message-content">
<div className="message-info"> <div className="message-info">
<span className="message-sender">{msg.sender}</span> <span className="message-sender">{msg.sender}</span>
<span className="message-time">{new Date(msg.time).toLocaleTimeString()}</span> <span className="message-time">{formatTimeToHoursMinutes(msg.time)}</span>
</div> </div>
<p className="message-text">{msg.content}</p> <p className="message-text">{msg.content}</p>
</div> </div>
...@@ -119,13 +157,19 @@ function MessageItem (props: MessageProps) { ...@@ -119,13 +157,19 @@ function MessageItem (props: MessageProps) {
))} ))}
</div> </div>
<div className="message-input"> <div className="message-input">
<input type="text" placeholder="Type a message..." className="Inputarea" onKeyUpCapture={ <input
type="text"
placeholder="Type a message..."
className="Inputarea"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyUpCapture={
(e: React.KeyboardEvent<HTMLInputElement>) => { (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
addNewComment(props.roomId, "蔡徐坤", (e.target as HTMLInputElement).value); handlerSend();
} }
}}/> }}/>
<button className="send-button" onClick={() => addNewComment(props.roomId, "蔡徐坤", (document.getElementsByClassName("Inputarea")[0] as HTMLInputElement).value)}> <button className="send-button" onClick={handlerSend}>
<div className="svg-wrapper-1"> <div className="svg-wrapper-1">
<div className="svg-wrapper"> <div className="svg-wrapper">
<svg <svg
...@@ -150,74 +194,156 @@ function MessageItem (props: MessageProps) { ...@@ -150,74 +194,156 @@ function MessageItem (props: MessageProps) {
// From Uiverse.io by adamgiebl // From Uiverse.io by adamgiebl
} }
// add new comment export function ChatRoom({ userName }: { userName: string }) {
function addNewComment(roomId: number, sender: string, content: string) { const [rooms, setRooms] = useState<RoomEntryProps[]>([]);
const messageList = document.getElementsByClassName("message-list"); const [currentRoom, setCurrentRoom] = useState<MessageProps | null>(null);
if (content === "") { useEffect(() => {
alert("Please enter a message before sending."); const fetchRooms = async () => {
return; try {
const response = await fetch(backEnd+"/room/list");
const result = await response.json();
if (result.code === 0) {
const ferchedRooms = result.data.map((room:any) => ({
...room,
lastSender: room.lastSender || { String: '', Valid: false },
laseComment: room.lastComment || { String: '', Valid: false },
lastTime: room.lastTime || { String: '', Valid: false }
}));
setRooms(ferchedRooms);
}
} catch (error) {
console.error("Error fetching rooms:", error);
}
} }
fetchRooms();
}, []);
var profileId = 0; const handleRoomClick = async (roomId: number, roomName: string) => {
if (sender === "蔡徐坤") { setCurrentRoom({
profileId = Profile.length - 1; roomId: roomId,
roomName: roomName,
messages: []
});
try {
const response = await fetch(backEnd+`/room/message/list?roomId=${roomId}`)
const result = await response.json();
debugger;
if (result.code === 0) {
setCurrentRoom({
roomId: roomId,
roomName: roomName,
messages: result.data || []
});
} else {
alert(`Error fetching messages: ${result.msg}`);
setCurrentRoom({
roomId: roomId,
roomName: roomName,
messages: []
});
}
} catch (error){
console.error("Error fetching messages:", error);
alert("Failed to fetch messages. See console for details.");
}
}
async function addNewRoom(roomName: string) {
if (roomName.trim() === "") {
alert("Please enter a room name.");
return;
} }
else {
try {
const response = await fetch (backEnd + "/room/add", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ roomName: roomName })
})
const result = await response.json();
if (result.room_id > 0) {
const newRoom:RoomEntryProps = {
roomId: result.RoomId,
roomName: roomName,
lastSender: { String: '', Valid: false } ,
lastContent: { String: '', Valid: false },
lastTime: { Time: '', Valid: false },
};
setRooms(prevRooms => [newRoom, ...prevRooms]);
} else {
alert("Faile to add a new room" + result.Msg);
}
} catch (error) {
console.error("Error adding new room" + error);
alert("Error adding new room.");
}
}
const addNewComment = async (content: string) => {
if (!currentRoom)
return;
let profileId = 0;
if (userName === '蔡徐坤') {
profileId = Profile.length - 1;
} else {
profileId = Math.floor(Math.random() * (Profile.length-1)); profileId = Math.floor(Math.random() * (Profile.length-1));
} }
const message = document.createElement("div"); try {
message.className = "message"; const response = await fetch(backEnd + '/room/message/add', {
message.innerHTML = ` method: 'POST',
<img src="${Profile[profileId]}" alt="${sender}'s avatar" class="avatar"> headers: {'Content-Type': 'application/json'},
<div class="message-content"> body: JSON.stringify({
<div class="message-info"> roomId: currentRoom.roomId,
<span class="message-sender">${sender}</span> profile_id: profileId,
<span class="message-time">${new Date().toLocaleTimeString()}</span> sender: userName,
</div> content
<p class="message-text">${content}</p> })
</div> })
`;
(document.getElementsByClassName("Inputarea")[0] as HTMLInputElement).value = ''; const result = await response.json();
debugger;
if (result.code != 0) {
alert(`Error: ${result.Msg}`);
return;
}
const newMessage: { profile: number, sender: string, content: string, time: string } = {
profile: profileId,
sender: userName,
content: content,
time: new Date().toISOString()
};
messageList[0].appendChild(message); setCurrentRoom(prevRoom => {
} if (!prevRoom) return null;
return {
...prevRoom,
messages: [...prevRoom.messages, newMessage]
};
});
export default function ChatRoom() { } catch (error) {
return ( console.error("Error in addNewComment:", error);
<div className="chat-room"> alert("An error occurred while sending the message.");
<RoomEntry roomId={1} roomName="General" lastSender="Alice" lastContent="Hello!aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" lastTime={Date.now()} /> }
<MessageItem }
roomId={1}
roomName="General" return (
messages={[ <div className="chat-room">
{ profile: 2, sender: "Alice", content: "Hello!", time: Date.now() - 60000 } <RoomEntry rooms={rooms} onRoomClick={handleRoomClick}/>
]} <MessageItem
/> roomId={currentRoom?.roomId || 0}
<div className="open"> roomName={currentRoom?.roomName || ""}
<div className="roomName-input"> messages={currentRoom?.messages || []}
<h3>Please Enter the New Room Name</h3> onAddNewComment={addNewComment}
<input />
type="text" <InputRoomNameArea onAddNewRoom={addNewRoom}/>
className="RoomNameInput" </div>
placeholder="Search or start new chat" );
onKeyUpCapture={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
addNewRoom();
}
else if (e.key === 'Escape') {
closeOpenDiv();
}
}}
/>
<div className="button-container">
<button className="create-button" onClick={addNewRoom}>Submit</button>
<button className="cancel-button" onClick={closeOpenDiv}>Cancel</button>
</div>
</div>
</div>
</div>
);
} }
\ No newline at end of file