Files
kefu_cloud/server/internal/handler/channel.go
T
yml2213 d4ba9e7a56 全局重命名:kefu_sys / kefu-sys 改为 kefu_cloud / kefu-cloud
- Go module 与全部 import 路径
- 数据库默认库名、Docker 服务/卷命名与部署文档
2026-07-15 15:23:10 +08:00

198 lines
5.4 KiB
Go

package handler
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"regexp"
"strings"
"github.com/gin-gonic/gin"
"kefu-cloud/server/internal/middleware"
"kefu-cloud/server/internal/model"
)
type ChannelHandler struct{}
func NewChannelHandler() *ChannelHandler { return &ChannelHandler{} }
var scriptIDPattern = regexp.MustCompile(`data-id="([A-Za-z0-9_-]+)"`)
type channelView struct {
ID uint `json:"id"`
TenantID uint `json:"tenant_id"`
Type string `json:"type"`
Name string `json:"name"`
Status string `json:"status"`
Config string `json:"config"`
ScriptCode string `json:"script_code"`
ChannelKey string `json:"channel_key"`
}
func extractChannelKey(script string) string {
m := scriptIDPattern.FindStringSubmatch(script)
if len(m) == 2 {
return m[1]
}
return ""
}
func buildWebScript(channelKey string) string {
return fmt.Sprintf(`<script src="/widget.js" data-id="%s"></script>`, channelKey)
}
func newChannelKey(prefix string) (string, error) {
buf := make([]byte, 4)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return fmt.Sprintf("%s_%s", prefix, hex.EncodeToString(buf)), nil
}
func toChannelView(ch model.Channel) channelView {
return channelView{
ID: ch.ID, TenantID: ch.TenantID, Type: ch.Type, Name: ch.Name,
Status: ch.Status, Config: ch.Config, ScriptCode: ch.ScriptCode,
ChannelKey: extractChannelKey(ch.ScriptCode),
}
}
func requireTenantAdmin(c *gin.Context) bool {
if middleware.HasAnyRole(c, "admin") {
return true
}
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "仅租户管理员可操作"})
return false
}
func (h *ChannelHandler) List(c *gin.Context) {
tenantID := middleware.GetTenantID(c)
var channels []model.Channel
if err := model.DB.Where("tenant_id = ?", tenantID).Order("id asc").Find(&channels).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询渠道失败"})
return
}
views := make([]channelView, 0, len(channels))
for _, ch := range channels {
views = append(views, toChannelView(ch))
}
middleware.JSON(c, views)
}
type updateChannelReq struct {
Name *string `json:"name"`
Status *string `json:"status"`
}
func (h *ChannelHandler) Update(c *gin.Context) {
if !requireTenantAdmin(c) {
return
}
tenantID := middleware.GetTenantID(c)
id := c.Param("id")
var channel model.Channel
if err := model.DB.Where("id = ? AND tenant_id = ?", id, tenantID).First(&channel).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "渠道不存在"})
return
}
var req updateChannelReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
return
}
updates := map[string]interface{}{}
if req.Name != nil {
name := strings.TrimSpace(*req.Name)
if name == "" || len([]rune(name)) > 50 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "渠道名称无效"})
return
}
updates["name"] = name
}
if req.Status != nil {
status := strings.TrimSpace(*req.Status)
if status != "enabled" && status != "disabled" {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "状态仅支持 enabled/disabled"})
return
}
updates["status"] = status
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "没有可更新字段"})
return
}
if err := model.DB.Model(&channel).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "更新失败"})
return
}
model.DB.First(&channel, channel.ID)
middleware.JSON(c, toChannelView(channel))
}
type createChannelReq struct {
Type string `json:"type" binding:"required"`
Name string `json:"name"`
}
func (h *ChannelHandler) Create(c *gin.Context) {
if !requireTenantAdmin(c) {
return
}
var req createChannelReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
return
}
channelType := strings.TrimSpace(req.Type)
allowed := map[string]string{
"web": "网页聊天", "wechat": "微信公众号", "app": "APP 内嵌",
"phone": "电话客服", "email": "邮件工单",
}
defaultName, ok := allowed[channelType]
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "不支持的渠道类型"})
return
}
tenantID := middleware.GetTenantID(c)
var exists int64
model.DB.Model(&model.Channel{}).Where("tenant_id = ? AND type = ?", tenantID, channelType).Count(&exists)
if exists > 0 {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "该类型渠道已存在"})
return
}
name := strings.TrimSpace(req.Name)
if name == "" {
name = defaultName
}
prefix := map[string]string{"web": "WK", "wechat": "WX", "app": "AP", "phone": "PH", "email": "EM"}[channelType]
key, err := newChannelKey(prefix)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "生成渠道标识失败"})
return
}
channel := model.Channel{
TenantID: tenantID,
Type: channelType,
Name: name,
Status: "disabled",
}
if channelType == "web" {
channel.Status = "enabled"
channel.ScriptCode = buildWebScript(key)
} else {
channel.ScriptCode = fmt.Sprintf(`data-id="%s"`, key)
}
if err := model.DB.Create(&channel).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "创建失败"})
return
}
middleware.JSON(c, toChannelView(channel))
}