Refactor API routes and controllers for improved structure and clarity

- Updated API route structure to remove versioning and consolidate endpoints.
- Refactored routing in `server.go` to use a unified `/api` prefix for all controllers.
- Added new `ChannelController`, `ConversationController`, and `MessageController` to handle specific API functionalities.
- Removed deprecated `OpenImContextMiddleware` and related logic.
- Enhanced channel validation logic in `ChannelService` and `ChannelRepository`.
- Updated frontend API calls to align with new endpoint structure.
This commit is contained in:
mlogclub
2026-04-25 10:27:43 +08:00
parent c1ab26ed71
commit 96168dc6fb
13 changed files with 98 additions and 132 deletions
+4 -6
View File
@@ -286,10 +286,9 @@ func BuildXxx(item *models.Xxx) *response.Xxx {
### 8.2 路径分层
- `/api/dashboard/*`:业务后台接口,默认归属
- `/api/admin/*`:平台总后台接口
- `/api/open/*`:开放接口IM 优先使用 `/api/open/im/*`
- `/api/auth/*`:认证接口,可未登录访问
- `/api/dashboard/*`:业务后台接口
- `/api/third/*`:第三方平台调用接口
- `/api/*`:开放接口
禁止新增 `/api/v1` 这类版本前缀。
@@ -305,8 +304,7 @@ func BuildXxx(item *models.Xxx) *response.Xxx {
### 8.4 路由注册
- 业务后台统一在 `internal/bootstrap/server.go` 中通过 `mvc.Configure(app.Party("/api/dashboard"), ...)` 注册
- 平台接口统一通过 `mvc.Configure(app.Party("/api/admin"), ...)` 注册
- 开放接口按领域归档,如 `mvc.Configure(app.Party("/api/open/im"), ...)`
- 开放接口按领域归档,如 `mvc.Configure(app.Party("/api"), ...)`
- 在分组内部通过 `m.Party("/xxx").Handle(...)` 挂载资源
- 不要为每个资源单独再写一层顶级 `mvc.Configure(app.Party("/api/dashboard/xxx"), ...)`
- 认证与鉴权中间件优先挂在 `/api/dashboard``/api/admin` 这一层
+12 -9
View File
@@ -10,7 +10,6 @@ import (
_ "cs-agent/internal/ai/runtime"
"cs-agent/internal/controllers/api"
"cs-agent/internal/controllers/dashboard"
"cs-agent/internal/controllers/open"
"cs-agent/internal/controllers/third"
"cs-agent/internal/middleware"
"cs-agent/internal/pkg/config"
@@ -95,10 +94,20 @@ func addRouter(app *iris.Application) {
app.Get("/api/dashboard/ws", middleware.AuthMiddleware, middleware.DashboardWsMiddleware)
app.Get("/api/open/im/ws", middleware.OpenImWsMiddleware)
mvc.Configure(app.Party("/api/auth"), func(m *mvc.Application) {
m.Handle(new(api.AuthController))
mvc.Configure(app.Party("/api"), func(m *mvc.Application) {
m.Party("/auth").Handle(new(api.AuthController))
m.Party("/channel", middleware.ChannelContextMiddleware).Handle(new(api.ChannelController))
m.Party("/conversation", middleware.ChannelContextMiddleware).Handle(new(api.ConversationController))
m.Party("/message", middleware.ChannelContextMiddleware).Handle(new(api.MessageController))
})
// mvc.Configure(app.Party("/api/open/im", middleware.ChannelContextMiddleware), func(m *mvc.Application) {
// m.Party("/widget").Handle(new(api.ImWidgetController))
// m.Party("/conversation").Handle(new(api.ConversationController))
// m.Party("/message").Handle(new(api.MessageController))
// })
mvc.Configure(app.Party("/api/dashboard", middleware.AuthMiddleware), func(m *mvc.Application) {
m.Party("/dashboard").Handle(new(dashboard.DashboardController))
m.Party("/user").Handle(new(dashboard.UserController))
@@ -131,12 +140,6 @@ func addRouter(app *iris.Application) {
m.Party("/mcp").Handle(new(dashboard.MCPController))
})
mvc.Configure(app.Party("/api/open/im", middleware.OpenImContextMiddleware), func(m *mvc.Application) {
m.Party("/widget").Handle(new(open.ImWidgetController))
m.Party("/conversation").Handle(new(open.ImConversationController))
m.Party("/message").Handle(new(open.ImMessageController))
})
mvc.Configure(app.Party("/api/third"), func(m *mvc.Application) {
m.Party("/wechat").Handle(new(third.WechatController))
})
@@ -1,20 +1,19 @@
package open
package api
import (
"cs-agent/internal/pkg/dto/response"
"cs-agent/internal/pkg/irisx"
"cs-agent/internal/services"
"github.com/kataras/iris/v12"
"github.com/mlogclub/simple/web"
)
type ImWidgetController struct {
type ChannelController struct {
Ctx iris.Context
}
func (c *ImWidgetController) AnyConfig() *web.JsonResult {
channel := irisx.GetChannel(c.Ctx)
func (c *ChannelController) AnyConfig() *web.JsonResult {
channel := services.ChannelService.GetEnabledChannel(c.Ctx)
if channel == nil {
return web.JsonErrorMsg("接入渠道未初始化")
}
@@ -1,4 +1,4 @@
package open
package api
import (
"cs-agent/internal/builders"
@@ -12,12 +12,12 @@ import (
"github.com/mlogclub/simple/web/params"
)
type ImConversationController struct {
type ConversationController struct {
Ctx iris.Context
}
func (c *ImConversationController) GetBy(id int64) *web.JsonResult {
if irisx.GetChannel(c.Ctx) == nil {
func (c *ConversationController) GetBy(id int64) *web.JsonResult {
if services.ChannelService.GetEnabledChannel(c.Ctx) == nil {
return web.JsonErrorMsg("接入渠道未初始化")
}
external := irisx.GetExternalInfo(c.Ctx)
@@ -40,8 +40,8 @@ func (c *ImConversationController) GetBy(id int64) *web.JsonResult {
return web.JsonData(detail)
}
func (c *ImConversationController) PostCreate_or_match() *web.JsonResult {
channel := irisx.GetChannel(c.Ctx)
func (c *ConversationController) PostCreate_or_match() *web.JsonResult {
channel := services.ChannelService.GetEnabledChannel(c.Ctx)
if channel == nil {
return web.JsonErrorMsg("接入渠道未初始化")
}
@@ -57,8 +57,8 @@ func (c *ImConversationController) PostCreate_or_match() *web.JsonResult {
return web.JsonData(builders.BuildConversation(item))
}
func (c *ImConversationController) PostClose() *web.JsonResult {
if irisx.GetChannel(c.Ctx) == nil {
func (c *ConversationController) PostClose() *web.JsonResult {
if services.ChannelService.GetEnabledChannel(c.Ctx) == nil {
return web.JsonErrorMsg("接入渠道未初始化")
}
external := irisx.GetExternalInfo(c.Ctx)
@@ -1,4 +1,4 @@
package open
package api
import (
"cs-agent/internal/builders"
@@ -15,12 +15,12 @@ import (
"github.com/spf13/cast"
)
type ImMessageController struct {
type MessageController struct {
Ctx iris.Context
}
func (c *ImMessageController) AnyList() *web.JsonResult {
if irisx.GetChannel(c.Ctx) == nil {
func (c *MessageController) AnyList() *web.JsonResult {
if services.ChannelService.GetEnabledChannel(c.Ctx) == nil {
return web.JsonErrorMsg("接入渠道未初始化")
}
external := irisx.GetExternalInfo(c.Ctx)
@@ -53,8 +53,8 @@ func (c *ImMessageController) AnyList() *web.JsonResult {
return web.JsonCursorData(results, cast.ToString(nextCursor), hasMore)
}
func (c *ImMessageController) PostSend() *web.JsonResult {
if irisx.GetChannel(c.Ctx) == nil {
func (c *MessageController) PostSend() *web.JsonResult {
if services.ChannelService.GetEnabledChannel(c.Ctx) == nil {
return web.JsonErrorMsg("接入渠道未初始化")
}
external := irisx.GetExternalInfo(c.Ctx)
@@ -74,8 +74,8 @@ func (c *ImMessageController) PostSend() *web.JsonResult {
return web.JsonData(builders.BuildMessage(item))
}
func (c *ImMessageController) PostRead() *web.JsonResult {
if irisx.GetChannel(c.Ctx) == nil {
func (c *MessageController) PostRead() *web.JsonResult {
if services.ChannelService.GetEnabledChannel(c.Ctx) == nil {
return web.JsonErrorMsg("接入渠道未初始化")
}
external := irisx.GetExternalInfo(c.Ctx)
@@ -93,8 +93,8 @@ func (c *ImMessageController) PostRead() *web.JsonResult {
return web.JsonSuccess()
}
func (c *ImMessageController) PostUpload_image() *web.JsonResult {
if irisx.GetChannel(c.Ctx) == nil {
func (c *MessageController) PostUpload_image() *web.JsonResult {
if services.ChannelService.GetEnabledChannel(c.Ctx) == nil {
return web.JsonErrorMsg("接入渠道未初始化")
}
external := irisx.GetExternalInfo(c.Ctx)
@@ -138,8 +138,8 @@ func (c *ImMessageController) PostUpload_image() *web.JsonResult {
return web.JsonData(builders.BuildAsset(item))
}
func (c *ImMessageController) PostUpload_attachment() *web.JsonResult {
if irisx.GetChannel(c.Ctx) == nil {
func (c *MessageController) PostUpload_attachment() *web.JsonResult {
if services.ChannelService.GetEnabledChannel(c.Ctx) == nil {
return web.JsonErrorMsg("接入渠道未初始化")
}
external := irisx.GetExternalInfo(c.Ctx)
+2 -27
View File
@@ -3,38 +3,13 @@ package middleware
import (
"cs-agent/internal/pkg/irisx"
"cs-agent/internal/pkg/openidentity"
"cs-agent/internal/services"
"strings"
"github.com/kataras/iris/v12"
"github.com/mlogclub/simple/web"
)
// OpenImContextMiddleware 校验 X-Channel-Id / channelId 对应启用 web 渠道;除 /api/open/im/widget 外解析并缓存外部访客身份。
func OpenImContextMiddleware(ctx iris.Context) {
channelID := strings.TrimSpace(ctx.GetHeader("X-Channel-Id"))
if channelID == "" {
channelID = strings.TrimSpace(ctx.URLParam("channelId"))
}
if channelID == "" {
ctx.StopExecution()
_ = ctx.JSON(web.JsonErrorMsg("channelId不能为空"))
return
}
channel := services.ChannelService.GetEnabledWebChannelByChannelID(channelID)
if channel == nil {
ctx.StopExecution()
_ = ctx.JSON(web.JsonErrorMsg("接入渠道不存在或已停用"))
return
}
irisx.SetOpenImChannel(ctx, channel)
path := ctx.Path()
if strings.Contains(path, "/open/im/widget") {
ctx.Next()
return
}
// ChannelContextMiddleware 校验 X-Channel-Id / channelId 对应启用 web 渠道
func ChannelContextMiddleware(ctx iris.Context) {
ext, err := openidentity.GetExternalInfo(ctx)
if err != nil {
ctx.StopExecution()
+5 -38
View File
@@ -1,12 +1,9 @@
package middleware
import (
"cs-agent/internal/models"
"cs-agent/internal/pkg/errorsx"
"cs-agent/internal/pkg/openidentity"
"cs-agent/internal/services"
"log/slog"
"strings"
"github.com/kataras/iris/v12"
"github.com/mlogclub/simple/web"
@@ -16,9 +13,7 @@ func DashboardWsMiddleware(ctx iris.Context) {
principal := services.AuthService.GetAuthPrincipal(ctx)
if principal == nil {
if _, err := services.AuthService.Authenticate(ctx); err != nil {
_ = ctx.StopWithJSON(iris.StatusUnauthorized, map[string]any{
"message": err.Error(),
})
_ = ctx.StopWithJSON(iris.StatusUnauthorized, web.JsonError(err))
return
}
principal = services.AuthService.GetAuthPrincipal(ctx)
@@ -31,14 +26,11 @@ func DashboardWsMiddleware(ctx iris.Context) {
}
func OpenImWsMiddleware(ctx iris.Context) {
channel, err := resolveEnabledChannelForWS(ctx)
if err != nil {
_ = ctx.StopWithJSON(iris.StatusBadRequest, map[string]any{
"message": err.Error(),
})
channel := services.ChannelService.GetEnabledChannel(ctx)
if channel == nil {
_ = ctx.StopWithJSON(iris.StatusBadRequest, web.JsonErrorMsg("接入渠道不存在或已停用"))
return
}
// 与 Open IM HTTP 一致:优先站内 AuthPrincipal;否则使用外部访客身份(Header/query,见 openidentity)。
// 二者不应在业务上同时作为「客户身份」使用;本入口在 principal 非空时不再解析 external,避免语义冲突。
principal := services.AuthService.GetAuthPrincipal(ctx)
@@ -46,9 +38,7 @@ func OpenImWsMiddleware(ctx iris.Context) {
if principal == nil {
ext, err := openidentity.GetExternalInfo(ctx)
if err != nil {
_ = ctx.StopWithJSON(iris.StatusUnauthorized, map[string]any{
"message": err.Error(),
})
_ = ctx.StopWithJSON(iris.StatusUnauthorized, web.JsonError(err))
return
}
external = ext
@@ -59,26 +49,3 @@ func OpenImWsMiddleware(ctx iris.Context) {
return
}
}
func resolveEnabledChannelForWS(ctx iris.Context) (*models.Channel, error) {
channel, rsp := requireEnabledChannel(ctx)
if rsp == nil {
return channel, nil
}
return nil, errorsx.InvalidParam("接入渠道不存在或已停用")
}
func requireEnabledChannel(ctx iris.Context) (*models.Channel, *web.JsonResult) {
channelID := strings.TrimSpace(ctx.GetHeader("X-Channel-Id"))
if channelID == "" {
channelID = strings.TrimSpace(ctx.URLParam("channelId"))
}
if channelID == "" {
return nil, web.JsonErrorMsg("channelId不能为空")
}
channel := services.ChannelService.GetEnabledWebChannelByChannelID(channelID)
if channel == nil {
return nil, web.JsonErrorMsg("接入渠道不存在或已停用")
}
return channel, nil
}
+21 -10
View File
@@ -1,10 +1,11 @@
package irisx
import (
"cs-agent/internal/models"
"cs-agent/internal/pkg/openidentity"
"github.com/kataras/iris/v12"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/web/params"
)
const (
@@ -12,16 +13,16 @@ const (
ctxKeyOpenImExternalInfo = "openImExternalInfo"
)
func SetOpenImChannel(ctx iris.Context, channel *models.Channel) {
ctx.Values().Set(ctxKeyOpenImChannel, channel)
}
// func SetOpenImChannel(ctx iris.Context, channel *models.Channel) {
// ctx.Values().Set(ctxKeyOpenImChannel, channel)
// }
// GetChannel 返回 OpenImContextMiddleware 注入的接入渠道(未走中间件时为 nil)。
func GetChannel(ctx iris.Context) *models.Channel {
v := ctx.Values().Get(ctxKeyOpenImChannel)
channel, _ := v.(*models.Channel)
return channel
}
// // GetChannel 返回 OpenImContextMiddleware 注入的接入渠道(未走中间件时为 nil)。
// func GetChannel(ctx iris.Context) *models.Channel {
// v := ctx.Values().Get(ctxKeyOpenImChannel)
// channel, _ := v.(*models.Channel)
// return channel
// }
func SetOpenImExternalInfo(ctx iris.Context, ext *openidentity.ExternalInfo) {
ctx.Values().Set(ctxKeyOpenImExternalInfo, ext)
@@ -33,3 +34,13 @@ func GetExternalInfo(ctx iris.Context) *openidentity.ExternalInfo {
ext, _ := v.(*openidentity.ExternalInfo)
return ext
}
func GetChannelID(ctx iris.Context) string {
if channelID := ctx.GetHeader("X-Channel-ID"); strs.IsNotBlank(channelID) {
return channelID
}
if channelID, _ := params.Get(ctx, "channelId"); strs.IsNotBlank(channelID) {
return channelID
}
return ""
}
+9 -2
View File
@@ -3,6 +3,7 @@ package repositories
import (
"cs-agent/internal/models"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
"github.com/mlogclub/simple/web/params"
"gorm.io/gorm"
@@ -62,12 +63,12 @@ func (r *channelRepository) FindPageByCnd(db *gorm.DB, cnd *sqls.Cnd) (list []mo
return
}
func (r *channelRepository) FindBySql(db *gorm.DB, sqlStr string, paramArr... interface{}) (list []models.Channel) {
func (r *channelRepository) FindBySql(db *gorm.DB, sqlStr string, paramArr ...interface{}) (list []models.Channel) {
db.Raw(sqlStr, paramArr...).Scan(&list)
return
}
func (r *channelRepository) CountBySql(db *gorm.DB, sqlStr string, paramArr... interface{}) (count int64) {
func (r *channelRepository) CountBySql(db *gorm.DB, sqlStr string, paramArr ...interface{}) (count int64) {
db.Raw(sqlStr, paramArr...).Count(&count)
return
}
@@ -100,3 +101,9 @@ func (r *channelRepository) Delete(db *gorm.DB, id int64) {
db.Delete(&models.Channel{}, "id = ?", id)
}
func (r *channelRepository) GetByChannelID(db *gorm.DB, channelID string) *models.Channel {
if strs.IsBlank(channelID) {
return nil
}
return r.FindOne(db, sqls.NewCnd().Where("channel_id = ?", channelID))
}
+10 -4
View File
@@ -6,12 +6,14 @@ import (
"cs-agent/internal/pkg/dto/request"
"cs-agent/internal/pkg/enums"
"cs-agent/internal/pkg/errorsx"
"cs-agent/internal/pkg/irisx"
"cs-agent/internal/pkg/utils"
"cs-agent/internal/repositories"
"encoding/json"
"strings"
"time"
"github.com/kataras/iris/v12"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
"github.com/mlogclub/simple/web/params"
@@ -217,12 +219,16 @@ func (s *channelService) GetEnabledWxWorkKFChannelByOpenKfID(openKfID string) *m
return nil
}
func (s *channelService) GetEnabledWebChannelByChannelID(channelID string) *models.Channel {
channelID = strings.TrimSpace(channelID)
if channelID == "" {
func (s *channelService) GetEnabledChannel(ctx iris.Context) *models.Channel {
channelID := irisx.GetChannelID(ctx)
channel := repositories.ChannelRepository.GetByChannelID(sqls.DB(), channelID)
if channel == nil {
return nil
}
return s.Take("channel_type = ? AND channel_id = ? AND status = ?", enums.ChannelTypeWeb, channelID, enums.StatusOk)
if channel.Status != enums.StatusOk {
return nil
}
return channel
}
func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRequest) (*models.Channel, error) {
+9 -9
View File
@@ -196,7 +196,7 @@ function toQueryString(query?: Record<string, string | number | undefined>) {
}
export function fetchImConversationDetail(id: number) {
return request<ImConversationDetail>(`/api/open/im/conversation/${id}`, {
return request<ImConversationDetail>(`/api/conversation/${id}`, {
...createRequestOptions(),
})
}
@@ -205,21 +205,21 @@ export function fetchImMessages(
query?: Record<string, string | number | undefined>
) {
return request<PageResult<ImMessage>>(
`/api/open/im/message/list${toQueryString(query)}`,
`/api/message/list${toQueryString(query)}`,
createRequestOptions()
)
}
/** 外部身份仅通过 createImHeaders()X-External-*)传递,无 JSON body */
export function createOrMatchImConversation() {
return request<ImConversation>("/api/open/im/conversation/create_or_match", {
return request<ImConversation>("/api/conversation/create_or_match", {
...createRequestOptions({ method: "POST" }),
})
}
export function fetchImWidgetConfig() {
return request<ImWidgetConfig>(
`/api/open/im/widget/config${toQueryString({
`/api/channel/config${toQueryString({
channelId: getRuntimeImConfig().channelId,
})}`,
createRequestOptions()
@@ -227,7 +227,7 @@ export function fetchImWidgetConfig() {
}
export function closeImConversation(conversationId: number) {
return request<void>("/api/open/im/conversation/close", {
return request<void>("/api/conversation/close", {
...createRequestOptions({
method: "POST",
body: JSON.stringify({ conversationId }),
@@ -242,7 +242,7 @@ export function sendImMessage(payload: {
payload?: string
clientMsgId?: string
}) {
return request<ImMessage>("/api/open/im/message/send", {
return request<ImMessage>("/api/message/send", {
...createRequestOptions({
method: "POST",
body: JSON.stringify(payload),
@@ -251,7 +251,7 @@ export function sendImMessage(payload: {
}
export function markImMessageRead(conversationId: number, messageId = 0) {
return request<void>("/api/open/im/message/read", {
return request<void>("/api/message/read", {
...createRequestOptions({
method: "POST",
body: JSON.stringify({ conversationId, messageId }),
@@ -263,7 +263,7 @@ export function uploadImImage(conversationId: number, file: File) {
const formData = new FormData()
formData.set("conversationId", String(conversationId))
formData.set("file", file)
return request<ImAsset>("/api/open/im/message/upload_image", {
return request<ImAsset>("/api/message/upload_image", {
...createRequestOptions({
method: "POST",
body: formData,
@@ -275,7 +275,7 @@ export function uploadImAttachment(conversationId: number, file: File) {
const formData = new FormData()
formData.set("conversationId", String(conversationId))
formData.set("file", file)
return request<ImAsset>("/api/open/im/message/upload_attachment", {
return request<ImAsset>("/api/message/upload_attachment", {
...createRequestOptions({
method: "POST",
body: formData,
+1 -1
View File
@@ -100,7 +100,7 @@
if (!baseUrl || !config.channelId || typeof fetch !== "function") {
return Promise.resolve(config);
}
var url = baseUrl + "/api/open/im/widget/config?channelId=" + encodeURIComponent(config.channelId);
var url = baseUrl + "/api/channel/config?channelId=" + encodeURIComponent(config.channelId);
return fetch(url, {
method: "GET",
cache: "no-store",
File diff suppressed because one or more lines are too long