Commit 671cf8df authored by yuguo's avatar yuguo

fix

parent 9b315e10
...@@ -41,7 +41,8 @@ ...@@ -41,7 +41,8 @@
"Bash(claude mcp:*)", "Bash(claude mcp:*)",
"WebSearch", "WebSearch",
"Bash(go env:*)", "Bash(go env:*)",
"Bash(go install:*)" "Bash(go install:*)",
"Bash(where psql:*)"
] ]
} }
} }
...@@ -133,8 +133,6 @@ func main() { ...@@ -133,8 +133,6 @@ func main() {
&model.UserRole{}, &model.UserRole{},
&model.Menu{}, &model.Menu{},
&model.RoleMenu{}, &model.RoleMenu{},
// 路由注册表(AI 导航工具)
&model.RouteEntry{},
} }
failCount := 0 failCount := 0
for _, m := range allModels { for _, m := range allModels {
...@@ -175,21 +173,16 @@ func main() { ...@@ -175,21 +173,16 @@ func main() {
ai.InitClient(&cfg.AI) ai.InitClient(&cfg.AI)
} }
// 初始化超级管理员账户 // 初始化超级管理员账户(唯一自动导入的种子数据)
if err := user.InitAdminUser(); err != nil { if err := user.InitAdminUser(); err != nil {
log.Printf("Warning: Failed to init admin user: %v", err) log.Printf("Warning: Failed to init admin user: %v", err)
} }
// 初始化 RBAC 种子数据(角色/权限/菜单,v12新增) // 注意:RBAC种子数据、科室医生、药品数据不再自动导入
admin.SeedRBAC() // 请通过管理端API手动导入:
// POST /api/v1/admin/seed/rbac — 导入角色/权限/菜单
// 初始化科室和医生数据 // POST /api/v1/admin/seed/departments — 导入科室和医生
if err := doctor.InitDepartmentsAndDoctors(); err != nil { // POST /api/v1/admin/seed/medicines — 导入药品数据
log.Printf("Warning: Failed to init departments and doctors: %v", err)
}
// 初始化药品种子数据
admin.SeedMedicines()
// 初始化Agent工具(内置工具 + HTTP 动态工具) // 初始化Agent工具(内置工具 + HTTP 动态工具)
internalagent.InitTools() internalagent.InitTools()
...@@ -251,10 +244,12 @@ func main() { ...@@ -251,10 +244,12 @@ func main() {
adminApi.Use(middleware.RequireRole("admin")) adminApi.Use(middleware.RequireRole("admin"))
adminHandler := admin.NewHandler() adminHandler := admin.NewHandler()
adminHandler.RegisterRoutes(adminApi) adminHandler.RegisterRoutes(adminApi)
adminHandler.RegisterUserRoutes(authApi) // /my/menus — 所有登录用户可访问
// Agent路由(需要认证 // Agent路由:聊天/会话(全角色)+ 管理(仅admin
agentHandler := internalagent.NewHandler() agentHandler := internalagent.NewHandler()
agentHandler.RegisterRoutes(authApi) agentHandler.RegisterRoutes(authApi)
agentHandler.RegisterAdminRoutes(adminApi)
// 工作流路由(需要认证) // 工作流路由(需要认证)
workflowHandler := workflowsvc.NewHandler() workflowHandler := workflowsvc.NewHandler()
......
...@@ -55,7 +55,8 @@ func defaultAgentDefinitions() []model.AgentDefinition { ...@@ -55,7 +55,8 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 所有医疗建议仅供参考,请以专业医生判断为准 - 所有医疗建议仅供参考,请以专业医生判断为准
页面导航能力: 页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如找医生、我的问诊、处方、健康档案等 - 你可以使用 navigate_page 工具打开患者端页面,如找医生、我的问诊、处方、健康档案等
- 你只能导航到 patient_* 开头的页面,不能访问管理端或医生端页面
- 当用户想查看某个页面时,直接调用 navigate_page 工具导航到对应页面`, - 当用户想查看某个页面时,直接调用 navigate_page 工具导航到对应页面`,
Tools: string(patientTools), Tools: string(patientTools),
Config: "{}", Config: "{}",
...@@ -87,7 +88,8 @@ func defaultAgentDefinitions() []model.AgentDefinition { ...@@ -87,7 +88,8 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 所有建议仅供医生参考,请结合临床实际情况 - 所有建议仅供医生参考,请结合临床实际情况
页面导航能力: 页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如工作台、问诊大厅、患者档案、排班管理等 - 你可以使用 navigate_page 工具打开医生端页面,如工作台、问诊大厅、患者档案、排班管理等
- 你只能导航到 doctor_* 开头的页面,不能访问管理端或患者端页面
- 当医生想查看某个页面时,直接调用 navigate_page 工具导航到对应页面`, - 当医生想查看某个页面时,直接调用 navigate_page 工具导航到对应页面`,
Tools: string(doctorTools), Tools: string(doctorTools),
Config: "{}", Config: "{}",
...@@ -117,7 +119,7 @@ func defaultAgentDefinitions() []model.AgentDefinition { ...@@ -117,7 +119,7 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 用中文回答 - 用中文回答
页面导航能力: 页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如医生管理、患者管理、科室管理、问诊管理等 - 你可以使用 navigate_page 工具打开所有系统页面,包括管理端、患者端、医生端
- 当管理员想查看或操作某个页面时,直接调用 navigate_page 工具导航到对应页面 - 当管理员想查看或操作某个页面时,直接调用 navigate_page 工具导航到对应页面
- 支持 open_add 操作自动打开新增弹窗(如新增医生、新增科室等)`, - 支持 open_add 操作自动打开新增弹窗(如新增医生、新增科室等)`,
Tools: string(adminTools), Tools: string(adminTools),
......
...@@ -27,6 +27,7 @@ func NewHandler() *Handler { ...@@ -27,6 +27,7 @@ func NewHandler() *Handler {
return &Handler{svc: GetService()} return &Handler{svc: GetService()}
} }
// RegisterRoutes 用户侧路由(全角色可用,仅需 JWT)
func (h *Handler) RegisterRoutes(r gin.IRouter) { func (h *Handler) RegisterRoutes(r gin.IRouter) {
g := r.Group("/agent") g := r.Group("/agent")
g.POST("/:agent_id/chat", h.Chat) g.POST("/:agent_id/chat", h.Chat)
...@@ -34,6 +35,11 @@ func (h *Handler) RegisterRoutes(r gin.IRouter) { ...@@ -34,6 +35,11 @@ func (h *Handler) RegisterRoutes(r gin.IRouter) {
g.GET("/sessions", h.ListSessions) g.GET("/sessions", h.ListSessions)
g.DELETE("/session/:session_id", h.DeleteSession) g.DELETE("/session/:session_id", h.DeleteSession)
g.GET("/list", h.ListAgents) g.GET("/list", h.ListAgents)
}
// RegisterAdminRoutes 管理侧路由(仅 admin,需挂在 adminApi 组下)
func (h *Handler) RegisterAdminRoutes(r gin.IRouter) {
g := r.Group("/agent")
g.GET("/tools", h.ListTools) g.GET("/tools", h.ListTools)
// HTTP 动态工具管理 // HTTP 动态工具管理
...@@ -44,7 +50,7 @@ func (h *Handler) RegisterRoutes(r gin.IRouter) { ...@@ -44,7 +50,7 @@ func (h *Handler) RegisterRoutes(r gin.IRouter) {
g.POST("/http-tools/:id/test", h.TestHTTPTool) g.POST("/http-tools/:id/test", h.TestHTTPTool)
g.POST("/http-tools/reload", h.ReloadHTTPToolsAPI) g.POST("/http-tools/reload", h.ReloadHTTPToolsAPI)
// Agent 配置管理(管理端使用) // Agent 配置管理
g.GET("/definitions", h.ListDefinitions) g.GET("/definitions", h.ListDefinitions)
g.GET("/definitions/:agent_id", h.GetDefinition) g.GET("/definitions/:agent_id", h.GetDefinition)
g.POST("/definitions", h.CreateDefinition) g.POST("/definitions", h.CreateDefinition)
...@@ -73,8 +79,10 @@ func (h *Handler) Chat(c *gin.Context) { ...@@ -73,8 +79,10 @@ func (h *Handler) Chat(c *gin.Context) {
userID, _ := c.Get("user_id") userID, _ := c.Get("user_id")
uid, _ := userID.(string) uid, _ := userID.(string)
role, _ := c.Get("role")
userRole, _ := role.(string)
output, err := h.svc.Chat(c.Request.Context(), agentID, uid, req.SessionID, req.Message, req.Context) output, err := h.svc.Chat(c.Request.Context(), agentID, uid, userRole, req.SessionID, req.Message, req.Context)
if err != nil { if err != nil {
response.Error(c, 500, err.Error()) response.Error(c, 500, err.Error())
return return
...@@ -113,12 +121,14 @@ func (h *Handler) ChatStream(c *gin.Context) { ...@@ -113,12 +121,14 @@ func (h *Handler) ChatStream(c *gin.Context) {
userID, _ := c.Get("user_id") userID, _ := c.Get("user_id")
uid, _ := userID.(string) uid, _ := userID.(string)
role, _ := c.Get("role")
userRole, _ := role.(string)
emit := func(event, data string) { emit := func(event, data string) {
sendSSEEvent(c, flusher, event, data) sendSSEEvent(c, flusher, event, data)
} }
h.svc.ChatStream(c.Request.Context(), agentID, uid, req.SessionID, req.Message, req.Context, emit) h.svc.ChatStream(c.Request.Context(), agentID, uid, userRole, req.SessionID, req.Message, req.Context, emit)
} }
func (h *Handler) ListAgents(c *gin.Context) { func (h *Handler) ListAgents(c *gin.Context) {
......
...@@ -81,7 +81,9 @@ func WireCallbacks() { ...@@ -81,7 +81,9 @@ func WireCallbacks() {
svc := GetService() svc := GetService()
tools.AgentCallFn = func(ctx context.Context, agentID, userID, sessionID, message string, ctxData map[string]interface{}) (string, error) { tools.AgentCallFn = func(ctx context.Context, agentID, userID, sessionID, message string, ctxData map[string]interface{}) (string, error) {
output, err := svc.Chat(ctx, agentID, userID, sessionID, message, ctxData) // 从 context 继承 userRole(agent-to-agent 调用)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
output, err := svc.Chat(ctx, agentID, userID, userRole, sessionID, message, ctxData)
if err != nil { if err != nil {
return "", err return "", err
} }
...@@ -97,7 +99,8 @@ func WireCallbacks() { ...@@ -97,7 +99,8 @@ func WireCallbacks() {
// v15: 初始化 SkillExecutor(多Agent编排引擎) // v15: 初始化 SkillExecutor(多Agent编排引擎)
agent.InitSkillExecutor(func(ctx context.Context, agentID, userID, sessionID, message string, ctxData map[string]interface{}) (string, error) { agent.InitSkillExecutor(func(ctx context.Context, agentID, userID, sessionID, message string, ctxData map[string]interface{}) (string, error) {
output, err := svc.Chat(ctx, agentID, userID, sessionID, message, ctxData) userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
output, err := svc.Chat(ctx, agentID, userID, userRole, sessionID, message, ctxData)
if err != nil { if err != nil {
return "", err return "", err
} }
......
...@@ -69,7 +69,12 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent { ...@@ -69,7 +69,12 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent {
systemPrompt := def.SystemPrompt systemPrompt := def.SystemPrompt
var skillIDs []string var skillIDs []string
if def.Skills != "" { if def.Skills != "" {
json.Unmarshal([]byte(def.Skills), &skillIDs) if err := json.Unmarshal([]byte(def.Skills), &skillIDs); err != nil {
var raw string
if json.Unmarshal([]byte(def.Skills), &raw) == nil {
json.Unmarshal([]byte(raw), &skillIDs)
}
}
} }
if len(skillIDs) > 0 { if len(skillIDs) > 0 {
db := database.GetDB() db := database.GetDB()
...@@ -83,7 +88,12 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent { ...@@ -83,7 +88,12 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent {
for _, skill := range skills { for _, skill := range skills {
// 合并工具(去重) // 合并工具(去重)
var skillTools []string var skillTools []string
json.Unmarshal([]byte(skill.Tools), &skillTools) if err := json.Unmarshal([]byte(skill.Tools), &skillTools); err != nil {
var raw string
if json.Unmarshal([]byte(skill.Tools), &raw) == nil {
json.Unmarshal([]byte(raw), &skillTools)
}
}
for _, st := range skillTools { for _, st := range skillTools {
if !toolSet[st] { if !toolSet[st] {
tools = append(tools, st) tools = append(tools, st)
...@@ -116,7 +126,12 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent { ...@@ -116,7 +126,12 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent {
func getOrchestrationSkills(def model.AgentDefinition) []model.AgentSkill { func getOrchestrationSkills(def model.AgentDefinition) []model.AgentSkill {
var skillIDs []string var skillIDs []string
if def.Skills != "" { if def.Skills != "" {
json.Unmarshal([]byte(def.Skills), &skillIDs) if err := json.Unmarshal([]byte(def.Skills), &skillIDs); err != nil {
var raw string
if json.Unmarshal([]byte(def.Skills), &raw) == nil {
json.Unmarshal([]byte(raw), &skillIDs)
}
}
} }
if len(skillIDs) == 0 { if len(skillIDs) == 0 {
return nil return nil
...@@ -212,7 +227,7 @@ func (s *AgentService) ListAgents() []map[string]interface{} { ...@@ -212,7 +227,7 @@ func (s *AgentService) ListAgents() []map[string]interface{} {
} }
// Chat 执行Agent对话并持久化会话 // Chat 执行Agent对话并持久化会话
func (s *AgentService) Chat(ctx context.Context, agentID, userID, sessionID, message string, contextData map[string]interface{}) (*agent.AgentOutput, error) { func (s *AgentService) Chat(ctx context.Context, agentID, userID, userRole, sessionID, message string, contextData map[string]interface{}) (*agent.AgentOutput, error) {
a, ok := s.GetAgent(agentID) a, ok := s.GetAgent(agentID)
if !ok { if !ok {
return nil, nil return nil, nil
...@@ -236,6 +251,7 @@ func (s *AgentService) Chat(ctx context.Context, agentID, userID, sessionID, mes ...@@ -236,6 +251,7 @@ func (s *AgentService) Chat(ctx context.Context, agentID, userID, sessionID, mes
input := agent.AgentInput{ input := agent.AgentInput{
SessionID: sessionID, SessionID: sessionID,
UserID: userID, UserID: userID,
UserRole: userRole,
Message: message, Message: message,
Context: contextData, Context: contextData,
History: history, History: history,
...@@ -297,7 +313,7 @@ func (s *AgentService) Chat(ctx context.Context, agentID, userID, sessionID, mes ...@@ -297,7 +313,7 @@ func (s *AgentService) Chat(ctx context.Context, agentID, userID, sessionID, mes
} }
// ChatStream 流式执行Agent对话(SSE),完成后持久化 // ChatStream 流式执行Agent对话(SSE),完成后持久化
func (s *AgentService) ChatStream(ctx context.Context, agentID, userID, sessionID, message string, contextData map[string]interface{}, emit func(event, data string)) { func (s *AgentService) ChatStream(ctx context.Context, agentID, userID, userRole, sessionID, message string, contextData map[string]interface{}, emit func(event, data string)) {
a, ok := s.GetAgent(agentID) a, ok := s.GetAgent(agentID)
if !ok { if !ok {
errJSON, _ := json.Marshal(map[string]string{"error": "agent not found"}) errJSON, _ := json.Marshal(map[string]string{"error": "agent not found"})
...@@ -378,6 +394,7 @@ func (s *AgentService) ChatStream(ctx context.Context, agentID, userID, sessionI ...@@ -378,6 +394,7 @@ func (s *AgentService) ChatStream(ctx context.Context, agentID, userID, sessionI
input := agent.AgentInput{ input := agent.AgentInput{
SessionID: sessionID, SessionID: sessionID,
UserID: userID, UserID: userID,
UserRole: userRole,
Message: message, Message: message,
Context: contextData, Context: contextData,
History: history, History: history,
......
package model
import "time"
// RouteEntry 前端路由注册表(供 AI 导航工具查询)
type RouteEntry struct {
ID uint `gorm:"primaryKey" json:"id"`
PageCode string `gorm:"type:varchar(100);uniqueIndex" json:"page_code"`
PageName string `gorm:"type:varchar(200)" json:"page_name"`
Module string `gorm:"type:varchar(50)" json:"module"` // admin/patient/doctor
Route string `gorm:"type:varchar(200)" json:"route"`
EditRoute string `gorm:"type:varchar(200)" json:"edit_route"`
AddRoute string `gorm:"type:varchar(200)" json:"add_route"`
Operations string `gorm:"type:jsonb;default:'[]'" json:"operations"` // ["view","create","edit","delete"]
RoleAccess string `gorm:"type:varchar(100)" json:"role_access"` // "admin"
Description string `gorm:"type:text" json:"description"`
Status string `gorm:"type:varchar(20);default:'active'" json:"status"`
SortOrder int `gorm:"default:0" json:"sort_order"`
Embeddable bool `gorm:"default:false" json:"embeddable"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
package admin package admin
import ( import (
"net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"internet-hospital/internal/model" "internet-hospital/internal/model"
"internet-hospital/pkg/database" "internet-hospital/pkg/database"
"internet-hospital/pkg/response"
) )
// GetAgentExecutionLogs 获取 Agent 执行日志(分页) // GetAgentExecutionLogs 获取 Agent 执行日志(分页)
...@@ -48,12 +48,12 @@ func (h *Handler) GetAgentExecutionLogs(c *gin.Context) { ...@@ -48,12 +48,12 @@ func (h *Handler) GetAgentExecutionLogs(c *gin.Context) {
Limit(pageSize). Limit(pageSize).
Find(&logs) Find(&logs)
c.JSON(http.StatusOK, gin.H{"data": gin.H{ response.Success(c, gin.H{
"list": logs, "list": logs,
"total": total, "total": total,
"page": page, "page": page,
"page_size": pageSize, "page_size": pageSize,
}}) })
} }
// GetAgentStats 获取 Agent 执行统计 // GetAgentStats 获取 Agent 执行统计
...@@ -83,5 +83,5 @@ func (h *Handler) GetAgentStats(c *gin.Context) { ...@@ -83,5 +83,5 @@ func (h *Handler) GetAgentStats(c *gin.Context) {
Group("agent_id"). Group("agent_id").
Scan(&stats) Scan(&stats)
c.JSON(http.StatusOK, gin.H{"data": stats}) response.Success(c, stats)
} }
package admin
import (
"fmt"
"log"
"internet-hospital/pkg/database"
"github.com/gin-gonic/gin"
"internet-hospital/pkg/response"
)
// expectedTables 所有已注册 GORM model 对应的数据库表名(白名单)
var expectedTables = map[string]bool{
// 用户相关
"users": true,
"user_verifications": true,
"patient_profiles": true,
// 医生相关
"departments": true,
"doctors": true,
"doctor_schedules": true,
"doctor_reviews": true,
// 问诊相关
"consultations": true,
"consult_messages": true,
"video_rooms": true,
"consult_transfers": true,
// 预问诊
"pre_consultations": true,
// 处方相关
"medicines": true,
"prescriptions": true,
"prescription_items": true,
// 健康档案
"lab_reports": true,
"family_members": true,
// 慢病管理
"chronic_records": true,
"renewal_requests": true,
"medication_reminders": true,
"health_metrics": true,
// AI & 系统
"ai_configs": true,
"ai_usage_logs": true,
"prompt_templates": true,
"system_logs": true,
// Agent 相关
"agent_tools": true,
"agent_tool_logs": true,
"agent_definitions": true,
"agent_sessions": true,
"agent_execution_logs": true,
"agent_skills": true,
// 工作流相关
"workflow_definitions": true,
"workflow_executions": true,
"workflow_human_tasks": true,
// 知识库相关
"knowledge_collections": true,
"knowledge_documents": true,
"knowledge_chunks": true,
// 支付相关
"payment_orders": true,
"doctor_incomes": true,
"doctor_withdrawals": true,
// 安全过滤
"safety_word_rules": true,
"safety_filter_logs": true,
// 通知
"notifications": true,
// 合规报告
"compliance_reports": true,
// HTTP / SQL 动态工具
"http_tool_definitions": true,
"sql_tool_definitions": true,
// 快捷回复
"quick_reply_templates": true,
// RBAC
"roles": true,
"permissions": true,
"role_permissions": true,
"user_roles": true,
"menus": true,
"role_menus": true,
}
// ScanExtraTables 扫描数据库中多余的表(不在 model 白名单中)
func ScanExtraTables() ([]string, error) {
db := database.GetDB()
if db == nil {
return nil, fmt.Errorf("数据库未初始化")
}
// 查询当前 schema 下所有用户表
var tables []string
db.Raw(`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename`).Scan(&tables)
var extra []string
for _, t := range tables {
if !expectedTables[t] {
extra = append(extra, t)
}
}
return extra, nil
}
// DropExtraTables 删除多余的数据库表
func DropExtraTables() ([]string, error) {
extra, err := ScanExtraTables()
if err != nil {
return nil, err
}
if len(extra) == 0 {
return nil, nil
}
db := database.GetDB()
var dropped []string
for _, t := range extra {
sql := fmt.Sprintf("DROP TABLE IF EXISTS %q CASCADE", t)
if err := db.Exec(sql).Error; err != nil {
log.Printf("[CleanupTables] 删除表 %s 失败: %v", t, err)
} else {
log.Printf("[CleanupTables] 已删除多余表: %s", t)
dropped = append(dropped, t)
}
}
return dropped, nil
}
// ScanExtraTablesHandler 扫描多余表(仅查看,不删除)
func (h *Handler) ScanExtraTablesHandler(c *gin.Context) {
extra, err := ScanExtraTables()
if err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, gin.H{
"extra_tables": extra,
"count": len(extra),
})
}
// DropExtraTablesHandler 删除多余的数据库表
func (h *Handler) DropExtraTablesHandler(c *gin.Context) {
dropped, err := DropExtraTables()
if err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, gin.H{
"dropped_tables": dropped,
"count": len(dropped),
"message": fmt.Sprintf("已删除 %d 张多余表", len(dropped)),
})
}
...@@ -3,6 +3,7 @@ package admin ...@@ -3,6 +3,7 @@ package admin
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"internet-hospital/internal/service/doctor"
"internet-hospital/pkg/response" "internet-hospital/pkg/response"
) )
...@@ -124,31 +125,29 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { ...@@ -124,31 +125,29 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
adm.GET("/ai-center/trace", h.GetTraceDetail) adm.GET("/ai-center/trace", h.GetTraceDetail)
adm.GET("/ai-center/stats", h.GetAICenterStats) adm.GET("/ai-center/stats", h.GetAICenterStats)
// 角色管理(v12新增 // 角色管理(只读
adm.GET("/roles", h.ListRoles) adm.GET("/roles", h.ListRoles)
adm.POST("/roles", h.CreateRole)
adm.PUT("/roles/:id", h.UpdateRole)
adm.DELETE("/roles/:id", h.DeleteRole)
adm.GET("/roles/:id/permissions", h.GetRolePermissions)
adm.PUT("/roles/:id/permissions", h.SetRolePermissions)
adm.GET("/roles/:id/menus", h.GetRoleMenus) adm.GET("/roles/:id/menus", h.GetRoleMenus)
adm.PUT("/roles/:id/menus", h.SetRoleMenus) adm.PUT("/roles/:id/menus", h.SetRoleMenus)
// 权限管理(v12新增) // 用户角色分配(直接更新 User.Role)
adm.GET("/permissions", h.ListPermissions) adm.PUT("/users/:id/role", h.SetUserRole)
adm.POST("/permissions", h.CreatePermission)
// 菜单管理(v12新增) // 菜单管理
adm.GET("/menus", h.ListMenus) adm.GET("/menus", h.ListMenus)
adm.GET("/menus/flat", h.ListMenusFlat) adm.GET("/menus/flat", h.ListMenusFlat)
adm.POST("/menus", h.CreateMenu) adm.POST("/menus", h.CreateMenu)
adm.PUT("/menus/:id", h.UpdateMenu) adm.PUT("/menus/:id", h.UpdateMenu)
adm.DELETE("/menus/:id", h.DeleteMenu) adm.DELETE("/menus/:id", h.DeleteMenu)
adm.POST("/menus/reseed", h.ReseedMenus)
// 用户角色分配(v12新增) // 手动种子数据导入
adm.GET("/users/:id/roles", h.GetUserRoles) adm.POST("/seed/rbac", h.SeedRBACHandler)
adm.PUT("/users/:id/roles", h.SetUserRoles) adm.POST("/seed/departments", h.SeedDepartmentsHandler)
adm.POST("/seed/medicines", h.SeedMedicinesHandler)
// 数据库维护
adm.GET("/db/extra-tables", h.ScanExtraTablesHandler)
adm.DELETE("/db/extra-tables", h.DropExtraTablesHandler)
// 数据导出 // 数据导出
adm.POST("/export/:type", h.ExportData) adm.POST("/export/:type", h.ExportData)
...@@ -159,7 +158,10 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { ...@@ -159,7 +158,10 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
adm.POST("/reports/:id/submit", h.SubmitReport) adm.POST("/reports/:id/submit", h.SubmitReport)
} }
// 当前用户菜单(不在 /admin 前缀下,所有登录用户可访问) }
// RegisterUserRoutes 注册所有登录用户可访问的路由(不受 admin 角色限制)
func (h *Handler) RegisterUserRoutes(r *gin.RouterGroup) {
r.GET("/my/menus", h.GetMyMenus) r.GET("/my/menus", h.GetMyMenus)
} }
...@@ -468,3 +470,24 @@ func (h *Handler) ResetAdminPassword(c *gin.Context) { ...@@ -468,3 +470,24 @@ func (h *Handler) ResetAdminPassword(c *gin.Context) {
response.Success(c, nil) response.Success(c, nil)
} }
// SeedRBACHandler 手动导入RBAC种子数据(角色、权限、菜单)
func (h *Handler) SeedRBACHandler(c *gin.Context) {
SeedRBAC()
response.Success(c, gin.H{"message": "RBAC种子数据(角色/权限/菜单)已导入"})
}
// SeedDepartmentsHandler 手动导入科室和医生数据
func (h *Handler) SeedDepartmentsHandler(c *gin.Context) {
if err := doctor.InitDepartmentsAndDoctors(); err != nil {
response.Error(c, 500, "导入科室和医生数据失败: "+err.Error())
return
}
response.Success(c, gin.H{"message": "科室和医生种子数据已导入"})
}
// SeedMedicinesHandler 手动导入药品种子数据
func (h *Handler) SeedMedicinesHandler(c *gin.Context) {
SeedMedicines()
response.Success(c, gin.H{"message": "药品种子数据已导入"})
}
...@@ -152,8 +152,3 @@ func (h *Handler) DeleteMenu(c *gin.Context) { ...@@ -152,8 +152,3 @@ func (h *Handler) DeleteMenu(c *gin.Context) {
response.Success(c, nil) response.Success(c, nil)
} }
// ReseedMenus 重新导入菜单数据(删除旧数据,以页面展示结构为准)
func (h *Handler) ReseedMenus(c *gin.Context) {
ReseedMenus()
response.Success(c, gin.H{"message": "菜单数据已重新导入"})
}
...@@ -5,15 +5,13 @@ import ( ...@@ -5,15 +5,13 @@ import (
"internet-hospital/internal/model" "internet-hospital/internal/model"
"internet-hospital/pkg/database" "internet-hospital/pkg/database"
"gorm.io/gorm"
) )
// SeedRBAC 初始化角色、权限、菜单种子数据(幂等,只在表空时插入) // SeedRBAC 初始化角色种子数据(幂等,仅在 roles 表为空时插入)
// 菜单数据通过管理端菜单管理页面维护,不在代码中硬编码。
func SeedRBAC() { func SeedRBAC() {
db := database.GetDB() db := database.GetDB()
// ── 1. 角色种子 ──
var roleCount int64 var roleCount int64
db.Model(&model.Role{}).Count(&roleCount) db.Model(&model.Role{}).Count(&roleCount)
if roleCount == 0 { if roleCount == 0 {
...@@ -27,185 +25,4 @@ func SeedRBAC() { ...@@ -27,185 +25,4 @@ func SeedRBAC() {
} }
log.Println("[SeedRBAC] 角色种子数据已创建") log.Println("[SeedRBAC] 角色种子数据已创建")
} }
// ── 2. 权限种子 ──
var permCount int64
db.Model(&model.Permission{}).Count(&permCount)
if permCount == 0 {
perms := []model.Permission{
// 用户管理
{Code: "admin:users:list", Name: "用户列表", Module: "users", Action: "list"},
{Code: "admin:users:create", Name: "创建用户", Module: "users", Action: "create"},
{Code: "admin:users:update", Name: "编辑用户", Module: "users", Action: "update"},
{Code: "admin:users:delete", Name: "删除用户", Module: "users", Action: "delete"},
// 医生管理
{Code: "admin:doctors:list", Name: "医生列表", Module: "doctors", Action: "list"},
{Code: "admin:doctors:create", Name: "创建医生", Module: "doctors", Action: "create"},
{Code: "admin:doctors:update", Name: "编辑医生", Module: "doctors", Action: "update"},
{Code: "admin:doctors:delete", Name: "删除医生", Module: "doctors", Action: "delete"},
{Code: "admin:doctors:review", Name: "医生审核", Module: "doctors", Action: "review"},
// 科室管理
{Code: "admin:departments:list", Name: "科室列表", Module: "departments", Action: "list"},
{Code: "admin:departments:create", Name: "创建科室", Module: "departments", Action: "create"},
{Code: "admin:departments:update", Name: "编辑科室", Module: "departments", Action: "update"},
{Code: "admin:departments:delete", Name: "删除科室", Module: "departments", Action: "delete"},
// 问诊管理
{Code: "admin:consultations:list", Name: "问诊列表", Module: "consultations", Action: "list"},
// 处方管理
{Code: "admin:prescriptions:list", Name: "处方列表", Module: "prescriptions", Action: "list"},
{Code: "admin:prescriptions:review", Name: "处方审核", Module: "prescriptions", Action: "review"},
// 药品管理
{Code: "admin:medicines:list", Name: "药品列表", Module: "medicines", Action: "list"},
{Code: "admin:medicines:create", Name: "创建药品", Module: "medicines", Action: "create"},
{Code: "admin:medicines:update", Name: "编辑药品", Module: "medicines", Action: "update"},
{Code: "admin:medicines:delete", Name: "删除药品", Module: "medicines", Action: "delete"},
// AI配置
{Code: "admin:ai-config:view", Name: "查看AI配置", Module: "ai-config", Action: "view"},
{Code: "admin:ai-config:edit", Name: "编辑AI配置", Module: "ai-config", Action: "edit"},
// Agent管理
{Code: "admin:agents:list", Name: "Agent列表", Module: "agents", Action: "list"},
{Code: "admin:agents:create", Name: "创建Agent", Module: "agents", Action: "create"},
{Code: "admin:agents:update", Name: "编辑Agent", Module: "agents", Action: "update"},
// 工具管理
{Code: "admin:tools:list", Name: "工具列表", Module: "tools", Action: "list"},
{Code: "admin:tools:manage", Name: "工具管理", Module: "tools", Action: "manage"},
// 知识库
{Code: "admin:knowledge:list", Name: "知识库列表", Module: "knowledge", Action: "list"},
{Code: "admin:knowledge:manage", Name: "知识库管理", Module: "knowledge", Action: "manage"},
// 角色管理
{Code: "admin:roles:list", Name: "角色列表", Module: "roles", Action: "list"},
{Code: "admin:roles:create", Name: "创建角色", Module: "roles", Action: "create"},
{Code: "admin:roles:update", Name: "编辑角色", Module: "roles", Action: "update"},
{Code: "admin:roles:delete", Name: "删除角色", Module: "roles", Action: "delete"},
{Code: "admin:roles:assign", Name: "分配权限/菜单", Module: "roles", Action: "assign"},
// 菜单管理
{Code: "admin:menus:list", Name: "菜单列表", Module: "menus", Action: "list"},
{Code: "admin:menus:create", Name: "创建菜单", Module: "menus", Action: "create"},
{Code: "admin:menus:update", Name: "编辑菜单", Module: "menus", Action: "update"},
{Code: "admin:menus:delete", Name: "删除菜单", Module: "menus", Action: "delete"},
// 运营中心
{Code: "admin:dashboard:view", Name: "运营大盘", Module: "dashboard", Action: "view"},
{Code: "admin:ai-center:view", Name: "AI运营中心", Module: "ai-center", Action: "view"},
{Code: "admin:safety:manage", Name: "内容安全管理", Module: "safety", Action: "manage"},
{Code: "admin:compliance:view", Name: "合规报表", Module: "compliance", Action: "view"},
}
for _, p := range perms {
db.Create(&p)
}
log.Println("[SeedRBAC] 权限种子数据已创建")
}
// ── 3. 菜单种子(仅在菜单表为空时导入,避免覆盖手动修改) ──
var menuCount int64
db.Model(&model.Menu{}).Count(&menuCount)
if menuCount == 0 {
ReseedMenus()
}
// ── 4. 为超级管理员角色分配全部权限(菜单已在 ReseedMenus 中分配) ──
var adminRole model.Role
if err := db.Where("code = ?", "admin").First(&adminRole).Error; err == nil {
var rpCount int64
db.Model(&model.RolePermission{}).Where("role_id = ?", adminRole.ID).Count(&rpCount)
if rpCount == 0 {
var allPerms []model.Permission
db.Find(&allPerms)
for _, p := range allPerms {
db.Create(&model.RolePermission{RoleID: adminRole.ID, PermissionID: p.ID})
}
log.Println("[SeedRBAC] 超管角色已分配全部权限")
}
}
// ── 5. 为已有 admin 用户绑定管理员角色 ──
if adminRole.ID > 0 {
var adminUser model.User
if err := db.Where("role = ?", "admin").First(&adminUser).Error; err == nil {
var urCount int64
db.Model(&model.UserRole{}).Where("user_id = ?", adminUser.ID).Count(&urCount)
if urCount == 0 {
db.Create(&model.UserRole{UserID: adminUser.ID, RoleID: adminRole.ID})
log.Printf("[SeedRBAC] 用户 %s 已绑定管理员角色", adminUser.ID)
}
}
}
}
// ReseedMenus 删除旧菜单数据并重新导入(以页面展示的菜单结构为准)
func ReseedMenus() {
db := database.GetDB()
// 清空旧的角色-菜单关联和菜单数据
db.Exec("DELETE FROM role_menus")
db.Exec("DELETE FROM menus")
log.Println("[ReseedMenus] 已清空旧菜单数据和角色-菜单关联")
// 重新创建菜单
seedMenus(db)
// 重新为超管分配全部菜单
var adminRole model.Role
if err := db.Where("code = ?", "admin").First(&adminRole).Error; err == nil {
var allMenus []model.Menu
db.Find(&allMenus)
for _, m := range allMenus {
db.Create(&model.RoleMenu{RoleID: adminRole.ID, MenuID: m.ID})
}
log.Println("[ReseedMenus] 超管角色已重新分配全部菜单")
}
}
// seedMenus 创建与前端路由完全一致的菜单结构(含 permission 字段供导航工具做 RBAC 校验)
func seedMenus(db *gorm.DB) {
m := func(parentID uint, name, path, icon, perm string, sort int) model.Menu {
menu := model.Menu{
ParentID: parentID, Name: name, Path: path, Icon: icon,
Permission: perm, Sort: sort, Type: "menu", Visible: true, Status: "active",
}
db.Create(&menu)
return menu
}
// 1. 运营大盘(顶级)
m(0, "运营大盘", "/admin/dashboard", "DashboardOutlined", "admin:dashboard:view", 1)
// 2. 用户管理(子菜单)
userMgmt := m(0, "用户管理", "", "TeamOutlined", "", 2)
m(userMgmt.ID, "患者管理", "/admin/patients", "UserOutlined", "admin:users:list", 1)
m(userMgmt.ID, "医生管理", "/admin/doctors", "MedicineBoxOutlined", "admin:doctors:list", 2)
m(userMgmt.ID, "管理员管理", "/admin/admins", "SettingOutlined", "admin:users:list", 3)
// 3. 科室管理(顶级)
m(0, "科室管理", "/admin/departments", "ApartmentOutlined", "admin:departments:list", 3)
// 4. 问诊管理(顶级)
m(0, "问诊管理", "/admin/consultations", "FileSearchOutlined", "admin:consultations:list", 4)
// 5. 处方监管(顶级)
m(0, "处方监管", "/admin/prescription", "FileTextOutlined", "admin:prescriptions:list", 5)
// 6. 药品库(顶级)
m(0, "药品库", "/admin/pharmacy", "MedicineBoxOutlined", "admin:medicines:list", 6)
// 7. AI配置(顶级)
m(0, "AI配置", "/admin/ai-config", "RobotOutlined", "admin:ai-config:view", 7)
// 8. 合规报表(顶级)
m(0, "合规报表", "/admin/compliance", "SafetyCertificateOutlined", "admin:compliance:view", 8)
// 9. 智能体平台(子菜单)
aiPlatform := m(0, "智能体平台", "", "ApiOutlined", "", 9)
m(aiPlatform.ID, "Agent管理", "/admin/agents", "RobotOutlined", "admin:agents:list", 1)
m(aiPlatform.ID, "工具中心", "/admin/tools", "AppstoreOutlined", "admin:tools:list", 2)
m(aiPlatform.ID, "工作流", "/admin/workflows", "DeploymentUnitOutlined", "admin:tools:manage", 3)
m(aiPlatform.ID, "人工审核", "/admin/tasks", "CheckCircleOutlined", "admin:tools:manage", 4)
m(aiPlatform.ID, "知识库", "/admin/knowledge", "BookOutlined", "admin:knowledge:list", 5)
m(aiPlatform.ID, "内容安全", "/admin/safety", "SafetyOutlined", "admin:safety:manage", 6)
m(aiPlatform.ID, "AI运营中心", "/admin/ai-center", "FundOutlined", "admin:ai-center:view", 7)
// 10. 系统管理(子菜单)
sysMgmt := m(0, "系统管理", "", "SafetyCertificateOutlined", "", 10)
m(sysMgmt.ID, "角色管理", "/admin/roles", "SafetyOutlined", "admin:roles:list", 1)
m(sysMgmt.ID, "菜单管理", "/admin/menus", "AppstoreOutlined", "admin:menus:list", 2)
log.Println("[SeedRBAC] 菜单种子数据已创建(与前端路由完全一致)")
} }
package admin package admin
import ( import (
"net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"internet-hospital/internal/model" "internet-hospital/internal/model"
"internet-hospital/pkg/database" "internet-hospital/pkg/database"
"internet-hospital/pkg/response"
) )
// ListWorkflows 工作流列表 // ListWorkflows 工作流列表
func (h *Handler) ListWorkflows(c *gin.Context) { func (h *Handler) ListWorkflows(c *gin.Context) {
var workflows []model.WorkflowDefinition var workflows []model.WorkflowDefinition
database.GetDB().Find(&workflows) database.GetDB().Find(&workflows)
c.JSON(http.StatusOK, gin.H{"data": workflows}) response.Success(c, workflows)
} }
// CreateWorkflow 创建工作流 // CreateWorkflow 创建工作流
...@@ -27,7 +27,7 @@ func (h *Handler) CreateWorkflow(c *gin.Context) { ...@@ -27,7 +27,7 @@ func (h *Handler) CreateWorkflow(c *gin.Context) {
Definition string `json:"definition"` Definition string `json:"definition"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) response.BadRequest(c, err.Error())
return return
} }
wf := model.WorkflowDefinition{ wf := model.WorkflowDefinition{
...@@ -40,7 +40,7 @@ func (h *Handler) CreateWorkflow(c *gin.Context) { ...@@ -40,7 +40,7 @@ func (h *Handler) CreateWorkflow(c *gin.Context) {
Version: 1, Version: 1,
} }
database.GetDB().Create(&wf) database.GetDB().Create(&wf)
c.JSON(http.StatusOK, gin.H{"data": wf}) response.Success(c, wf)
} }
// UpdateWorkflow 更新工作流(名称/描述/定义) // UpdateWorkflow 更新工作流(名称/描述/定义)
...@@ -51,7 +51,7 @@ func (h *Handler) UpdateWorkflow(c *gin.Context) { ...@@ -51,7 +51,7 @@ func (h *Handler) UpdateWorkflow(c *gin.Context) {
Definition string `json:"definition"` Definition string `json:"definition"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) response.BadRequest(c, err.Error())
return return
} }
updates := map[string]interface{}{} updates := map[string]interface{}{}
...@@ -67,7 +67,7 @@ func (h *Handler) UpdateWorkflow(c *gin.Context) { ...@@ -67,7 +67,7 @@ func (h *Handler) UpdateWorkflow(c *gin.Context) {
database.GetDB().Model(&model.WorkflowDefinition{}). database.GetDB().Model(&model.WorkflowDefinition{}).
Where("id = ?", c.Param("id")). Where("id = ?", c.Param("id")).
Updates(updates) Updates(updates)
c.JSON(http.StatusOK, gin.H{"message": "ok"}) response.Success(c, nil)
} }
// PublishWorkflow 发布工作流 // PublishWorkflow 发布工作流
...@@ -75,7 +75,7 @@ func (h *Handler) PublishWorkflow(c *gin.Context) { ...@@ -75,7 +75,7 @@ func (h *Handler) PublishWorkflow(c *gin.Context) {
database.GetDB().Model(&model.WorkflowDefinition{}). database.GetDB().Model(&model.WorkflowDefinition{}).
Where("id = ?", c.Param("id")). Where("id = ?", c.Param("id")).
Update("status", "active") Update("status", "active")
c.JSON(http.StatusOK, gin.H{"message": "ok"}) response.Success(c, nil)
} }
// ListWorkflowExecutions 工作流执行记录列表 // ListWorkflowExecutions 工作流执行记录列表
...@@ -107,10 +107,10 @@ func (h *Handler) ListWorkflowExecutions(c *gin.Context) { ...@@ -107,10 +107,10 @@ func (h *Handler) ListWorkflowExecutions(c *gin.Context) {
Limit(pageSize). Limit(pageSize).
Find(&executions) Find(&executions)
c.JSON(http.StatusOK, gin.H{"data": gin.H{ response.Success(c, gin.H{
"list": executions, "list": executions,
"total": total, "total": total,
"page": page, "page": page,
"page_size": pageSize, "page_size": pageSize,
}}) })
} }
...@@ -131,7 +131,7 @@ func (s *Service) GetAIRenewalAdvice(ctx context.Context, userID, renewalID stri ...@@ -131,7 +131,7 @@ func (s *Service) GetAIRenewalAdvice(ctx context.Context, userID, renewalID stri
msg := fmt.Sprintf("患者%s申请续药%d个月,当前用药:%s,原因:%s。请评估续药合理性并给出专业建议。", msg := fmt.Sprintf("患者%s申请续药%d个月,当前用药:%s,原因:%s。请评估续药合理性并给出专业建议。",
r.DiseaseName, durationMonths, r.Medicines, r.Reason) r.DiseaseName, durationMonths, r.Medicines, r.Reason)
output, err := internalagent.GetService().Chat(ctx, "patient_universal_agent", userID, "", msg, agentCtx) output, err := internalagent.GetService().Chat(ctx, "patient_universal_agent", userID, "patient", "", msg, agentCtx)
if err != nil { if err != nil {
return "", fmt.Errorf("AI续药建议获取失败: %w", err) return "", fmt.Errorf("AI续药建议获取失败: %w", err)
} }
......
...@@ -380,7 +380,7 @@ func (s *Service) AIAssist(ctx context.Context, consultID string, scene string) ...@@ -380,7 +380,7 @@ func (s *Service) AIAssist(ctx context.Context, consultID string, scene string)
} }
agentSvc := internalagent.GetService() agentSvc := internalagent.GetService()
output, err := agentSvc.Chat(ctx, agentID, consult.DoctorID, "", message, agentCtx) output, err := agentSvc.Chat(ctx, agentID, consult.DoctorID, "doctor", "", message, agentCtx)
if err != nil { if err != nil {
return nil, fmt.Errorf("AI分析失败: %w", err) return nil, fmt.Errorf("AI分析失败: %w", err)
} }
......
...@@ -224,7 +224,7 @@ func (s *Service) CheckPrescriptionSafety(ctx context.Context, userID, patientID ...@@ -224,7 +224,7 @@ func (s *Service) CheckPrescriptionSafety(ctx context.Context, userID, patientID
message := fmt.Sprintf("请审核以下处方的安全性:%s,检查药物相互作用和禁忌症", drugList) message := fmt.Sprintf("请审核以下处方的安全性:%s,检查药物相互作用和禁忌症", drugList)
agentSvc := internalagent.GetService() agentSvc := internalagent.GetService()
output, err := agentSvc.Chat(ctx, "doctor_universal_agent", userID, "", message, agentCtx) output, err := agentSvc.Chat(ctx, "doctor_universal_agent", userID, "doctor", "", message, agentCtx)
if err != nil { if err != nil {
return nil, fmt.Errorf("处方安全审核失败: %w", err) return nil, fmt.Errorf("处方安全审核失败: %w", err)
} }
......
...@@ -201,6 +201,7 @@ func (h *Handler) AIGenerateReplies(c *gin.Context) { ...@@ -201,6 +201,7 @@ func (h *Handler) AIGenerateReplies(c *gin.Context) {
c.Request.Context(), c.Request.Context(),
"doctor_universal_agent", "doctor_universal_agent",
userID.(string), userID.(string),
"doctor",
"", "",
"请根据以上对话上下文,生成3-5条医生可以使用的快捷回复建议。每条回复独立一行,不要编号,直接输出回复内容。回复应该专业、简洁、有针对性。", "请根据以上对话上下文,生成3-5条医生可以使用的快捷回复建议。每条回复独立一行,不要编号,直接输出回复内容。回复应该专业、简洁、有针对性。",
agentCtx, agentCtx,
......
package knowledgesvc package knowledgesvc
import ( import (
"net/http"
"strings" "strings"
"internet-hospital/internal/model" "internet-hospital/internal/model"
"internet-hospital/pkg/database" "internet-hospital/pkg/database"
"internet-hospital/pkg/response"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
...@@ -28,12 +28,12 @@ func (h *Handler) RegisterRoutes(r gin.IRouter) { ...@@ -28,12 +28,12 @@ func (h *Handler) RegisterRoutes(r gin.IRouter) {
func (h *Handler) Search(c *gin.Context) { func (h *Handler) Search(c *gin.Context) {
var req struct { var req struct {
Query string `json:"query" binding:"required"` Query string `json:"query" binding:"required"`
CollectionID string `json:"collection_id"` CollectionID string `json:"collection_id"`
TopK int `json:"top_k"` TopK int `json:"top_k"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) response.BadRequest(c, err.Error())
return return
} }
if req.TopK <= 0 { if req.TopK <= 0 {
...@@ -57,13 +57,13 @@ func (h *Handler) Search(c *gin.Context) { ...@@ -57,13 +57,13 @@ func (h *Handler) Search(c *gin.Context) {
"document_id": ch.DocumentID, "document_id": ch.DocumentID,
}) })
} }
c.JSON(http.StatusOK, gin.H{"data": results}) response.Success(c, results)
} }
func (h *Handler) ListCollections(c *gin.Context) { func (h *Handler) ListCollections(c *gin.Context) {
var cols []model.KnowledgeCollection var cols []model.KnowledgeCollection
database.GetDB().Find(&cols) database.GetDB().Find(&cols)
c.JSON(http.StatusOK, gin.H{"data": cols}) response.Success(c, cols)
} }
func (h *Handler) CreateCollection(c *gin.Context) { func (h *Handler) CreateCollection(c *gin.Context) {
...@@ -73,7 +73,7 @@ func (h *Handler) CreateCollection(c *gin.Context) { ...@@ -73,7 +73,7 @@ func (h *Handler) CreateCollection(c *gin.Context) {
Category string `json:"category"` Category string `json:"category"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) response.BadRequest(c, err.Error())
return return
} }
col := model.KnowledgeCollection{ col := model.KnowledgeCollection{
...@@ -83,7 +83,7 @@ func (h *Handler) CreateCollection(c *gin.Context) { ...@@ -83,7 +83,7 @@ func (h *Handler) CreateCollection(c *gin.Context) {
Category: req.Category, Category: req.Category,
} }
database.GetDB().Create(&col) database.GetDB().Create(&col)
c.JSON(http.StatusOK, gin.H{"data": col}) response.Success(c, col)
} }
func (h *Handler) CreateDocument(c *gin.Context) { func (h *Handler) CreateDocument(c *gin.Context) {
...@@ -94,7 +94,7 @@ func (h *Handler) CreateDocument(c *gin.Context) { ...@@ -94,7 +94,7 @@ func (h *Handler) CreateDocument(c *gin.Context) {
FileType string `json:"file_type"` FileType string `json:"file_type"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) response.BadRequest(c, err.Error())
return return
} }
...@@ -128,7 +128,7 @@ func (h *Handler) CreateDocument(c *gin.Context) { ...@@ -128,7 +128,7 @@ func (h *Handler) CreateDocument(c *gin.Context) {
Where("collection_id = ?", req.CollectionID). Where("collection_id = ?", req.CollectionID).
UpdateColumn("document_count", db.Raw("document_count + 1")) UpdateColumn("document_count", db.Raw("document_count + 1"))
c.JSON(http.StatusOK, gin.H{"data": doc}) response.Success(c, doc)
} }
func (h *Handler) ListDocuments(c *gin.Context) { func (h *Handler) ListDocuments(c *gin.Context) {
...@@ -139,20 +139,19 @@ func (h *Handler) ListDocuments(c *gin.Context) { ...@@ -139,20 +139,19 @@ func (h *Handler) ListDocuments(c *gin.Context) {
q = q.Where("collection_id = ?", collectionID) q = q.Where("collection_id = ?", collectionID)
} }
q.Find(&docs) q.Find(&docs)
c.JSON(http.StatusOK, gin.H{"data": docs}) response.Success(c, docs)
} }
func (h *Handler) DeleteCollection(c *gin.Context) { func (h *Handler) DeleteCollection(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
db := database.GetDB() db := database.GetDB()
// Delete chunks, documents, then collection
db.Where("collection_id IN (SELECT document_id FROM knowledge_documents WHERE collection_id = ?)", id).Delete(&model.KnowledgeChunk{}) db.Where("collection_id IN (SELECT document_id FROM knowledge_documents WHERE collection_id = ?)", id).Delete(&model.KnowledgeChunk{})
db.Where("collection_id = ?", id).Delete(&model.KnowledgeDocument{}) db.Where("collection_id = ?", id).Delete(&model.KnowledgeDocument{})
if err := db.Where("collection_id = ?", id).Delete(&model.KnowledgeCollection{}).Error; err != nil { if err := db.Where("collection_id = ?", id).Delete(&model.KnowledgeCollection{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) response.Error(c, 500, "删除失败")
return return
} }
c.JSON(http.StatusOK, gin.H{"data": nil, "message": "ok"}) response.Success(c, nil)
} }
func (h *Handler) DeleteDocument(c *gin.Context) { func (h *Handler) DeleteDocument(c *gin.Context) {
...@@ -160,10 +159,10 @@ func (h *Handler) DeleteDocument(c *gin.Context) { ...@@ -160,10 +159,10 @@ func (h *Handler) DeleteDocument(c *gin.Context) {
db := database.GetDB() db := database.GetDB()
db.Where("document_id = ?", id).Delete(&model.KnowledgeChunk{}) db.Where("document_id = ?", id).Delete(&model.KnowledgeChunk{})
if err := db.Where("document_id = ?", id).Delete(&model.KnowledgeDocument{}).Error; err != nil { if err := db.Where("document_id = ?", id).Delete(&model.KnowledgeDocument{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) response.Error(c, 500, "删除失败")
return return
} }
c.JSON(http.StatusOK, gin.H{"data": nil, "message": "ok"}) response.Success(c, nil)
} }
func splitIntoChunks(text string, chunkSize int) []string { func splitIntoChunks(text string, chunkSize int) []string {
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
"strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
...@@ -195,7 +196,7 @@ type UserProfileResponse struct { ...@@ -195,7 +196,7 @@ type UserProfileResponse struct {
Menus []model.Menu `json:"menus"` Menus []model.Menu `json:"menus"`
} }
// GetUserProfile 获取用户完整信息(包含基于 RBAC 的菜单树) // GetUserProfile 获取用户完整信息(包含菜单树)
func (s *Service) GetUserProfile(ctx context.Context, userID string) (*UserProfileResponse, error) { func (s *Service) GetUserProfile(ctx context.Context, userID string) (*UserProfileResponse, error) {
var user model.User var user model.User
if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil { if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil {
...@@ -210,27 +211,79 @@ func (s *Service) GetUserProfile(ctx context.Context, userID string) (*UserProfi ...@@ -210,27 +211,79 @@ func (s *Service) GetUserProfile(ctx context.Context, userID string) (*UserProfi
}, nil }, nil
} }
// getUserMenus 基于 RBAC 查询用户的菜单树 // getUserMenus 基于 User.Role 返回对应端菜单树
// 流程:user_roles → role_menus → menus → 构建树 // admin → /admin/* , patient → /patient/* , doctor → /doctor/*
func (s *Service) getUserMenus(userID string) []model.Menu { func (s *Service) getUserMenus(userID string) []model.Menu {
// 通过 user_roles + role_menus 获取该用户可见的菜单 ID var user model.User
var menuIDs []uint if err := s.db.Select("role").Where("id = ?", userID).First(&user).Error; err != nil {
s.db.Raw(`SELECT DISTINCT rm.menu_id FROM role_menus rm fmt.Printf("[getUserMenus] 查询用户失败: %v\n", err)
INNER JOIN user_roles ur ON ur.role_id = rm.role_id return nil
WHERE ur.user_id = ?`, userID).Scan(&menuIDs) }
prefix := rolePathPrefix(user.Role)
fmt.Printf("[getUserMenus] userID=%s role=%s prefix=%s\n", userID, user.Role, prefix)
if prefix == "" {
return []model.Menu{}
}
var flatMenus []model.Menu var flatMenus []model.Menu
if len(menuIDs) > 0 { s.db.Where("visible = ? AND status = ?", true, "active").
// RBAC 模式:只返回角色关联的菜单 Order("sort ASC, id ASC").Find(&flatMenus)
s.db.Where("id IN ? AND visible = ? AND status = ?", menuIDs, true, "active"). fmt.Printf("[getUserMenus] 查询到 %d 条可见菜单\n", len(flatMenus))
Order("sort ASC, id ASC").Find(&flatMenus)
} else { filtered := filterMenusByPrefix(flatMenus, prefix)
// 兼容模式:用户没有角色绑定时返回全部可见菜单 fmt.Printf("[getUserMenus] 过滤后 %d 条菜单 (prefix=%s)\n", len(filtered), prefix)
s.db.Where("visible = ? AND status = ?", true, "active").
Order("sort ASC, id ASC").Find(&flatMenus) tree := buildMenuTree(filtered, 0)
fmt.Printf("[getUserMenus] 构建菜单树 %d 个顶级节点\n", len(tree))
return tree
}
// rolePathPrefix 返回角色对应的路由前缀
func rolePathPrefix(role string) string {
switch role {
case "admin":
return "/admin/"
case "patient":
return "/patient/"
case "doctor":
return "/doctor/"
default:
return ""
} }
}
return buildMenuTree(flatMenus, 0) // filterMenusByPrefix 筛选属于指定路径前缀的菜单(包含父级分组节点)
func filterMenusByPrefix(menus []model.Menu, prefix string) []model.Menu {
matchIDs := map[uint]bool{}
parentIDs := map[uint]bool{}
for _, m := range menus {
if m.Path != "" && strings.HasPrefix(m.Path, prefix) {
matchIDs[m.ID] = true
if m.ParentID != 0 {
parentIDs[m.ParentID] = true
}
}
}
for changed := true; changed; {
changed = false
for _, m := range menus {
if parentIDs[m.ID] && !matchIDs[m.ID] {
matchIDs[m.ID] = true
if m.ParentID != 0 {
parentIDs[m.ParentID] = true
}
changed = true
}
}
}
var result []model.Menu
for _, m := range menus {
if matchIDs[m.ID] {
result = append(result, m)
}
}
return result
} }
// buildMenuTree 将扁平菜单列表构建为树形结构 // buildMenuTree 将扁平菜单列表构建为树形结构
......
...@@ -2,10 +2,10 @@ package workflowsvc ...@@ -2,10 +2,10 @@ package workflowsvc
import ( import (
"encoding/json" "encoding/json"
"net/http"
"internet-hospital/internal/model" "internet-hospital/internal/model"
"internet-hospital/pkg/database" "internet-hospital/pkg/database"
"internet-hospital/pkg/response"
"internet-hospital/pkg/workflow" "internet-hospital/pkg/workflow"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
...@@ -35,25 +35,25 @@ func (h *Handler) Execute(c *gin.Context) { ...@@ -35,25 +35,25 @@ func (h *Handler) Execute(c *gin.Context) {
execID, err := workflow.GetEngine().Execute(c.Request.Context(), workflowID, input, uid) execID, err := workflow.GetEngine().Execute(c.Request.Context(), workflowID, input, uid)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) response.Error(c, 500, err.Error())
return return
} }
c.JSON(http.StatusOK, gin.H{"data": gin.H{"execution_id": execID}}) response.Success(c, gin.H{"execution_id": execID})
} }
func (h *Handler) GetExecution(c *gin.Context) { func (h *Handler) GetExecution(c *gin.Context) {
var exec model.WorkflowExecution var exec model.WorkflowExecution
if err := database.GetDB().Where("execution_id = ?", c.Param("id")).First(&exec).Error; err != nil { if err := database.GetDB().Where("execution_id = ?", c.Param("id")).First(&exec).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) response.Error(c, 404, "not found")
return return
} }
c.JSON(http.StatusOK, gin.H{"data": exec}) response.Success(c, exec)
} }
func (h *Handler) ListTasks(c *gin.Context) { func (h *Handler) ListTasks(c *gin.Context) {
var tasks []model.WorkflowHumanTask var tasks []model.WorkflowHumanTask
database.GetDB().Where("status = 'pending'").Find(&tasks) database.GetDB().Where("status = 'pending'").Find(&tasks)
c.JSON(http.StatusOK, gin.H{"data": tasks}) response.Success(c, tasks)
} }
func (h *Handler) CompleteTask(c *gin.Context) { func (h *Handler) CompleteTask(c *gin.Context) {
...@@ -65,16 +65,16 @@ func (h *Handler) CompleteTask(c *gin.Context) { ...@@ -65,16 +65,16 @@ func (h *Handler) CompleteTask(c *gin.Context) {
database.GetDB().Model(&model.WorkflowHumanTask{}). database.GetDB().Model(&model.WorkflowHumanTask{}).
Where("task_id = ?", c.Param("id")). Where("task_id = ?", c.Param("id")).
Updates(map[string]interface{}{"status": "completed", "result": string(resultJSON)}) Updates(map[string]interface{}{"status": "completed", "result": string(resultJSON)})
c.JSON(http.StatusOK, gin.H{"message": "ok"}) response.Success(c, nil)
} }
func (h *Handler) DeleteWorkflow(c *gin.Context) { func (h *Handler) DeleteWorkflow(c *gin.Context) {
id := c.Param("workflow_id") id := c.Param("workflow_id")
if err := database.GetDB().Where("workflow_id = ?", id).Delete(&model.WorkflowDefinition{}).Error; err != nil { if err := database.GetDB().Where("workflow_id = ?", id).Delete(&model.WorkflowDefinition{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) response.Error(c, 500, "删除失败")
return return
} }
c.JSON(http.StatusOK, gin.H{"message": "ok"}) response.Success(c, nil)
} }
func (h *Handler) UpdateWorkflowStatus(c *gin.Context) { func (h *Handler) UpdateWorkflowStatus(c *gin.Context) {
...@@ -83,12 +83,12 @@ func (h *Handler) UpdateWorkflowStatus(c *gin.Context) { ...@@ -83,12 +83,12 @@ func (h *Handler) UpdateWorkflowStatus(c *gin.Context) {
Status string `json:"status" binding:"required"` Status string `json:"status" binding:"required"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) response.BadRequest(c, "请求参数错误")
return return
} }
if err := database.GetDB().Model(&model.WorkflowDefinition{}).Where("workflow_id = ?", id).Update("status", req.Status).Error; err != nil { if err := database.GetDB().Model(&model.WorkflowDefinition{}).Where("workflow_id = ?", id).Update("status", req.Status).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"}) response.Error(c, 500, "更新失败")
return return
} }
c.JSON(http.StatusOK, gin.H{"message": "ok"}) response.Success(c, nil)
} }
...@@ -14,7 +14,8 @@ import ( ...@@ -14,7 +14,8 @@ import (
// contextKey 避免 context key 冲突 // contextKey 避免 context key 冲突
type contextKey string type contextKey string
const ContextKeyUserID contextKey = "user_id" const ContextKeyUserID contextKey = "user_id"
const ContextKeyUserRole contextKey = "user_role"
// StreamEventType SSE 事件类型 // StreamEventType SSE 事件类型
type StreamEventType string type StreamEventType string
...@@ -36,6 +37,7 @@ type StreamEvent struct { ...@@ -36,6 +37,7 @@ type StreamEvent struct {
type AgentInput struct { type AgentInput struct {
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
UserRole string `json:"user_role"`
Message string `json:"message"` Message string `json:"message"`
Context map[string]interface{} `json:"context"` Context map[string]interface{} `json:"context"`
History []ai.ChatMessage `json:"history"` History []ai.ChatMessage `json:"history"`
...@@ -123,10 +125,13 @@ func (a *ReActAgent) Description() string { return a.cfg.Description } ...@@ -123,10 +125,13 @@ func (a *ReActAgent) Description() string { return a.cfg.Description }
// Run 执行Agent(非流式) // Run 执行Agent(非流式)
func (a *ReActAgent) Run(ctx context.Context, input AgentInput) (*AgentOutput, error) { func (a *ReActAgent) Run(ctx context.Context, input AgentInput) (*AgentOutput, error) {
// v15: 将 userID 注入 context,供工具(如 navigate_page)做权限校验 // 将 userID / userRole 注入 context,供工具(如 navigate_page)做权限校验
if input.UserID != "" { if input.UserID != "" {
ctx = context.WithValue(ctx, ContextKeyUserID, input.UserID) ctx = context.WithValue(ctx, ContextKeyUserID, input.UserID)
} }
if input.UserRole != "" {
ctx = context.WithValue(ctx, ContextKeyUserRole, input.UserRole)
}
// 生成链路追踪ID(如果未提供则新建) // 生成链路追踪ID(如果未提供则新建)
traceID := input.TraceID traceID := input.TraceID
if traceID == "" { if traceID == "" {
...@@ -234,10 +239,13 @@ func (a *ReActAgent) Run(ctx context.Context, input AgentInput) (*AgentOutput, e ...@@ -234,10 +239,13 @@ func (a *ReActAgent) Run(ctx context.Context, input AgentInput) (*AgentOutput, e
// RunStream 流式执行Agent,通过 onEvent 实时推送中间过程 // RunStream 流式执行Agent,通过 onEvent 实时推送中间过程
// v12改造:最终文本回答使用真流式(ChatWithToolsStream),工具调用阶段仍用非流式。 // v12改造:最终文本回答使用真流式(ChatWithToolsStream),工具调用阶段仍用非流式。
func (a *ReActAgent) RunStream(ctx context.Context, input AgentInput, onEvent func(StreamEvent) error) (*AgentOutput, error) { func (a *ReActAgent) RunStream(ctx context.Context, input AgentInput, onEvent func(StreamEvent) error) (*AgentOutput, error) {
// v15: 将 userID 注入 context,供工具(如 navigate_page)做权限校验 // 将 userID / userRole 注入 context,供工具(如 navigate_page)做权限校验
if input.UserID != "" { if input.UserID != "" {
ctx = context.WithValue(ctx, ContextKeyUserID, input.UserID) ctx = context.WithValue(ctx, ContextKeyUserID, input.UserID)
} }
if input.UserRole != "" {
ctx = context.WithValue(ctx, ContextKeyUserRole, input.UserRole)
}
traceID := input.TraceID traceID := input.TraceID
if traceID == "" { if traceID == "" {
traceID = uuid.New().String() traceID = uuid.New().String()
......
...@@ -10,8 +10,6 @@ import ( ...@@ -10,8 +10,6 @@ import (
"internet-hospital/internal/model" "internet-hospital/internal/model"
"internet-hospital/pkg/agent" "internet-hospital/pkg/agent"
"internet-hospital/pkg/database" "internet-hospital/pkg/database"
"gorm.io/gorm"
) )
// menuEntry 缓存的菜单条目(从 menus 表加载) // menuEntry 缓存的菜单条目(从 menus 表加载)
...@@ -155,18 +153,15 @@ func (t *NavigatePageTool) Execute(ctx context.Context, params map[string]interf ...@@ -155,18 +153,15 @@ func (t *NavigatePageTool) Execute(ctx context.Context, params map[string]interf
} }
} }
// RBAC 权限校验 // 基于 User.Role 的权限校验
if userID, ok := ctx.Value(agent.ContextKeyUserID).(string); ok && userID != "" { userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
db := database.GetDB() if allowed, reason := checkPagePermission(userRole, normalizedCode, entry); !allowed {
allowed, reason := checkPagePermission(db, userID, normalizedCode, entry) return map[string]interface{}{
if !allowed { "action": "permission_denied",
return map[string]interface{}{ "page": normalizedCode,
"action": "permission_denied", "page_name": entry.Name,
"page": normalizedCode, "message": reason,
"page_name": entry.Name, }, nil
"message": reason,
}, nil
}
} }
// 根据 operation 构建最终路由 // 根据 operation 构建最终路由
...@@ -194,43 +189,38 @@ func (t *NavigatePageTool) Execute(ctx context.Context, params map[string]interf ...@@ -194,43 +189,38 @@ func (t *NavigatePageTool) Execute(ctx context.Context, params map[string]interf
}, nil }, nil
} }
// checkPagePermission 基于菜单的权限字段做 RBAC 校验 // checkPagePermission 基于 User.Role 的页面导航权限校验
func checkPagePermission(db *gorm.DB, userID, pageCode string, entry menuEntry) (bool, string) { // 规则:
if db == nil || userID == "" { // - admin → 可访问所有页面(admin_*、patient_*、doctor_*)
return true, "" // - patient → 仅可访问 patient_* 页面
// - doctor → 仅可访问 doctor_* 页面
// - 未知角色 → 拒绝所有
func checkPagePermission(userRole, pageCode string, entry menuEntry) (bool, string) {
if userRole == "" {
return false, fmt.Sprintf("未识别您的角色,无法访问「%s」", entry.Name)
} }
// 非 admin 页面不做额外权限校验(依赖 JWT 角色即可) // admin 拥有全部页面访问权限
if !strings.HasPrefix(pageCode, "admin_") { if userRole == "admin" {
return true, "" return true, ""
} }
// admin 角色拥有所有权限 // 判断页面所属端:admin_* / patient_* / doctor_*
var adminCount int64 switch {
db.Table("user_roles"). case strings.HasPrefix(pageCode, "admin_"):
Joins("JOIN roles ON roles.id = user_roles.role_id"). return false, fmt.Sprintf("您没有访问管理端「%s」页面的权限", entry.Name)
Where("user_roles.user_id = ? AND roles.code = 'admin' AND roles.status = 'active'", userID). case strings.HasPrefix(pageCode, "patient_"):
Count(&adminCount) if userRole == "patient" {
if adminCount > 0 { return true, ""
return true, "" }
} return false, fmt.Sprintf("该页面仅限患者访问", )
case strings.HasPrefix(pageCode, "doctor_"):
// 如果菜单没有配置权限码,放行 if userRole == "doctor" {
if entry.Permission == "" { return true, ""
return true, "" }
} return false, fmt.Sprintf("该页面仅限医生访问")
default:
// 检查用户是否拥有该权限 // 无明确前缀的页面(如果有),放行
var permCount int64
db.Table("permissions").
Joins("JOIN role_permissions ON role_permissions.permission_id = permissions.id").
Joins("JOIN user_roles ON user_roles.role_id = role_permissions.role_id").
Where("user_roles.user_id = ? AND permissions.code = ?", userID, entry.Permission).
Count(&permCount)
if permCount > 0 {
return true, "" return true, ""
} }
return false, fmt.Sprintf("您没有访问「%s」页面的权限,请联系管理员", entry.Name)
} }
// Deprecated: 本文件中的 RequirePermission 中间件基于 RBAC 表(roles/permissions/
// user_roles/role_permissions),但系统已简化为以 User.Role 作为唯一权限依据。
// 当前无任何路由挂载此中间件,保留文件仅供参考,请勿在新代码中使用。
package middleware package middleware
import ( import (
......
...@@ -70,7 +70,7 @@ type ExecutionContext struct { ...@@ -70,7 +70,7 @@ type ExecutionContext struct {
// Engine 工作流引擎 // Engine 工作流引擎
type Engine struct { type Engine struct {
agentSvc interface { agentSvc interface {
Chat(ctx context.Context, agentID, userID, sessionID, message string, ctxData map[string]interface{}) (interface{}, error) Chat(ctx context.Context, agentID, userID, userRole, sessionID, message string, ctxData map[string]interface{}) (interface{}, error)
} }
toolRegistry *agentpkg.ToolRegistry toolRegistry *agentpkg.ToolRegistry
} }
...@@ -85,7 +85,7 @@ func GetEngine() *Engine { ...@@ -85,7 +85,7 @@ func GetEngine() *Engine {
} }
func SetAgentService(svc interface { func SetAgentService(svc interface {
Chat(ctx context.Context, agentID, userID, sessionID, message string, ctxData map[string]interface{}) (interface{}, error) Chat(ctx context.Context, agentID, userID, userRole, sessionID, message string, ctxData map[string]interface{}) (interface{}, error)
}) { }) {
GetEngine().agentSvc = svc GetEngine().agentSvc = svc
} }
...@@ -226,7 +226,8 @@ func (e *Engine) executeAgent(ctx context.Context, node *Node, execCtx *Executio ...@@ -226,7 +226,8 @@ func (e *Engine) executeAgent(ctx context.Context, node *Node, execCtx *Executio
agentID, _ := node.Config["agent_id"].(string) agentID, _ := node.Config["agent_id"].(string)
message, _ := execCtx.Variables["message"].(string) message, _ := execCtx.Variables["message"].(string)
userID, _ := execCtx.Variables["user_id"].(string) userID, _ := execCtx.Variables["user_id"].(string)
return e.agentSvc.Chat(ctx, agentID, userID, execCtx.ExecutionID, message, execCtx.Variables) userRole, _ := execCtx.Variables["user_role"].(string)
return e.agentSvc.Chat(ctx, agentID, userID, userRole, execCtx.ExecutionID, message, execCtx.Variables)
} }
func (e *Engine) executeCondition(_ context.Context, node *Node, execCtx *ExecutionContext) (interface{}, error) { func (e *Engine) executeCondition(_ context.Context, node *Node, execCtx *ExecutionContext) (interface{}, error) {
......
import { get, post, put, del } from './request'; import { get, put, del, post } from './request';
// ==================== Types ==================== // ==================== Types ====================
...@@ -14,16 +14,6 @@ export interface Role { ...@@ -14,16 +14,6 @@ export interface Role {
updated_at: string; updated_at: string;
} }
export interface Permission {
id: number;
code: string;
name: string;
module: string;
action: string;
description: string;
created_at: string;
}
export interface Menu { export interface Menu {
id: number; id: number;
parent_id: number; parent_id: number;
...@@ -41,28 +31,19 @@ export interface Menu { ...@@ -41,28 +31,19 @@ export interface Menu {
children?: Menu[]; children?: Menu[];
} }
// ==================== Role API ==================== // ==================== Role API (read-only) ====================
export const roleApi = { export const roleApi = {
list: () => get<Role[]>('/admin/roles'), list: () => get<Role[]>('/admin/roles'),
create: (data: Partial<Role>) => post<Role>('/admin/roles', data), getMenus: (roleId: number) => get<{ menu_ids: number[] }>(`/admin/roles/${roleId}/menus`),
update: (id: number, data: Partial<Role>) => put<Role>(`/admin/roles/${id}`, data), setMenus: (roleId: number, menuIds: number[]) => put<null>(`/admin/roles/${roleId}/menus`, { menu_ids: menuIds }),
delete: (id: number) => del<null>(`/admin/roles/${id}`),
getPermissions: (id: number) => get<Permission[]>(`/admin/roles/${id}/permissions`),
setPermissions: (id: number, permissionIds: number[]) =>
put<null>(`/admin/roles/${id}/permissions`, { permission_ids: permissionIds }),
getMenus: (id: number) => get<number[]>(`/admin/roles/${id}/menus`),
setMenus: (id: number, menuIds: number[]) =>
put<null>(`/admin/roles/${id}/menus`, { menu_ids: menuIds }),
}; };
// ==================== Permission API ==================== // ==================== User Role API ====================
export const permissionApi = { export const userRoleApi = {
list: () => get<Permission[]>('/admin/permissions'), setUserRole: (userId: number | string, role: 'patient' | 'doctor' | 'admin') =>
create: (data: Partial<Permission>) => post<Permission>('/admin/permissions', data), put<null>(`/admin/users/${userId}/role`, { role }),
}; };
// ==================== Menu API ==================== // ==================== Menu API ====================
...@@ -73,15 +54,6 @@ export const menuApi = { ...@@ -73,15 +54,6 @@ export const menuApi = {
create: (data: Partial<Menu>) => post<Menu>('/admin/menus', data), create: (data: Partial<Menu>) => post<Menu>('/admin/menus', data),
update: (id: number, data: Partial<Menu>) => put<Menu>(`/admin/menus/${id}`, data), update: (id: number, data: Partial<Menu>) => put<Menu>(`/admin/menus/${id}`, data),
delete: (id: number) => del<null>(`/admin/menus/${id}`), delete: (id: number) => del<null>(`/admin/menus/${id}`),
reseed: () => post<{ message: string }>('/admin/menus/reseed', {}),
};
// ==================== User Role API ====================
export const userRoleApi = {
getUserRoles: (userId: number) => get<Role[]>(`/admin/users/${userId}/roles`),
setUserRoles: (userId: number, roleIds: number[]) =>
put<null>(`/admin/users/${userId}/roles`, { role_ids: roleIds }),
}; };
// ==================== My Menus (current user) ==================== // ==================== My Menus (current user) ====================
......
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore'; import { useUserStore } from '@/store/userStore';
import type { Menu as MenuType } from '@/api/rbac'; import type { Menu as MenuType } from '@/api/rbac';
import { myMenuApi } from '@/api/rbac';
import { notificationApi, type Notification } from '@/api/notification'; import { notificationApi, type Notification } from '@/api/notification';
const { Sider, Content } = Layout; const { Sider, Content } = Layout;
...@@ -102,12 +103,20 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) { ...@@ -102,12 +103,20 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const isEmbed = searchParams?.get('embed') === '1'; const isEmbed = searchParams?.get('embed') === '1';
const { user, logout, menus } = useUserStore(); const { user, logout, menus, setMenus } = useUserStore();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const [notifications, setNotifications] = useState<Notification[]>([]); const [notifications, setNotifications] = useState<Notification[]>([]);
const currentPath = pathname || ''; const currentPath = pathname || '';
// 菜单为空时主动拉取
useEffect(() => {
if (!user || menus.length > 0) return;
myMenuApi.getMenus().then(res => {
if (res.data && res.data.length > 0) setMenus(res.data);
}).catch(() => {});
}, [user, menus.length, setMenus]);
useEffect(() => { useEffect(() => {
if (!user) return; if (!user) return;
const fetchUnread = () => { const fetchUnread = () => {
......
This diff is collapsed.
This diff is collapsed.
'use client'; 'use client';
import React, { Suspense, useState, useEffect } from 'react'; import React, { Suspense, useState, useEffect, useMemo, useCallback } from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Badge, Space, Switch, Typography, Tag, Popover, List } from 'antd'; import { Layout, Menu, Avatar, Dropdown, Badge, Space, Switch, Typography, Tag, Popover, List } from 'antd';
import { import {
...@@ -8,24 +8,78 @@ import { ...@@ -8,24 +8,78 @@ import {
SettingOutlined, LogoutOutlined, BellOutlined, MedicineBoxOutlined, SettingOutlined, LogoutOutlined, BellOutlined, MedicineBoxOutlined,
DollarOutlined, FolderOpenOutlined, SafetyCertificateOutlined, DollarOutlined, FolderOpenOutlined, SafetyCertificateOutlined,
CheckCircleOutlined, MenuFoldOutlined, MenuUnfoldOutlined, CheckCircleOutlined, MenuFoldOutlined, MenuUnfoldOutlined,
HomeOutlined, FileTextOutlined, HeartOutlined, VideoCameraOutlined,
PayCircleOutlined, ShoppingCartOutlined, SearchOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore'; import { useUserStore } from '@/store/userStore';
import type { Menu as MenuType } from '@/api/rbac';
import { myMenuApi } from '@/api/rbac';
import { doctorPortalApi } from '@/api/doctorPortal'; import { doctorPortalApi } from '@/api/doctorPortal';
import { notificationApi, type Notification } from '@/api/notification'; import { notificationApi, type Notification } from '@/api/notification';
const { Sider, Content } = Layout; const { Sider, Content } = Layout;
const { Text } = Typography; const { Text } = Typography;
const menuItems = [ const ICON_MAP: Record<string, React.ReactNode> = {
{ key: '/doctor/workbench', icon: <DashboardOutlined />, label: '工作台' }, DashboardOutlined: <DashboardOutlined />,
{ key: '/doctor/consult', icon: <MessageOutlined />, label: '问诊大厅' }, UserOutlined: <UserOutlined />,
{ key: '/doctor/tasks', icon: <CheckCircleOutlined />, label: '待办任务' }, MessageOutlined: <MessageOutlined />,
{ key: '/doctor/chronic/review', icon: <SafetyCertificateOutlined />, label: '慢病续方' }, CalendarOutlined: <CalendarOutlined />,
{ key: '/doctor/patient', icon: <FolderOpenOutlined />, label: '患者档案' }, MedicineBoxOutlined: <MedicineBoxOutlined />,
{ key: '/doctor/schedule', icon: <CalendarOutlined />, label: '排班管理' }, DollarOutlined: <DollarOutlined />,
{ key: '/doctor/income', icon: <DollarOutlined />, label: '收入统计' }, FolderOpenOutlined: <FolderOpenOutlined />,
{ key: '/doctor/profile', icon: <UserOutlined />, label: '个人信息' }, SafetyCertificateOutlined: <SafetyCertificateOutlined />,
]; CheckCircleOutlined: <CheckCircleOutlined />,
HomeOutlined: <HomeOutlined />,
FileTextOutlined: <FileTextOutlined />,
HeartOutlined: <HeartOutlined />,
VideoCameraOutlined: <VideoCameraOutlined />,
PayCircleOutlined: <PayCircleOutlined />,
ShoppingCartOutlined: <ShoppingCartOutlined />,
SearchOutlined: <SearchOutlined />,
SettingOutlined: <SettingOutlined />,
BellOutlined: <BellOutlined />,
};
function convertMenuTree(menus: MenuType[]): any[] {
return menus.map(m => {
const item: any = {
key: m.path || `menu-${m.id}`,
label: m.name,
icon: ICON_MAP[m.icon] || null,
};
if (m.children && m.children.length > 0) {
item.children = convertMenuTree(m.children);
}
return item;
});
}
function collectLeafPaths(menus: MenuType[]): string[] {
const paths: string[] = [];
for (const m of menus) {
if (m.children && m.children.length > 0) {
paths.push(...collectLeafPaths(m.children));
} else if (m.path) {
paths.push(m.path);
}
}
return paths;
}
function findOpenKeys(menus: MenuType[], targetPath: string): string[] {
const keys: string[] = [];
for (const m of menus) {
if (m.children && m.children.length > 0) {
const childPaths = collectLeafPaths(m.children);
if (childPaths.some(p => targetPath.startsWith(p))) {
keys.push(m.path || `menu-${m.id}`);
}
keys.push(...findOpenKeys(m.children, targetPath));
}
}
return keys;
}
export default function DoctorLayout({ children }: { children: React.ReactNode }) { export default function DoctorLayout({ children }: { children: React.ReactNode }) {
return ( return (
...@@ -40,13 +94,23 @@ function DoctorLayoutInner({ children }: { children: React.ReactNode }) { ...@@ -40,13 +94,23 @@ function DoctorLayoutInner({ children }: { children: React.ReactNode }) {
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const isEmbed = searchParams?.get('embed') === '1'; const isEmbed = searchParams?.get('embed') === '1';
const { user, logout } = useUserStore(); const { user, logout, menus, setMenus } = useUserStore();
const [isOnline, setIsOnline] = useState(false); const [isOnline, setIsOnline] = useState(false);
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const [notifications, setNotifications] = useState<Notification[]>([]); const [notifications, setNotifications] = useState<Notification[]>([]);
const currentPath = pathname || ''; const currentPath = pathname || '';
const menuItems = useMemo(() => convertMenuTree(menus), [menus]);
// 菜单为空时主动拉取
useEffect(() => {
if (!user || menus.length > 0) return;
myMenuApi.getMenus().then(res => {
if (res.data && res.data.length > 0) setMenus(res.data);
}).catch(() => {});
}, [user, menus.length, setMenus]);
useEffect(() => { useEffect(() => {
if (!user) return; if (!user) return;
const fetchUnread = () => { const fetchUnread = () => {
...@@ -108,10 +172,16 @@ function DoctorLayoutInner({ children }: { children: React.ReactNode }) { ...@@ -108,10 +172,16 @@ function DoctorLayoutInner({ children }: { children: React.ReactNode }) {
else if (key === 'settings') { router.push('/doctor/profile'); } else if (key === 'settings') { router.push('/doctor/profile'); }
}; };
const getSelectedKeys = () => { const getSelectedKeys = useCallback(() => {
const match = menuItems.find(item => currentPath.startsWith(item.key)); const allPaths = collectLeafPaths(menus);
return match ? [match.key] : []; const match = allPaths.find(k => currentPath.startsWith(k));
}; return match ? [match] : [];
}, [menus, currentPath]);
const getOpenKeys = useCallback(() => {
if (collapsed) return [];
return findOpenKeys(menus, currentPath);
}, [menus, currentPath, collapsed]);
if (isEmbed) { if (isEmbed) {
return <div style={{ minHeight: '100vh', background: '#f5f8ff' }}>{children}</div>; return <div style={{ minHeight: '100vh', background: '#f5f8ff' }}>{children}</div>;
...@@ -161,6 +231,7 @@ function DoctorLayoutInner({ children }: { children: React.ReactNode }) { ...@@ -161,6 +231,7 @@ function DoctorLayoutInner({ children }: { children: React.ReactNode }) {
<Menu <Menu
mode="inline" mode="inline"
selectedKeys={getSelectedKeys()} selectedKeys={getSelectedKeys()}
defaultOpenKeys={getOpenKeys()}
items={menuItems} items={menuItems}
onClick={handleMenuClick} onClick={handleMenuClick}
style={{ border: 'none', padding: '8px 0' }} style={{ border: 'none', padding: '8px 0' }}
......
import { Suspense } from 'react';
import { Spin } from 'antd';
import PageComponent from '@/pages/patient/ConsultCreate';
export default function Page() {
return (
<Suspense fallback={<div style={{ textAlign: 'center', padding: 80 }}><Spin size="large" tip="加载中..." /></div>}>
<PageComponent />
</Suspense>
);
}
'use client'; 'use client';
import React, { Suspense, useState, useEffect } from 'react'; import React, { Suspense, useState, useEffect, useMemo, useCallback } from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag, Popover, List } from 'antd'; import { Layout, Menu, Avatar, Dropdown, Button, Badge, Space, Typography, Tag, Popover, List } from 'antd';
import { import {
...@@ -8,46 +8,79 @@ import { ...@@ -8,46 +8,79 @@ import {
HeartOutlined, SettingOutlined, LogoutOutlined, BellOutlined, HeartOutlined, SettingOutlined, LogoutOutlined, BellOutlined,
VideoCameraOutlined, PayCircleOutlined, VideoCameraOutlined, PayCircleOutlined,
ShoppingCartOutlined, FolderOpenOutlined, SearchOutlined, ShoppingCartOutlined, FolderOpenOutlined, SearchOutlined,
MenuFoldOutlined, MenuUnfoldOutlined, CheckOutlined, MenuFoldOutlined, MenuUnfoldOutlined, DashboardOutlined,
MessageOutlined, CalendarOutlined, DollarOutlined,
SafetyCertificateOutlined, CheckCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore'; import { useUserStore } from '@/store/userStore';
import type { Menu as MenuType } from '@/api/rbac';
import { myMenuApi } from '@/api/rbac';
import { notificationApi, type Notification } from '@/api/notification'; import { notificationApi, type Notification } from '@/api/notification';
const { Sider, Content } = Layout; const { Sider, Content } = Layout;
const { Text } = Typography; const { Text } = Typography;
const menuItems = [ // 图标名称 → React 图标组件映射
{ key: '/patient/home', icon: <HomeOutlined />, label: '首页' }, const ICON_MAP: Record<string, React.ReactNode> = {
{ HomeOutlined: <HomeOutlined />,
key: 'consult-services', icon: <VideoCameraOutlined />, label: '问诊服务', UserOutlined: <UserOutlined />,
children: [ MedicineBoxOutlined: <MedicineBoxOutlined />,
{ key: '/patient/doctors', icon: <SearchOutlined />, label: '找医生' }, FileTextOutlined: <FileTextOutlined />,
{ key: '/patient/consult', icon: <VideoCameraOutlined />, label: '我的问诊' }, HeartOutlined: <HeartOutlined />,
], VideoCameraOutlined: <VideoCameraOutlined />,
}, PayCircleOutlined: <PayCircleOutlined />,
{ ShoppingCartOutlined: <ShoppingCartOutlined />,
key: 'rx-pharmacy', icon: <MedicineBoxOutlined />, label: '处方购药', FolderOpenOutlined: <FolderOpenOutlined />,
children: [ SearchOutlined: <SearchOutlined />,
{ key: '/patient/prescription', icon: <FileTextOutlined />, label: '电子处方' }, SettingOutlined: <SettingOutlined />,
{ key: '/patient/pharmacy/order', icon: <ShoppingCartOutlined />, label: '药品配送' }, DashboardOutlined: <DashboardOutlined />,
{ key: '/patient/payment', icon: <PayCircleOutlined />, label: '支付管理' }, MessageOutlined: <MessageOutlined />,
], CalendarOutlined: <CalendarOutlined />,
}, DollarOutlined: <DollarOutlined />,
{ SafetyCertificateOutlined: <SafetyCertificateOutlined />,
key: 'health-mgmt', icon: <HeartOutlined />, label: '健康管理', CheckCircleOutlined: <CheckCircleOutlined />,
children: [ BellOutlined: <BellOutlined />,
{ key: '/patient/chronic', icon: <HeartOutlined />, label: '慢病管理' }, };
{ key: '/patient/health-records', icon: <FolderOpenOutlined />, label: '健康档案' },
],
},
{ key: '/patient/profile', icon: <UserOutlined />, label: '个人中心' },
];
const allRouteKeys = [ function convertMenuTree(menus: MenuType[]): any[] {
'/patient/home', '/patient/doctors', '/patient/consult', return menus.map(m => {
'/patient/prescription', '/patient/pharmacy/order', '/patient/payment', const item: any = {
'/patient/chronic', '/patient/health-records', '/patient/profile', key: m.path || `menu-${m.id}`,
]; label: m.name,
icon: ICON_MAP[m.icon] || null,
};
if (m.children && m.children.length > 0) {
item.children = convertMenuTree(m.children);
}
return item;
});
}
function collectLeafPaths(menus: MenuType[]): string[] {
const paths: string[] = [];
for (const m of menus) {
if (m.children && m.children.length > 0) {
paths.push(...collectLeafPaths(m.children));
} else if (m.path) {
paths.push(m.path);
}
}
return paths;
}
function findOpenKeys(menus: MenuType[], targetPath: string): string[] {
const keys: string[] = [];
for (const m of menus) {
if (m.children && m.children.length > 0) {
const childPaths = collectLeafPaths(m.children);
if (childPaths.some(p => targetPath.startsWith(p))) {
keys.push(m.path || `menu-${m.id}`);
}
keys.push(...findOpenKeys(m.children, targetPath));
}
}
return keys;
}
export default function PatientLayout({ children }: { children: React.ReactNode }) { export default function PatientLayout({ children }: { children: React.ReactNode }) {
return ( return (
...@@ -62,12 +95,22 @@ function PatientLayoutInner({ children }: { children: React.ReactNode }) { ...@@ -62,12 +95,22 @@ function PatientLayoutInner({ children }: { children: React.ReactNode }) {
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const isEmbed = searchParams?.get('embed') === '1'; const isEmbed = searchParams?.get('embed') === '1';
const { user, logout } = useUserStore(); const { user, logout, menus, setMenus } = useUserStore();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const [notifications, setNotifications] = useState<Notification[]>([]); const [notifications, setNotifications] = useState<Notification[]>([]);
const currentPath = pathname || ''; const currentPath = pathname || '';
const menuItems = useMemo(() => convertMenuTree(menus), [menus]);
// 菜单为空时主动拉取
useEffect(() => {
if (!user || menus.length > 0) return;
myMenuApi.getMenus().then(res => {
if (res.data && res.data.length > 0) setMenus(res.data);
}).catch(() => {});
}, [user, menus.length, setMenus]);
useEffect(() => { useEffect(() => {
if (!user) return; if (!user) return;
const fetchUnread = () => { const fetchUnread = () => {
...@@ -116,25 +159,16 @@ function PatientLayoutInner({ children }: { children: React.ReactNode }) { ...@@ -116,25 +159,16 @@ function PatientLayoutInner({ children }: { children: React.ReactNode }) {
else if (key === 'profile') router.push('/patient/profile'); else if (key === 'profile') router.push('/patient/profile');
}; };
const getSelectedKeys = () => { const getSelectedKeys = useCallback(() => {
const match = allRouteKeys.find(k => currentPath.startsWith(k)); const allPaths = collectLeafPaths(menus);
const match = allPaths.find(k => currentPath.startsWith(k));
return match ? [match] : []; return match ? [match] : [];
}; }, [menus, currentPath]);
const getOpenKeys = () => { const getOpenKeys = useCallback(() => {
if (collapsed) return []; if (collapsed) return [];
const keys: string[] = []; return findOpenKeys(menus, currentPath);
if (['/patient/doctors', '/patient/consult'].some(k => currentPath.startsWith(k))) { }, [menus, currentPath, collapsed]);
keys.push('consult-services');
}
if (['/patient/prescription', '/patient/pharmacy/order', '/patient/payment'].some(k => currentPath.startsWith(k))) {
keys.push('rx-pharmacy');
}
if (['/patient/chronic', '/patient/health-records'].some(k => currentPath.startsWith(k))) {
keys.push('health-mgmt');
}
return keys;
};
if (isEmbed) { if (isEmbed) {
return <div style={{ minHeight: '100vh', background: '#f5f8ff' }}>{children}</div>; return <div style={{ minHeight: '100vh', background: '#f5f8ff' }}>{children}</div>;
......
'use client'; 'use client';
import React, { useCallback, useMemo, useState, useEffect } from 'react'; import React, { useCallback, useMemo, useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { Tooltip, Spin } from 'antd'; import { Tooltip, Spin } from 'antd';
import { import {
RobotOutlined, RobotOutlined,
...@@ -137,6 +138,9 @@ const FloatContainer: React.FC = () => { ...@@ -137,6 +138,9 @@ const FloatContainer: React.FC = () => {
if (!mounted || hidden) return null; if (!mounted || hidden) return null;
// 使用 Portal 渲染到 document.body,确保不受父级 Layout stacking context 影响
const content = (() => {
// ==================== Minimized: floating circle button ==================== // ==================== Minimized: floating circle button ====================
if (!isOpen) { if (!isOpen) {
return ( return (
...@@ -375,6 +379,10 @@ const FloatContainer: React.FC = () => { ...@@ -375,6 +379,10 @@ const FloatContainer: React.FC = () => {
</div> </div>
</Rnd> </Rnd>
); );
})(); // end content IIFE
return createPortal(content, document.body);
}; };
// ---------- Small header button ---------- // ---------- Small header button ----------
......
...@@ -10,6 +10,7 @@ interface UserState { ...@@ -10,6 +10,7 @@ interface UserState {
isAuthenticated: boolean; isAuthenticated: boolean;
menus: Menu[]; menus: Menu[];
setUser: (user: UserInfo, menus?: Menu[]) => void; setUser: (user: UserInfo, menus?: Menu[]) => void;
setMenus: (menus: Menu[]) => void;
setTokens: (accessToken: string, refreshToken: string) => void; setTokens: (accessToken: string, refreshToken: string) => void;
logout: () => void; logout: () => void;
} }
...@@ -25,6 +26,8 @@ export const useUserStore = create<UserState>()( ...@@ -25,6 +26,8 @@ export const useUserStore = create<UserState>()(
setUser: (user, menus) => set({ user, isAuthenticated: true, menus: menus || [] }), setUser: (user, menus) => set({ user, isAuthenticated: true, menus: menus || [] }),
setMenus: (menus) => set({ menus }),
setTokens: (accessToken, refreshToken) => { setTokens: (accessToken, refreshToken) => {
localStorage.setItem('access_token', accessToken); localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken); localStorage.setItem('refresh_token', refreshToken);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment