diff --git a/controller/offline_message.go b/controller/offline_message.go
new file mode 100644
index 0000000..e4a87da
--- /dev/null
+++ b/controller/offline_message.go
@@ -0,0 +1,451 @@
+package controller
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "goflylivechat/models"
+ "goflylivechat/tools"
+ "log"
+ "strconv"
+ "time"
+)
+
+func SubmitOfflineMessage(c *gin.Context) {
+ kefuId := c.PostForm("kefu_id")
+ visitorId := c.PostForm("visitor_id")
+ visitorName := c.PostForm("visitor_name")
+ visitorEmail := c.PostForm("visitor_email")
+ visitorPhone := c.PostForm("visitor_phone")
+ subject := c.PostForm("subject")
+ content := c.PostForm("content")
+ refer := c.PostForm("refer")
+ extra := c.PostForm("extra")
+
+ if kefuId == "" || content == "" {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "客服ID和留言内容不能为空",
+ })
+ return
+ }
+
+ if visitorName == "" {
+ visitorName = "匿名访客"
+ }
+
+ if !tools.LimitFreqSingle("offline_message:"+c.ClientIP(), 1, 60) {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "提交频率过快,请稍后再试",
+ })
+ return
+ }
+
+ kefuInfo := models.FindUser(kefuId)
+ if kefuInfo.ID == 0 {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "客服不存在",
+ })
+ return
+ }
+
+ if visitorId == "" {
+ visitorId = tools.Uuid()
+ }
+
+ sourceIp := c.ClientIP()
+
+ msgId := models.CreateOfflineMessage(
+ kefuId,
+ visitorId,
+ visitorName,
+ visitorEmail,
+ visitorPhone,
+ subject,
+ content,
+ sourceIp,
+ refer,
+ extra,
+ )
+
+ if visitorEmail != "" {
+ go SendOfflineMessageNotice(kefuInfo.Nickname, visitorName, content)
+ }
+
+ c.JSON(200, gin.H{
+ "code": 200,
+ "msg": "留言提交成功,客服将尽快回复您",
+ "result": gin.H{
+ "message_id": msgId,
+ },
+ })
+}
+
+func GetOfflineMessages(c *gin.Context) {
+ kefuName, _ := c.Get("kefu_name")
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+ pageSize, _ := strconv.Atoi(c.DefaultQuery("pagesize", "10"))
+ status := c.DefaultQuery("status", "")
+
+ if pageSize > 50 {
+ pageSize = 50
+ }
+
+ var query interface{}
+ var args []interface{}
+
+ if status != "" {
+ if !models.IsValidOfflineMessageStatus(status) {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "无效的状态值",
+ })
+ return
+ }
+ query = "offline_message.kefu_id = ? AND offline_message.status = ?"
+ args = append(args, kefuName.(string), status)
+ } else {
+ query = "offline_message.kefu_id = ?"
+ args = append(args, kefuName.(string))
+ }
+
+ count := models.CountOfflineMessages(query, args...)
+ list := models.FindOfflineMessagesByPage(uint(page), uint(pageSize), query, args...)
+
+ c.JSON(200, gin.H{
+ "code": 200,
+ "msg": "ok",
+ "result": gin.H{
+ "count": count,
+ "page": page,
+ "pagesize": pageSize,
+ "list": list,
+ },
+ })
+}
+
+func GetOfflineMessageDetail(c *gin.Context) {
+ kefuName, _ := c.Get("kefu_name")
+ idStr := c.Param("id")
+ id, err := strconv.ParseUint(idStr, 10, 64)
+ if err != nil {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "无效的留言ID",
+ })
+ return
+ }
+
+ msg := models.FindOfflineMessageDetailById(uint(id))
+ if msg.ID == 0 {
+ c.JSON(200, gin.H{
+ "code": 404,
+ "msg": "留言不存在",
+ })
+ return
+ }
+
+ if msg.KefuId != kefuName.(string) {
+ c.JSON(200, gin.H{
+ "code": 403,
+ "msg": "无权访问此留言",
+ })
+ return
+ }
+
+ if msg.Status == models.OfflineMessageStatusUnread {
+ models.MarkOfflineMessageAsRead(uint(id))
+ }
+
+ c.JSON(200, gin.H{
+ "code": 200,
+ "msg": "ok",
+ "result": gin.H{
+ "id": msg.ID,
+ "kefu_id": msg.KefuId,
+ "visitor_id": msg.VisitorId,
+ "visitor_name": msg.VisitorName,
+ "visitor_email": msg.VisitorEmail,
+ "visitor_phone": msg.VisitorPhone,
+ "subject": msg.Subject,
+ "content": msg.Content,
+ "status": msg.Status,
+ "status_text": models.GetOfflineMessageStatusText(msg.Status),
+ "reply_content": msg.ReplyContent,
+ "reply_time": msg.ReplyTime,
+ "replied_by": msg.RepliedBy,
+ "source_ip": msg.SourceIp,
+ "refer": msg.Refer,
+ "visitor_avator": msg.VisitorAvator,
+ "kefu_name": msg.KefuName,
+ "kefu_avator": msg.KefuAvator,
+ "create_time": msg.CreateTime,
+ },
+ })
+}
+
+func ReplyOfflineMessage(c *gin.Context) {
+ kefuName, _ := c.Get("kefu_name")
+ idStr := c.PostForm("id")
+ replyContent := c.PostForm("reply_content")
+
+ if idStr == "" || replyContent == "" {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "留言ID和回复内容不能为空",
+ })
+ return
+ }
+
+ id, err := strconv.ParseUint(idStr, 10, 64)
+ if err != nil {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "无效的留言ID",
+ })
+ return
+ }
+
+ msg := models.FindOfflineMessageById(uint(id))
+ if msg.ID == 0 {
+ c.JSON(200, gin.H{
+ "code": 404,
+ "msg": "留言不存在",
+ })
+ return
+ }
+
+ if msg.KefuId != kefuName.(string) {
+ c.JSON(200, gin.H{
+ "code": 403,
+ "msg": "无权回复此留言",
+ })
+ return
+ }
+
+ models.UpdateOfflineMessageReply(uint(id), replyContent, kefuName.(string))
+
+ if msg.VisitorEmail != "" {
+ go SendOfflineReplyNotice(msg.VisitorName, msg.VisitorEmail, replyContent)
+ }
+
+ c.JSON(200, gin.H{
+ "code": 200,
+ "msg": "回复成功",
+ })
+}
+
+func UpdateOfflineMessageStatus(c *gin.Context) {
+ kefuName, _ := c.Get("kefu_name")
+ idStr := c.Param("id")
+ status := c.PostForm("status")
+
+ if idStr == "" || status == "" {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "留言ID和状态不能为空",
+ })
+ return
+ }
+
+ if !models.IsValidOfflineMessageStatus(status) {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "无效的状态值",
+ })
+ return
+ }
+
+ id, err := strconv.ParseUint(idStr, 10, 64)
+ if err != nil {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "无效的留言ID",
+ })
+ return
+ }
+
+ msg := models.FindOfflineMessageById(uint(id))
+ if msg.ID == 0 {
+ c.JSON(200, gin.H{
+ "code": 404,
+ "msg": "留言不存在",
+ })
+ return
+ }
+
+ if msg.KefuId != kefuName.(string) {
+ c.JSON(200, gin.H{
+ "code": 403,
+ "msg": "无权操作此留言",
+ })
+ return
+ }
+
+ models.UpdateOfflineMessageStatus(uint(id), models.OfflineMessageStatus(status))
+
+ c.JSON(200, gin.H{
+ "code": 200,
+ "msg": "状态更新成功",
+ })
+}
+
+func DeleteOfflineMessage(c *gin.Context) {
+ kefuName, _ := c.Get("kefu_name")
+ idStr := c.Param("id")
+
+ if idStr == "" {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "留言ID不能为空",
+ })
+ return
+ }
+
+ id, err := strconv.ParseUint(idStr, 10, 64)
+ if err != nil {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "无效的留言ID",
+ })
+ return
+ }
+
+ msg := models.FindOfflineMessageById(uint(id))
+ if msg.ID == 0 {
+ c.JSON(200, gin.H{
+ "code": 404,
+ "msg": "留言不存在",
+ })
+ return
+ }
+
+ if msg.KefuId != kefuName.(string) {
+ c.JSON(200, gin.H{
+ "code": 403,
+ "msg": "无权删除此留言",
+ })
+ return
+ }
+
+ models.DeleteOfflineMessage(uint(id))
+
+ c.JSON(200, gin.H{
+ "code": 200,
+ "msg": "删除成功",
+ })
+}
+
+func GetOfflineMessageStats(c *gin.Context) {
+ kefuName, _ := c.Get("kefu_name")
+ stats := models.GetOfflineMessageStatsByKefuId(kefuName.(string))
+
+ c.JSON(200, gin.H{
+ "code": 200,
+ "msg": "ok",
+ "result": gin.H{
+ "unread": stats["unread"],
+ "read": stats["read"],
+ "replied": stats["replied"],
+ "total": stats["total"],
+ },
+ })
+}
+
+func BatchMarkOfflineMessagesAsRead(c *gin.Context) {
+ kefuName, _ := c.Get("kefu_name")
+ idsJson := c.PostForm("ids")
+
+ now := time.Now()
+ var updateCount int64
+
+ if idsJson == "" {
+ result := models.DB.Model(&models.OfflineMessage{}).
+ Where("kefu_id = ? AND status = ?", kefuName.(string), models.OfflineMessageStatusUnread).
+ Updates(map[string]interface{}{
+ "status": models.OfflineMessageStatusRead,
+ "updated_at": now,
+ })
+ updateCount = result.RowsAffected
+ } else {
+ var ids []uint
+ err := json.Unmarshal([]byte(idsJson), &ids)
+ if err != nil {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "无效的ID格式",
+ })
+ return
+ }
+
+ if len(ids) == 0 {
+ c.JSON(200, gin.H{
+ "code": 400,
+ "msg": "没有有效的留言ID",
+ })
+ return
+ }
+
+ for _, id := range ids {
+ msg := models.FindOfflineMessageById(id)
+ if msg.ID == 0 || msg.KefuId != kefuName.(string) {
+ c.JSON(200, gin.H{
+ "code": 403,
+ "msg": fmt.Sprintf("无权操作留言ID: %d", id),
+ })
+ return
+ }
+ }
+
+ err = models.BatchMarkAsRead(ids)
+ if err != nil {
+ c.JSON(200, gin.H{
+ "code": 500,
+ "msg": "批量标记失败",
+ })
+ return
+ }
+ updateCount = int64(len(ids))
+ }
+
+ c.JSON(200, gin.H{
+ "code": 200,
+ "msg": fmt.Sprintf("已标记 %d 条消息为已读", updateCount),
+ "result": gin.H{
+ "count": updateCount,
+ },
+ })
+}
+
+
+func SendOfflineMessageNotice(kefuName, visitorName, content string) {
+ smtp := models.FindConfig("NoticeEmailSmtp")
+ email := models.FindConfig("NoticeEmailAddress")
+ password := models.FindConfig("NoticeEmailPassword")
+ if smtp == "" || email == "" || password == "" {
+ return
+ }
+ subject := fmt.Sprintf("[离线留言] %s 发来新消息", visitorName)
+ body := fmt.Sprintf("访客: %s\n留言内容: %s", visitorName, content)
+ err := tools.SendSmtp(smtp, email, password, []string{email}, subject, body)
+ if err != nil {
+ log.Println("SendOfflineMessageNotice error:", err)
+ }
+}
+
+func SendOfflineReplyNotice(visitorName, visitorEmail, replyContent string) {
+ smtp := models.FindConfig("NoticeEmailSmtp")
+ email := models.FindConfig("NoticeEmailAddress")
+ password := models.FindConfig("NoticeEmailPassword")
+ if smtp == "" || email == "" || password == "" {
+ return
+ }
+ subject := "[留言回复] 客服已回复您的留言"
+ body := fmt.Sprintf("尊敬的 %s:\n\n客服已回复您的留言:\n%s", visitorName, replyContent)
+ err := tools.SendSmtp(smtp, email, password, []string{visitorEmail}, subject, body)
+ if err != nil {
+ log.Println("SendOfflineReplyNotice error:", err)
+ }
+}
diff --git a/goflylivechat.exe b/goflylivechat.exe
new file mode 100644
index 0000000..ecb8d45
Binary files /dev/null and b/goflylivechat.exe differ
diff --git a/import.sql b/import.sql
index 6d4cb54..7545b94 100644
--- a/import.sql
+++ b/import.sql
@@ -110,4 +110,31 @@ CREATE TABLE `reply_item` (
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `group_id` (`group_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8;
\ No newline at end of file
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+DROP TABLE IF EXISTS `offline_message`;
+CREATE TABLE `offline_message` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `kefu_id` varchar(100) NOT NULL DEFAULT '' COMMENT '客服ID',
+ `visitor_id` varchar(100) NOT NULL DEFAULT '' COMMENT '访客ID',
+ `visitor_name` varchar(100) NOT NULL DEFAULT '' COMMENT '访客姓名',
+ `visitor_email` varchar(100) NOT NULL DEFAULT '' COMMENT '访客邮箱',
+ `visitor_phone` varchar(50) NOT NULL DEFAULT '' COMMENT '访客电话',
+ `subject` varchar(255) NOT NULL DEFAULT '' COMMENT '留言主题',
+ `content` text NOT NULL COMMENT '留言内容',
+ `status` enum('unread','read','replied') NOT NULL DEFAULT 'unread' COMMENT '状态:未读/已读/已回复',
+ `reply_content` text COMMENT '回复内容',
+ `reply_time` timestamp NULL DEFAULT NULL COMMENT '回复时间',
+ `replied_by` varchar(100) NOT NULL DEFAULT '' COMMENT '回复人',
+ `source_ip` varchar(50) NOT NULL DEFAULT '' COMMENT '来源IP',
+ `refer` varchar(500) NOT NULL DEFAULT '' COMMENT '来源页面',
+ `extra` varchar(2048) NOT NULL DEFAULT '' COMMENT '额外信息',
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `deleted_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `kefu_id` (`kefu_id`),
+ KEY `visitor_id` (`visitor_id`),
+ KEY `status` (`status`),
+ KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='离线留言表';
\ No newline at end of file
diff --git a/models/offline_message.go b/models/offline_message.go
new file mode 100644
index 0000000..9fcb0f6
--- /dev/null
+++ b/models/offline_message.go
@@ -0,0 +1,232 @@
+package models
+
+import (
+ "time"
+)
+
+type OfflineMessageStatus string
+
+const (
+ OfflineMessageStatusUnread OfflineMessageStatus = "unread"
+ OfflineMessageStatusRead OfflineMessageStatus = "read"
+ OfflineMessageStatusReplied OfflineMessageStatus = "replied"
+)
+
+type OfflineMessage struct {
+ Model
+ KefuId string `json:"kefu_id"`
+ VisitorId string `json:"visitor_id"`
+ VisitorName string `json:"visitor_name"`
+ VisitorEmail string `json:"visitor_email"`
+ VisitorPhone string `json:"visitor_phone"`
+ Subject string `json:"subject"`
+ Content string `json:"content"`
+ Status OfflineMessageStatus `json:"status"`
+ ReplyContent string `json:"reply_content"`
+ ReplyTime *time.Time `json:"reply_time"`
+ RepliedBy string `json:"replied_by"`
+ SourceIp string `json:"source_ip"`
+ Refer string `json:"refer"`
+ Extra string `json:"extra"`
+}
+
+type OfflineMessageDetail struct {
+ OfflineMessage
+ VisitorAvator string `json:"visitor_avator"`
+ KefuName string `json:"kefu_name"`
+ KefuAvator string `json:"kefu_avator"`
+ CreateTime string `json:"create_time"`
+}
+
+func CreateOfflineMessage(kefuId, visitorId, visitorName, visitorEmail, visitorPhone, subject, content, sourceIp, refer, extra string) uint {
+ msg := &OfflineMessage{
+ KefuId: kefuId,
+ VisitorId: visitorId,
+ VisitorName: visitorName,
+ VisitorEmail: visitorEmail,
+ VisitorPhone: visitorPhone,
+ Subject: subject,
+ Content: content,
+ Status: OfflineMessageStatusUnread,
+ SourceIp: sourceIp,
+ Refer: refer,
+ Extra: extra,
+ }
+ msg.UpdatedAt = time.Now()
+ DB.Create(msg)
+ return msg.ID
+}
+
+func FindOfflineMessageById(id uint) OfflineMessage {
+ var msg OfflineMessage
+ DB.Where("id = ?", id).First(&msg)
+ return msg
+}
+
+func FindOfflineMessageDetailById(id uint) OfflineMessageDetail {
+ var msg OfflineMessageDetail
+ DB.Table("offline_message").
+ Select("offline_message.*, visitor.avator as visitor_avator, user.nickname as kefu_name, user.avator as kefu_avator").
+ Joins("left join user on offline_message.kefu_id = user.name").
+ Joins("left join visitor on offline_message.visitor_id = visitor.visitor_id").
+ Where("offline_message.id = ?", id).
+ First(&msg)
+ msg.CreateTime = msg.CreatedAt.Format("2006-01-02 15:04:05")
+ return msg
+}
+
+func UpdateOfflineMessageStatus(id uint, status OfflineMessageStatus) {
+ msg := &OfflineMessage{
+ Status: status,
+ }
+ msg.UpdatedAt = time.Now()
+ DB.Model(&OfflineMessage{}).Where("id = ?", id).Update(msg)
+}
+
+func UpdateOfflineMessageReply(id uint, replyContent string, repliedBy string) {
+ now := time.Now()
+ msg := &OfflineMessage{
+ Status: OfflineMessageStatusReplied,
+ ReplyContent: replyContent,
+ ReplyTime: &now,
+ RepliedBy: repliedBy,
+ }
+ msg.UpdatedAt = now
+ DB.Model(&OfflineMessage{}).Where("id = ?", id).Updates(msg)
+}
+
+func DeleteOfflineMessage(id uint) {
+ DB.Where("id = ?", id).Delete(OfflineMessage{})
+}
+
+func CountOfflineMessagesByKefuId(kefuId string, status OfflineMessageStatus) uint {
+ var count uint
+ query := DB.Model(&OfflineMessage{}).Where("kefu_id = ?", kefuId)
+ if status != "" {
+ query = query.Where("status = ?", status)
+ }
+ query.Count(&count)
+ return count
+}
+
+func CountOfflineMessages(query interface{}, args ...interface{}) uint {
+ var count uint
+ DB.Model(&OfflineMessage{}).Where(query, args...).Count(&count)
+ return count
+}
+
+func FindOfflineMessagesByPage(page uint, pagesize uint, query interface{}, args ...interface{}) []*OfflineMessageDetail {
+ offset := (page - 1) * pagesize
+ if offset < 0 {
+ offset = 0
+ }
+ var messages []*OfflineMessageDetail
+ baseQuery := DB.Table("offline_message").
+ Select("offline_message.*, visitor.avator as visitor_avator, user.nickname as kefu_name, user.avator as kefu_avator").
+ Joins("left join user on offline_message.kefu_id = user.name").
+ Joins("left join visitor on offline_message.visitor_id = visitor.visitor_id")
+
+ if query != nil {
+ baseQuery = baseQuery.Where(query, args...)
+ }
+
+ baseQuery.Offset(offset).Limit(pagesize).Order("offline_message.created_at desc").Find(&messages)
+
+ for _, msg := range messages {
+ msg.CreateTime = msg.CreatedAt.Format("2006-01-02 15:04:05")
+ }
+ return messages
+}
+
+func FindOfflineMessagesByStatus(kefuId string, status OfflineMessageStatus) []OfflineMessage {
+ var messages []OfflineMessage
+ query := DB.Where("kefu_id = ?", kefuId)
+ if status != "" {
+ query = query.Where("status = ?", status)
+ }
+ query.Order("created_at desc").Find(&messages)
+ return messages
+}
+
+func FindUnreadOfflineMessagesByKefuId(kefuId string) []OfflineMessage {
+ return FindOfflineMessagesByStatus(kefuId, OfflineMessageStatusUnread)
+}
+
+func MarkOfflineMessageAsRead(id uint) {
+ UpdateOfflineMessageStatus(id, OfflineMessageStatusRead)
+}
+
+func IsValidOfflineMessageStatus(status string) bool {
+ s := OfflineMessageStatus(status)
+ return s == OfflineMessageStatusUnread ||
+ s == OfflineMessageStatusRead ||
+ s == OfflineMessageStatusReplied
+}
+
+func (s OfflineMessageStatus) String() string {
+ return string(s)
+}
+
+func GetOfflineMessageStatusText(status OfflineMessageStatus) string {
+ switch status {
+ case OfflineMessageStatusUnread:
+ return "未读"
+ case OfflineMessageStatusRead:
+ return "已读"
+ case OfflineMessageStatusReplied:
+ return "已回复"
+ default:
+ return "未知"
+ }
+}
+
+func BatchUpdateOfflineMessageStatus(ids []uint, status OfflineMessageStatus) error {
+ if len(ids) == 0 {
+ return nil
+ }
+ now := time.Now()
+ return DB.Model(&OfflineMessage{}).
+ Where("id IN (?)", ids).
+ Updates(map[string]interface{}{
+ "status": status,
+ "updated_at": now,
+ }).Error
+}
+
+func BatchMarkAsRead(ids []uint) error {
+ return BatchUpdateOfflineMessageStatus(ids, OfflineMessageStatusRead)
+}
+
+func GetOfflineMessageStatsByKefuId(kefuId string) map[string]int64 {
+ stats := make(map[string]int64)
+
+ var unreadCount int64
+ DB.Model(&OfflineMessage{}).Where("kefu_id = ? AND status = ?", kefuId, OfflineMessageStatusUnread).Count(&unreadCount)
+ stats["unread"] = unreadCount
+
+ var readCount int64
+ DB.Model(&OfflineMessage{}).Where("kefu_id = ? AND status = ?", kefuId, OfflineMessageStatusRead).Count(&readCount)
+ stats["read"] = readCount
+
+ var repliedCount int64
+ DB.Model(&OfflineMessage{}).Where("kefu_id = ? AND status = ?", kefuId, OfflineMessageStatusReplied).Count(&repliedCount)
+ stats["replied"] = repliedCount
+
+ var totalCount int64
+ DB.Model(&OfflineMessage{}).Where("kefu_id = ?", kefuId).Count(&totalCount)
+ stats["total"] = totalCount
+
+ return stats
+}
+
+func FindOfflineMessagesByVisitorId(visitorId string) []OfflineMessage {
+ var messages []OfflineMessage
+ DB.Where("visitor_id = ?", visitorId).Order("created_at desc").Find(&messages)
+ return messages
+}
+
+func FindLastOfflineMessageByVisitorId(visitorId string) OfflineMessage {
+ var msg OfflineMessage
+ DB.Where("visitor_id = ?", visitorId).Order("created_at desc").First(&msg)
+ return msg
+}
diff --git a/router/api.go b/router/api.go
index 36ba4cc..d151cb7 100644
--- a/router/api.go
+++ b/router/api.go
@@ -91,6 +91,21 @@ func InitApiRouter(engine *gin.Engine) {
kefuGroup.GET("/chartStatistics", controller.GetChartStatistic)
kefuGroup.POST("/message", controller.SendKefuMessage)
}
+ //离线留言接口 - 访客端
+ engine.POST("/offline_message", middleware.Ipblack, controller.SubmitOfflineMessage)
+
+ //离线留言接口 - 客服端(需要认证)
+ offlineMessageGroup := engine.Group("/offline_messages")
+ offlineMessageGroup.Use(middleware.JwtApiMiddleware)
+ {
+ offlineMessageGroup.GET("", controller.GetOfflineMessages)
+ offlineMessageGroup.GET("/stats", controller.GetOfflineMessageStats)
+ offlineMessageGroup.GET("/:id", controller.GetOfflineMessageDetail)
+ offlineMessageGroup.POST("/reply", controller.ReplyOfflineMessage)
+ offlineMessageGroup.POST("/:id/status", controller.UpdateOfflineMessageStatus)
+ offlineMessageGroup.DELETE("/:id", controller.DeleteOfflineMessage)
+ offlineMessageGroup.POST("/mark_read", controller.BatchMarkOfflineMessagesAsRead)
+ }
//微信接口
engine.GET("/micro_program", middleware.JwtApiMiddleware, controller.GetCheckWeixinSign)
}
diff --git a/static/css/common.css b/static/css/common.css
index 373c74c..d9f0280 100644
--- a/static/css/common.css
+++ b/static/css/common.css
@@ -583,4 +583,82 @@ a{color: #07a9fe;text-decoration: none;}
/* 定义滚动条滑块在 hover 状态下的样式 */
::-webkit-scrollbar-thumb:hover {
background-color: #999;
+}
+
+/* 离线留言表单样式 */
+.offlineMessageContainer {
+ height: 100%;
+ padding: 20px;
+ background: #f5f5f5;
+ overflow-y: auto;
+}
+
+.offlineMessageTitle {
+ text-align: center;
+ margin-bottom: 20px;
+}
+
+.offlineMessageTitle h3 {
+ color: #303133;
+ font-size: 18px;
+ margin-bottom: 10px;
+}
+
+.offlineMessageTitle h3 i {
+ margin-right: 8px;
+ color: #409EFF;
+}
+
+.offlineMessageTitle p {
+ color: #909399;
+ font-size: 14px;
+}
+
+.offlineMessageForm {
+ max-width: 500px;
+ margin: 0 auto;
+ background: #fff;
+ padding: 30px;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.offlineMessageForm .el-form-item {
+ margin-bottom: 20px;
+}
+
+.offlineMessageForm .el-form-item__label {
+ color: #606266;
+ font-weight: 500;
+}
+
+.offlineMessageForm .el-textarea__inner {
+ resize: none;
+}
+
+.offlineMessageSuccess {
+ max-width: 500px;
+ margin: 20px auto 0;
+}
+
+.offlineMessageSuccess .el-alert {
+ border-radius: 8px;
+}
+
+@media screen and (max-width: 600px) {
+ .offlineMessageContainer {
+ padding: 15px;
+ }
+
+ .offlineMessageForm {
+ padding: 20px;
+ }
+
+ .offlineMessageTitle h3 {
+ font-size: 16px;
+ }
+
+ .offlineMessageTitle p {
+ font-size: 12px;
+ }
}
\ No newline at end of file
diff --git a/static/templates/chat_main.html b/static/templates/chat_main.html
index 755a6a6..301a970 100644
--- a/static/templates/chat_main.html
+++ b/static/templates/chat_main.html
@@ -75,6 +75,52 @@
:total="visitorCount">
+
我们的客服目前不在线,请填写以下表单,客服上线后会尽快回复您。
+