Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
I
Internet-hospital
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Labels
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Jobs
Commits
Open sidebar
yuguo
Internet-hospital
Commits
671cf8df
Commit
671cf8df
authored
Mar 04, 2026
by
yuguo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
9b315e10
Changes
34
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
34 changed files
with
922 additions
and
1230 deletions
+922
-1230
.claude/settings.local.json
.claude/settings.local.json
+2
-1
server/cmd/api/main.go
server/cmd/api/main.go
+9
-14
server/internal/agent/agents.go
server/internal/agent/agents.go
+5
-3
server/internal/agent/handler.go
server/internal/agent/handler.go
+13
-3
server/internal/agent/init.go
server/internal/agent/init.go
+5
-2
server/internal/agent/service.go
server/internal/agent/service.go
+22
-5
server/internal/model/route_registry.go
server/internal/model/route_registry.go
+0
-22
server/internal/service/admin/agent_handler.go
server/internal/service/admin/agent_handler.go
+4
-4
server/internal/service/admin/cleanup_tables.go
server/internal/service/admin/cleanup_tables.go
+158
-0
server/internal/service/admin/handler.go
server/internal/service/admin/handler.go
+38
-15
server/internal/service/admin/menu_service.go
server/internal/service/admin/menu_service.go
+0
-5
server/internal/service/admin/role_service.go
server/internal/service/admin/role_service.go
+102
-230
server/internal/service/admin/seed_rbac.go
server/internal/service/admin/seed_rbac.go
+2
-185
server/internal/service/admin/workflow_handler.go
server/internal/service/admin/workflow_handler.go
+9
-9
server/internal/service/chronic/service.go
server/internal/service/chronic/service.go
+1
-1
server/internal/service/consult/service.go
server/internal/service/consult/service.go
+1
-1
server/internal/service/doctorportal/prescription_service.go
server/internal/service/doctorportal/prescription_service.go
+1
-1
server/internal/service/doctorportal/quick_reply_handler.go
server/internal/service/doctorportal/quick_reply_handler.go
+1
-0
server/internal/service/knowledgesvc/handler.go
server/internal/service/knowledgesvc/handler.go
+16
-17
server/internal/service/user/service.go
server/internal/service/user/service.go
+70
-17
server/internal/service/workflowsvc/handler.go
server/internal/service/workflowsvc/handler.go
+12
-12
server/pkg/agent/react_agent.go
server/pkg/agent/react_agent.go
+11
-3
server/pkg/agent/tools/navigate_page.go
server/pkg/agent/tools/navigate_page.go
+36
-46
server/pkg/middleware/permission.go
server/pkg/middleware/permission.go
+3
-0
server/pkg/workflow/engine.go
server/pkg/workflow/engine.go
+4
-3
web/src/api/rbac.ts
web/src/api/rbac.ts
+8
-36
web/src/app/(main)/admin/layout.tsx
web/src/app/(main)/admin/layout.tsx
+10
-1
web/src/app/(main)/admin/menus/page.tsx
web/src/app/(main)/admin/menus/page.tsx
+185
-235
web/src/app/(main)/admin/roles/page.tsx
web/src/app/(main)/admin/roles/page.tsx
+12
-281
web/src/app/(main)/doctor/layout.tsx
web/src/app/(main)/doctor/layout.tsx
+87
-16
web/src/app/(main)/patient/consult/create/page.tsx
web/src/app/(main)/patient/consult/create/page.tsx
+0
-12
web/src/app/(main)/patient/layout.tsx
web/src/app/(main)/patient/layout.tsx
+84
-50
web/src/components/GlobalAIFloat/FloatContainer.tsx
web/src/components/GlobalAIFloat/FloatContainer.tsx
+8
-0
web/src/store/userStore.ts
web/src/store/userStore.ts
+3
-0
No files found.
.claude/settings.local.json
View file @
671cf8df
...
...
@@ -41,7 +41,8 @@
"Bash(claude mcp:*)"
,
"WebSearch"
,
"Bash(go env:*)"
,
"Bash(go install:*)"
"Bash(go install:*)"
,
"Bash(where psql:*)"
]
}
}
server/cmd/api/main.go
View file @
671cf8df
...
...
@@ -133,8 +133,6 @@ func main() {
&
model
.
UserRole
{},
&
model
.
Menu
{},
&
model
.
RoleMenu
{},
// 路由注册表(AI 导航工具)
&
model
.
RouteEntry
{},
}
failCount
:=
0
for
_
,
m
:=
range
allModels
{
...
...
@@ -175,21 +173,16 @@ func main() {
ai
.
InitClient
(
&
cfg
.
AI
)
}
// 初始化超级管理员账户
// 初始化超级管理员账户
(唯一自动导入的种子数据)
if
err
:=
user
.
InitAdminUser
();
err
!=
nil
{
log
.
Printf
(
"Warning: Failed to init admin user: %v"
,
err
)
}
// 初始化 RBAC 种子数据(角色/权限/菜单,v12新增)
admin
.
SeedRBAC
()
// 初始化科室和医生数据
if
err
:=
doctor
.
InitDepartmentsAndDoctors
();
err
!=
nil
{
log
.
Printf
(
"Warning: Failed to init departments and doctors: %v"
,
err
)
}
// 初始化药品种子数据
admin
.
SeedMedicines
()
// 注意:RBAC种子数据、科室医生、药品数据不再自动导入
// 请通过管理端API手动导入:
// POST /api/v1/admin/seed/rbac — 导入角色/权限/菜单
// POST /api/v1/admin/seed/departments — 导入科室和医生
// POST /api/v1/admin/seed/medicines — 导入药品数据
// 初始化Agent工具(内置工具 + HTTP 动态工具)
internalagent
.
InitTools
()
...
...
@@ -251,10 +244,12 @@ func main() {
adminApi
.
Use
(
middleware
.
RequireRole
(
"admin"
))
adminHandler
:=
admin
.
NewHandler
()
adminHandler
.
RegisterRoutes
(
adminApi
)
adminHandler
.
RegisterUserRoutes
(
authApi
)
// /my/menus — 所有登录用户可访问
// Agent路由
(需要认证
)
// Agent路由
:聊天/会话(全角色)+ 管理(仅admin
)
agentHandler
:=
internalagent
.
NewHandler
()
agentHandler
.
RegisterRoutes
(
authApi
)
agentHandler
.
RegisterAdminRoutes
(
adminApi
)
// 工作流路由(需要认证)
workflowHandler
:=
workflowsvc
.
NewHandler
()
...
...
server/internal/agent/agents.go
View file @
671cf8df
...
...
@@ -55,7 +55,8 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 所有医疗建议仅供参考,请以专业医生判断为准
页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如找医生、我的问诊、处方、健康档案等
- 你可以使用 navigate_page 工具打开患者端页面,如找医生、我的问诊、处方、健康档案等
- 你只能导航到 patient_* 开头的页面,不能访问管理端或医生端页面
- 当用户想查看某个页面时,直接调用 navigate_page 工具导航到对应页面`
,
Tools
:
string
(
patientTools
),
Config
:
"{}"
,
...
...
@@ -87,7 +88,8 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 所有建议仅供医生参考,请结合临床实际情况
页面导航能力:
- 你可以使用 navigate_page 工具打开系统页面,如工作台、问诊大厅、患者档案、排班管理等
- 你可以使用 navigate_page 工具打开医生端页面,如工作台、问诊大厅、患者档案、排班管理等
- 你只能导航到 doctor_* 开头的页面,不能访问管理端或患者端页面
- 当医生想查看某个页面时,直接调用 navigate_page 工具导航到对应页面`
,
Tools
:
string
(
doctorTools
),
Config
:
"{}"
,
...
...
@@ -117,7 +119,7 @@ func defaultAgentDefinitions() []model.AgentDefinition {
- 用中文回答
页面导航能力:
- 你可以使用 navigate_page 工具打开
系统页面,如医生管理、患者管理、科室管理、问诊管理等
- 你可以使用 navigate_page 工具打开
所有系统页面,包括管理端、患者端、医生端
- 当管理员想查看或操作某个页面时,直接调用 navigate_page 工具导航到对应页面
- 支持 open_add 操作自动打开新增弹窗(如新增医生、新增科室等)`
,
Tools
:
string
(
adminTools
),
...
...
server/internal/agent/handler.go
View file @
671cf8df
...
...
@@ -27,6 +27,7 @@ func NewHandler() *Handler {
return
&
Handler
{
svc
:
GetService
()}
}
// RegisterRoutes 用户侧路由(全角色可用,仅需 JWT)
func
(
h
*
Handler
)
RegisterRoutes
(
r
gin
.
IRouter
)
{
g
:=
r
.
Group
(
"/agent"
)
g
.
POST
(
"/:agent_id/chat"
,
h
.
Chat
)
...
...
@@ -34,6 +35,11 @@ func (h *Handler) RegisterRoutes(r gin.IRouter) {
g
.
GET
(
"/sessions"
,
h
.
ListSessions
)
g
.
DELETE
(
"/session/:session_id"
,
h
.
DeleteSession
)
g
.
GET
(
"/list"
,
h
.
ListAgents
)
}
// RegisterAdminRoutes 管理侧路由(仅 admin,需挂在 adminApi 组下)
func
(
h
*
Handler
)
RegisterAdminRoutes
(
r
gin
.
IRouter
)
{
g
:=
r
.
Group
(
"/agent"
)
g
.
GET
(
"/tools"
,
h
.
ListTools
)
// HTTP 动态工具管理
...
...
@@ -44,7 +50,7 @@ func (h *Handler) RegisterRoutes(r gin.IRouter) {
g
.
POST
(
"/http-tools/:id/test"
,
h
.
TestHTTPTool
)
g
.
POST
(
"/http-tools/reload"
,
h
.
ReloadHTTPToolsAPI
)
// Agent 配置管理
(管理端使用)
// Agent 配置管理
g
.
GET
(
"/definitions"
,
h
.
ListDefinitions
)
g
.
GET
(
"/definitions/:agent_id"
,
h
.
GetDefinition
)
g
.
POST
(
"/definitions"
,
h
.
CreateDefinition
)
...
...
@@ -73,8 +79,10 @@ func (h *Handler) Chat(c *gin.Context) {
userID
,
_
:=
c
.
Get
(
"user_id"
)
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
{
response
.
Error
(
c
,
500
,
err
.
Error
())
return
...
...
@@ -113,12 +121,14 @@ func (h *Handler) ChatStream(c *gin.Context) {
userID
,
_
:=
c
.
Get
(
"user_id"
)
uid
,
_
:=
userID
.
(
string
)
role
,
_
:=
c
.
Get
(
"role"
)
userRole
,
_
:=
role
.
(
string
)
emit
:=
func
(
event
,
data
string
)
{
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
)
{
...
...
server/internal/agent/init.go
View file @
671cf8df
...
...
@@ -81,7 +81,9 @@ func WireCallbacks() {
svc
:=
GetService
()
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
{
return
""
,
err
}
...
...
@@ -97,7 +99,8 @@ func WireCallbacks() {
// v15: 初始化 SkillExecutor(多Agent编排引擎)
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
{
return
""
,
err
}
...
...
server/internal/agent/service.go
View file @
671cf8df
...
...
@@ -69,7 +69,12 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent {
systemPrompt
:=
def
.
SystemPrompt
var
skillIDs
[]
string
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
{
db
:=
database
.
GetDB
()
...
...
@@ -83,7 +88,12 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent {
for
_
,
skill
:=
range
skills
{
// 合并工具(去重)
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
{
if
!
toolSet
[
st
]
{
tools
=
append
(
tools
,
st
)
...
...
@@ -116,7 +126,12 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent {
func
getOrchestrationSkills
(
def
model
.
AgentDefinition
)
[]
model
.
AgentSkill
{
var
skillIDs
[]
string
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
{
return
nil
...
...
@@ -212,7 +227,7 @@ func (s *AgentService) ListAgents() []map[string]interface{} {
}
// 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
)
if
!
ok
{
return
nil
,
nil
...
...
@@ -236,6 +251,7 @@ func (s *AgentService) Chat(ctx context.Context, agentID, userID, sessionID, mes
input
:=
agent
.
AgentInput
{
SessionID
:
sessionID
,
UserID
:
userID
,
UserRole
:
userRole
,
Message
:
message
,
Context
:
contextData
,
History
:
history
,
...
...
@@ -297,7 +313,7 @@ func (s *AgentService) Chat(ctx context.Context, agentID, userID, sessionID, mes
}
// 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
)
if
!
ok
{
errJSON
,
_
:=
json
.
Marshal
(
map
[
string
]
string
{
"error"
:
"agent not found"
})
...
...
@@ -378,6 +394,7 @@ func (s *AgentService) ChatStream(ctx context.Context, agentID, userID, sessionI
input
:=
agent
.
AgentInput
{
SessionID
:
sessionID
,
UserID
:
userID
,
UserRole
:
userRole
,
Message
:
message
,
Context
:
contextData
,
History
:
history
,
...
...
server/internal/model/route_registry.go
deleted
100644 → 0
View file @
9b315e10
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"`
}
server/internal/service/admin/agent_handler.go
View file @
671cf8df
package
admin
import
(
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"internet-hospital/internal/model"
"internet-hospital/pkg/database"
"internet-hospital/pkg/response"
)
// GetAgentExecutionLogs 获取 Agent 执行日志(分页)
...
...
@@ -48,12 +48,12 @@ func (h *Handler) GetAgentExecutionLogs(c *gin.Context) {
Limit
(
pageSize
)
.
Find
(
&
logs
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
gin
.
H
{
response
.
Success
(
c
,
gin
.
H
{
"list"
:
logs
,
"total"
:
total
,
"page"
:
page
,
"page_size"
:
pageSize
,
}
}
)
})
}
// GetAgentStats 获取 Agent 执行统计
...
...
@@ -83,5 +83,5 @@ func (h *Handler) GetAgentStats(c *gin.Context) {
Group
(
"agent_id"
)
.
Scan
(
&
stats
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
stats
}
)
response
.
Success
(
c
,
stats
)
}
server/internal/service/admin/cleanup_tables.go
0 → 100644
View file @
671cf8df
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
)),
})
}
server/internal/service/admin/handler.go
View file @
671cf8df
...
...
@@ -3,6 +3,7 @@ package admin
import
(
"github.com/gin-gonic/gin"
"internet-hospital/internal/service/doctor"
"internet-hospital/pkg/response"
)
...
...
@@ -124,31 +125,29 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
adm
.
GET
(
"/ai-center/trace"
,
h
.
GetTraceDetail
)
adm
.
GET
(
"/ai-center/stats"
,
h
.
GetAICenterStats
)
// 角色管理(
v12新增
)
// 角色管理(
只读
)
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
.
PUT
(
"/roles/:id/menus"
,
h
.
SetRoleMenus
)
// 权限管理(v12新增)
adm
.
GET
(
"/permissions"
,
h
.
ListPermissions
)
adm
.
POST
(
"/permissions"
,
h
.
CreatePermission
)
// 用户角色分配(直接更新 User.Role)
adm
.
PUT
(
"/users/:id/role"
,
h
.
SetUserRole
)
// 菜单管理
(v12新增)
// 菜单管理
adm
.
GET
(
"/menus"
,
h
.
ListMenus
)
adm
.
GET
(
"/menus/flat"
,
h
.
ListMenusFlat
)
adm
.
POST
(
"/menus"
,
h
.
CreateMenu
)
adm
.
PUT
(
"/menus/:id"
,
h
.
UpdateMenu
)
adm
.
DELETE
(
"/menus/:id"
,
h
.
DeleteMenu
)
adm
.
POST
(
"/menus/reseed"
,
h
.
ReseedMenus
)
// 用户角色分配(v12新增)
adm
.
GET
(
"/users/:id/roles"
,
h
.
GetUserRoles
)
adm
.
PUT
(
"/users/:id/roles"
,
h
.
SetUserRoles
)
// 手动种子数据导入
adm
.
POST
(
"/seed/rbac"
,
h
.
SeedRBACHandler
)
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
)
...
...
@@ -159,7 +158,10 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
adm
.
POST
(
"/reports/:id/submit"
,
h
.
SubmitReport
)
}
// 当前用户菜单(不在 /admin 前缀下,所有登录用户可访问)
}
// RegisterUserRoutes 注册所有登录用户可访问的路由(不受 admin 角色限制)
func
(
h
*
Handler
)
RegisterUserRoutes
(
r
*
gin
.
RouterGroup
)
{
r
.
GET
(
"/my/menus"
,
h
.
GetMyMenus
)
}
...
...
@@ -468,3 +470,24 @@ func (h *Handler) ResetAdminPassword(c *gin.Context) {
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"
:
"药品种子数据已导入"
})
}
server/internal/service/admin/menu_service.go
View file @
671cf8df
...
...
@@ -152,8 +152,3 @@ func (h *Handler) DeleteMenu(c *gin.Context) {
response
.
Success
(
c
,
nil
)
}
// ReseedMenus 重新导入菜单数据(删除旧数据,以页面展示结构为准)
func
(
h
*
Handler
)
ReseedMenus
(
c
*
gin
.
Context
)
{
ReseedMenus
()
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"菜单数据已重新导入"
})
}
server/internal/service/admin/role_service.go
View file @
671cf8df
This diff is collapsed.
Click to expand it.
server/internal/service/admin/seed_rbac.go
View file @
671cf8df
...
...
@@ -5,15 +5,13 @@ import (
"internet-hospital/internal/model"
"internet-hospital/pkg/database"
"gorm.io/gorm"
)
// SeedRBAC 初始化角色、权限、菜单种子数据(幂等,只在表空时插入)
// SeedRBAC 初始化角色种子数据(幂等,仅在 roles 表为空时插入)
// 菜单数据通过管理端菜单管理页面维护,不在代码中硬编码。
func
SeedRBAC
()
{
db
:=
database
.
GetDB
()
// ── 1. 角色种子 ──
var
roleCount
int64
db
.
Model
(
&
model
.
Role
{})
.
Count
(
&
roleCount
)
if
roleCount
==
0
{
...
...
@@ -27,185 +25,4 @@ func 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] 菜单种子数据已创建(与前端路由完全一致)"
)
}
server/internal/service/admin/workflow_handler.go
View file @
671cf8df
package
admin
import
(
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"internet-hospital/internal/model"
"internet-hospital/pkg/database"
"internet-hospital/pkg/response"
)
// ListWorkflows 工作流列表
func
(
h
*
Handler
)
ListWorkflows
(
c
*
gin
.
Context
)
{
var
workflows
[]
model
.
WorkflowDefinition
database
.
GetDB
()
.
Find
(
&
workflows
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
workflows
}
)
response
.
Success
(
c
,
workflows
)
}
// CreateWorkflow 创建工作流
...
...
@@ -27,7 +27,7 @@ func (h *Handler) CreateWorkflow(c *gin.Context) {
Definition
string
`json:"definition"`
}
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
c
.
JSON
(
http
.
StatusBadRequest
,
gin
.
H
{
"error"
:
err
.
Error
()}
)
response
.
BadRequest
(
c
,
err
.
Error
()
)
return
}
wf
:=
model
.
WorkflowDefinition
{
...
...
@@ -40,7 +40,7 @@ func (h *Handler) CreateWorkflow(c *gin.Context) {
Version
:
1
,
}
database
.
GetDB
()
.
Create
(
&
wf
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
wf
}
)
response
.
Success
(
c
,
wf
)
}
// UpdateWorkflow 更新工作流(名称/描述/定义)
...
...
@@ -51,7 +51,7 @@ func (h *Handler) UpdateWorkflow(c *gin.Context) {
Definition
string
`json:"definition"`
}
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
c
.
JSON
(
http
.
StatusBadRequest
,
gin
.
H
{
"error"
:
err
.
Error
()}
)
response
.
BadRequest
(
c
,
err
.
Error
()
)
return
}
updates
:=
map
[
string
]
interface
{}{}
...
...
@@ -67,7 +67,7 @@ func (h *Handler) UpdateWorkflow(c *gin.Context) {
database
.
GetDB
()
.
Model
(
&
model
.
WorkflowDefinition
{})
.
Where
(
"id = ?"
,
c
.
Param
(
"id"
))
.
Updates
(
updates
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"ok"
}
)
response
.
Success
(
c
,
nil
)
}
// PublishWorkflow 发布工作流
...
...
@@ -75,7 +75,7 @@ func (h *Handler) PublishWorkflow(c *gin.Context) {
database
.
GetDB
()
.
Model
(
&
model
.
WorkflowDefinition
{})
.
Where
(
"id = ?"
,
c
.
Param
(
"id"
))
.
Update
(
"status"
,
"active"
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"ok"
}
)
response
.
Success
(
c
,
nil
)
}
// ListWorkflowExecutions 工作流执行记录列表
...
...
@@ -107,10 +107,10 @@ func (h *Handler) ListWorkflowExecutions(c *gin.Context) {
Limit
(
pageSize
)
.
Find
(
&
executions
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
gin
.
H
{
response
.
Success
(
c
,
gin
.
H
{
"list"
:
executions
,
"total"
:
total
,
"page"
:
page
,
"page_size"
:
pageSize
,
}
}
)
})
}
server/internal/service/chronic/service.go
View file @
671cf8df
...
...
@@ -131,7 +131,7 @@ func (s *Service) GetAIRenewalAdvice(ctx context.Context, userID, renewalID stri
msg
:=
fmt
.
Sprintf
(
"患者%s申请续药%d个月,当前用药:%s,原因:%s。请评估续药合理性并给出专业建议。"
,
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
{
return
""
,
fmt
.
Errorf
(
"AI续药建议获取失败: %w"
,
err
)
}
...
...
server/internal/service/consult/service.go
View file @
671cf8df
...
...
@@ -380,7 +380,7 @@ func (s *Service) AIAssist(ctx context.Context, consultID string, scene string)
}
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
{
return
nil
,
fmt
.
Errorf
(
"AI分析失败: %w"
,
err
)
}
...
...
server/internal/service/doctorportal/prescription_service.go
View file @
671cf8df
...
...
@@ -224,7 +224,7 @@ func (s *Service) CheckPrescriptionSafety(ctx context.Context, userID, patientID
message
:=
fmt
.
Sprintf
(
"请审核以下处方的安全性:%s,检查药物相互作用和禁忌症"
,
drugList
)
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
{
return
nil
,
fmt
.
Errorf
(
"处方安全审核失败: %w"
,
err
)
}
...
...
server/internal/service/doctorportal/quick_reply_handler.go
View file @
671cf8df
...
...
@@ -201,6 +201,7 @@ func (h *Handler) AIGenerateReplies(c *gin.Context) {
c
.
Request
.
Context
(),
"doctor_universal_agent"
,
userID
.
(
string
),
"doctor"
,
""
,
"请根据以上对话上下文,生成3-5条医生可以使用的快捷回复建议。每条回复独立一行,不要编号,直接输出回复内容。回复应该专业、简洁、有针对性。"
,
agentCtx
,
...
...
server/internal/service/knowledgesvc/handler.go
View file @
671cf8df
package
knowledgesvc
import
(
"net/http"
"strings"
"internet-hospital/internal/model"
"internet-hospital/pkg/database"
"internet-hospital/pkg/response"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
...
...
@@ -33,7 +33,7 @@ func (h *Handler) Search(c *gin.Context) {
TopK
int
`json:"top_k"`
}
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
c
.
JSON
(
http
.
StatusBadRequest
,
gin
.
H
{
"error"
:
err
.
Error
()}
)
response
.
BadRequest
(
c
,
err
.
Error
()
)
return
}
if
req
.
TopK
<=
0
{
...
...
@@ -57,13 +57,13 @@ func (h *Handler) Search(c *gin.Context) {
"document_id"
:
ch
.
DocumentID
,
})
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
results
}
)
response
.
Success
(
c
,
results
)
}
func
(
h
*
Handler
)
ListCollections
(
c
*
gin
.
Context
)
{
var
cols
[]
model
.
KnowledgeCollection
database
.
GetDB
()
.
Find
(
&
cols
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
cols
}
)
response
.
Success
(
c
,
cols
)
}
func
(
h
*
Handler
)
CreateCollection
(
c
*
gin
.
Context
)
{
...
...
@@ -73,7 +73,7 @@ func (h *Handler) CreateCollection(c *gin.Context) {
Category
string
`json:"category"`
}
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
c
.
JSON
(
http
.
StatusBadRequest
,
gin
.
H
{
"error"
:
err
.
Error
()}
)
response
.
BadRequest
(
c
,
err
.
Error
()
)
return
}
col
:=
model
.
KnowledgeCollection
{
...
...
@@ -83,7 +83,7 @@ func (h *Handler) CreateCollection(c *gin.Context) {
Category
:
req
.
Category
,
}
database
.
GetDB
()
.
Create
(
&
col
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
col
}
)
response
.
Success
(
c
,
col
)
}
func
(
h
*
Handler
)
CreateDocument
(
c
*
gin
.
Context
)
{
...
...
@@ -94,7 +94,7 @@ func (h *Handler) CreateDocument(c *gin.Context) {
FileType
string
`json:"file_type"`
}
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
c
.
JSON
(
http
.
StatusBadRequest
,
gin
.
H
{
"error"
:
err
.
Error
()}
)
response
.
BadRequest
(
c
,
err
.
Error
()
)
return
}
...
...
@@ -128,7 +128,7 @@ func (h *Handler) CreateDocument(c *gin.Context) {
Where
(
"collection_id = ?"
,
req
.
CollectionID
)
.
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
)
{
...
...
@@ -139,20 +139,19 @@ func (h *Handler) ListDocuments(c *gin.Context) {
q
=
q
.
Where
(
"collection_id = ?"
,
collectionID
)
}
q
.
Find
(
&
docs
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
docs
}
)
response
.
Success
(
c
,
docs
)
}
func
(
h
*
Handler
)
DeleteCollection
(
c
*
gin
.
Context
)
{
id
:=
c
.
Param
(
"id"
)
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 = ?"
,
id
)
.
Delete
(
&
model
.
KnowledgeDocument
{})
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
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
nil
,
"message"
:
"ok"
}
)
response
.
Success
(
c
,
nil
)
}
func
(
h
*
Handler
)
DeleteDocument
(
c
*
gin
.
Context
)
{
...
...
@@ -160,10 +159,10 @@ func (h *Handler) DeleteDocument(c *gin.Context) {
db
:=
database
.
GetDB
()
db
.
Where
(
"document_id = ?"
,
id
)
.
Delete
(
&
model
.
KnowledgeChunk
{})
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
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
nil
,
"message"
:
"ok"
}
)
response
.
Success
(
c
,
nil
)
}
func
splitIntoChunks
(
text
string
,
chunkSize
int
)
[]
string
{
...
...
server/internal/service/user/service.go
View file @
671cf8df
...
...
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"math/rand"
"strings"
"time"
"github.com/google/uuid"
...
...
@@ -195,7 +196,7 @@ type UserProfileResponse struct {
Menus
[]
model
.
Menu
`json:"menus"`
}
// GetUserProfile 获取用户完整信息(包含
基于 RBAC 的
菜单树)
// GetUserProfile 获取用户完整信息(包含菜单树)
func
(
s
*
Service
)
GetUserProfile
(
ctx
context
.
Context
,
userID
string
)
(
*
UserProfileResponse
,
error
)
{
var
user
model
.
User
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
},
nil
}
// getUserMenus 基于
RBAC 查询用户的
菜单树
//
流程:user_roles → role_menus → menus → 构建树
// getUserMenus 基于
User.Role 返回对应端
菜单树
//
admin → /admin/* , patient → /patient/* , doctor → /doctor/*
func
(
s
*
Service
)
getUserMenus
(
userID
string
)
[]
model
.
Menu
{
// 通过 user_roles + role_menus 获取该用户可见的菜单 ID
var
menuIDs
[]
uint
s
.
db
.
Raw
(
`SELECT DISTINCT rm.menu_id FROM role_menus rm
INNER JOIN user_roles ur ON ur.role_id = rm.role_id
WHERE ur.user_id = ?`
,
userID
)
.
Scan
(
&
menuIDs
)
var
user
model
.
User
if
err
:=
s
.
db
.
Select
(
"role"
)
.
Where
(
"id = ?"
,
userID
)
.
First
(
&
user
)
.
Error
;
err
!=
nil
{
fmt
.
Printf
(
"[getUserMenus] 查询用户失败: %v
\n
"
,
err
)
return
nil
}
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
if
len
(
menuIDs
)
>
0
{
// RBAC 模式:只返回角色关联的菜单
s
.
db
.
Where
(
"id IN ? AND visible = ? AND status = ?"
,
menuIDs
,
true
,
"active"
)
.
Order
(
"sort ASC, id ASC"
)
.
Find
(
&
flatMenus
)
}
else
{
// 兼容模式:用户没有角色绑定时返回全部可见菜单
s
.
db
.
Where
(
"visible = ? AND status = ?"
,
true
,
"active"
)
.
Order
(
"sort ASC, id ASC"
)
.
Find
(
&
flatMenus
)
fmt
.
Printf
(
"[getUserMenus] 查询到 %d 条可见菜单
\n
"
,
len
(
flatMenus
))
filtered
:=
filterMenusByPrefix
(
flatMenus
,
prefix
)
fmt
.
Printf
(
"[getUserMenus] 过滤后 %d 条菜单 (prefix=%s)
\n
"
,
len
(
filtered
),
prefix
)
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 将扁平菜单列表构建为树形结构
...
...
server/internal/service/workflowsvc/handler.go
View file @
671cf8df
...
...
@@ -2,10 +2,10 @@ package workflowsvc
import
(
"encoding/json"
"net/http"
"internet-hospital/internal/model"
"internet-hospital/pkg/database"
"internet-hospital/pkg/response"
"internet-hospital/pkg/workflow"
"github.com/gin-gonic/gin"
...
...
@@ -35,25 +35,25 @@ func (h *Handler) Execute(c *gin.Context) {
execID
,
err
:=
workflow
.
GetEngine
()
.
Execute
(
c
.
Request
.
Context
(),
workflowID
,
input
,
uid
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusInternalServerError
,
gin
.
H
{
"error"
:
err
.
Error
()}
)
response
.
Error
(
c
,
500
,
err
.
Error
()
)
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
)
{
var
exec
model
.
WorkflowExecution
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
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"data"
:
exec
}
)
response
.
Success
(
c
,
exec
)
}
func
(
h
*
Handler
)
ListTasks
(
c
*
gin
.
Context
)
{
var
tasks
[]
model
.
WorkflowHumanTask
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
)
{
...
...
@@ -65,16 +65,16 @@ func (h *Handler) CompleteTask(c *gin.Context) {
database
.
GetDB
()
.
Model
(
&
model
.
WorkflowHumanTask
{})
.
Where
(
"task_id = ?"
,
c
.
Param
(
"id"
))
.
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
)
{
id
:=
c
.
Param
(
"workflow_id"
)
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
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"ok"
}
)
response
.
Success
(
c
,
nil
)
}
func
(
h
*
Handler
)
UpdateWorkflowStatus
(
c
*
gin
.
Context
)
{
...
...
@@ -83,12 +83,12 @@ func (h *Handler) UpdateWorkflowStatus(c *gin.Context) {
Status
string
`json:"status" binding:"required"`
}
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
c
.
JSON
(
http
.
StatusBadRequest
,
gin
.
H
{
"error"
:
"请求参数错误"
}
)
response
.
BadRequest
(
c
,
"请求参数错误"
)
return
}
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
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
"ok"
}
)
response
.
Success
(
c
,
nil
)
}
server/pkg/agent/react_agent.go
View file @
671cf8df
...
...
@@ -15,6 +15,7 @@ import (
type
contextKey
string
const
ContextKeyUserID
contextKey
=
"user_id"
const
ContextKeyUserRole
contextKey
=
"user_role"
// StreamEventType SSE 事件类型
type
StreamEventType
string
...
...
@@ -36,6 +37,7 @@ type StreamEvent struct {
type
AgentInput
struct
{
SessionID
string
`json:"session_id"`
UserID
string
`json:"user_id"`
UserRole
string
`json:"user_role"`
Message
string
`json:"message"`
Context
map
[
string
]
interface
{}
`json:"context"`
History
[]
ai
.
ChatMessage
`json:"history"`
...
...
@@ -123,10 +125,13 @@ func (a *ReActAgent) Description() string { return a.cfg.Description }
// Run 执行Agent(非流式)
func
(
a
*
ReActAgent
)
Run
(
ctx
context
.
Context
,
input
AgentInput
)
(
*
AgentOutput
,
error
)
{
//
v15: 将 userID
注入 context,供工具(如 navigate_page)做权限校验
//
将 userID / userRole
注入 context,供工具(如 navigate_page)做权限校验
if
input
.
UserID
!=
""
{
ctx
=
context
.
WithValue
(
ctx
,
ContextKeyUserID
,
input
.
UserID
)
}
if
input
.
UserRole
!=
""
{
ctx
=
context
.
WithValue
(
ctx
,
ContextKeyUserRole
,
input
.
UserRole
)
}
// 生成链路追踪ID(如果未提供则新建)
traceID
:=
input
.
TraceID
if
traceID
==
""
{
...
...
@@ -234,10 +239,13 @@ func (a *ReActAgent) Run(ctx context.Context, input AgentInput) (*AgentOutput, e
// RunStream 流式执行Agent,通过 onEvent 实时推送中间过程
// v12改造:最终文本回答使用真流式(ChatWithToolsStream),工具调用阶段仍用非流式。
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
!=
""
{
ctx
=
context
.
WithValue
(
ctx
,
ContextKeyUserID
,
input
.
UserID
)
}
if
input
.
UserRole
!=
""
{
ctx
=
context
.
WithValue
(
ctx
,
ContextKeyUserRole
,
input
.
UserRole
)
}
traceID
:=
input
.
TraceID
if
traceID
==
""
{
traceID
=
uuid
.
New
()
.
String
()
...
...
server/pkg/agent/tools/navigate_page.go
View file @
671cf8df
...
...
@@ -10,8 +10,6 @@ import (
"internet-hospital/internal/model"
"internet-hospital/pkg/agent"
"internet-hospital/pkg/database"
"gorm.io/gorm"
)
// menuEntry 缓存的菜单条目(从 menus 表加载)
...
...
@@ -155,11 +153,9 @@ func (t *NavigatePageTool) Execute(ctx context.Context, params map[string]interf
}
}
// RBAC 权限校验
if
userID
,
ok
:=
ctx
.
Value
(
agent
.
ContextKeyUserID
)
.
(
string
);
ok
&&
userID
!=
""
{
db
:=
database
.
GetDB
()
allowed
,
reason
:=
checkPagePermission
(
db
,
userID
,
normalizedCode
,
entry
)
if
!
allowed
{
// 基于 User.Role 的权限校验
userRole
,
_
:=
ctx
.
Value
(
agent
.
ContextKeyUserRole
)
.
(
string
)
if
allowed
,
reason
:=
checkPagePermission
(
userRole
,
normalizedCode
,
entry
);
!
allowed
{
return
map
[
string
]
interface
{}{
"action"
:
"permission_denied"
,
"page"
:
normalizedCode
,
...
...
@@ -167,7 +163,6 @@ func (t *NavigatePageTool) Execute(ctx context.Context, params map[string]interf
"message"
:
reason
,
},
nil
}
}
// 根据 operation 构建最终路由
route
:=
entry
.
Path
...
...
@@ -194,43 +189,38 @@ func (t *NavigatePageTool) Execute(ctx context.Context, params map[string]interf
},
nil
}
// checkPagePermission 基于菜单的权限字段做 RBAC 校验
func
checkPagePermission
(
db
*
gorm
.
DB
,
userID
,
pageCode
string
,
entry
menuEntry
)
(
bool
,
string
)
{
if
db
==
nil
||
userID
==
""
{
return
true
,
""
// checkPagePermission 基于 User.Role 的页面导航权限校验
// 规则:
// - admin → 可访问所有页面(admin_*、patient_*、doctor_*)
// - patient → 仅可访问 patient_* 页面
// - doctor → 仅可访问 doctor_* 页面
// - 未知角色 → 拒绝所有
func
checkPagePermission
(
userRole
,
pageCode
string
,
entry
menuEntry
)
(
bool
,
string
)
{
if
userRole
==
""
{
return
false
,
fmt
.
Sprintf
(
"未识别您的角色,无法访问「%s」"
,
entry
.
Name
)
}
//
非 admin 页面不做额外权限校验(依赖 JWT 角色即可)
if
!
strings
.
HasPrefix
(
pageCode
,
"admin_"
)
{
//
admin 拥有全部页面访问权限
if
userRole
==
"admin"
{
return
true
,
""
}
// admin 角色拥有所有权限
var
adminCount
int64
db
.
Table
(
"user_roles"
)
.
Joins
(
"JOIN roles ON roles.id = user_roles.role_id"
)
.
Where
(
"user_roles.user_id = ? AND roles.code = 'admin' AND roles.status = 'active'"
,
userID
)
.
Count
(
&
adminCount
)
if
adminCount
>
0
{
// 判断页面所属端:admin_* / patient_* / doctor_*
switch
{
case
strings
.
HasPrefix
(
pageCode
,
"admin_"
)
:
return
false
,
fmt
.
Sprintf
(
"您没有访问管理端「%s」页面的权限"
,
entry
.
Name
)
case
strings
.
HasPrefix
(
pageCode
,
"patient_"
)
:
if
userRole
==
"patient"
{
return
true
,
""
}
// 如果菜单没有配置权限码,放行
if
entry
.
Permission
==
"
"
{
return
false
,
fmt
.
Sprintf
(
"该页面仅限患者访问"
,
)
case
strings
.
HasPrefix
(
pageCode
,
"doctor_"
)
:
if
userRole
==
"doctor
"
{
return
true
,
""
}
// 检查用户是否拥有该权限
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
false
,
fmt
.
Sprintf
(
"该页面仅限医生访问"
)
default
:
// 无明确前缀的页面(如果有),放行
return
true
,
""
}
return
false
,
fmt
.
Sprintf
(
"您没有访问「%s」页面的权限,请联系管理员"
,
entry
.
Name
)
}
server/pkg/middleware/permission.go
View file @
671cf8df
// Deprecated: 本文件中的 RequirePermission 中间件基于 RBAC 表(roles/permissions/
// user_roles/role_permissions),但系统已简化为以 User.Role 作为唯一权限依据。
// 当前无任何路由挂载此中间件,保留文件仅供参考,请勿在新代码中使用。
package
middleware
import
(
...
...
server/pkg/workflow/engine.go
View file @
671cf8df
...
...
@@ -70,7 +70,7 @@ type ExecutionContext struct {
// Engine 工作流引擎
type
Engine
struct
{
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
}
...
...
@@ -85,7 +85,7 @@ func GetEngine() *Engine {
}
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
}
...
...
@@ -226,7 +226,8 @@ func (e *Engine) executeAgent(ctx context.Context, node *Node, execCtx *Executio
agentID
,
_
:=
node
.
Config
[
"agent_id"
]
.
(
string
)
message
,
_
:=
execCtx
.
Variables
[
"message"
]
.
(
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
)
{
...
...
web/src/api/rbac.ts
View file @
671cf8df
import
{
get
,
p
ost
,
put
,
del
}
from
'
./request
'
;
import
{
get
,
p
ut
,
del
,
post
}
from
'
./request
'
;
// ==================== Types ====================
...
...
@@ -14,16 +14,6 @@ export interface Role {
updated_at
:
string
;
}
export
interface
Permission
{
id
:
number
;
code
:
string
;
name
:
string
;
module
:
string
;
action
:
string
;
description
:
string
;
created_at
:
string
;
}
export
interface
Menu
{
id
:
number
;
parent_id
:
number
;
...
...
@@ -41,28 +31,19 @@ export interface Menu {
children
?:
Menu
[];
}
// ==================== Role API ====================
// ==================== Role API
(read-only)
====================
export
const
roleApi
=
{
list
:
()
=>
get
<
Role
[]
>
(
'
/admin/roles
'
),
create
:
(
data
:
Partial
<
Role
>
)
=>
post
<
Role
>
(
'
/admin/roles
'
,
data
),
update
:
(
id
:
number
,
data
:
Partial
<
Role
>
)
=>
put
<
Role
>
(
`/admin/roles/
${
id
}
`
,
data
),
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
}),
getMenus
:
(
roleId
:
number
)
=>
get
<
{
menu_ids
:
number
[]
}
>
(
`/admin/roles/
${
roleId
}
/menus`
),
setMenus
:
(
roleId
:
number
,
menuIds
:
number
[])
=>
put
<
null
>
(
`/admin/roles/
${
roleId
}
/menus`
,
{
menu_ids
:
menuIds
}),
};
// ====================
Permission
API ====================
// ====================
User Role
API ====================
export
const
permission
Api
=
{
list
:
()
=>
get
<
Permission
[]
>
(
'
/admin/permissions
'
),
create
:
(
data
:
Partial
<
Permission
>
)
=>
post
<
Permission
>
(
'
/admin/permissions
'
,
data
),
export
const
userRole
Api
=
{
setUserRole
:
(
userId
:
number
|
string
,
role
:
'
patient
'
|
'
doctor
'
|
'
admin
'
)
=>
put
<
null
>
(
`/admin/users/
${
userId
}
/role`
,
{
role
}
),
};
// ==================== Menu API ====================
...
...
@@ -73,15 +54,6 @@ export const menuApi = {
create
:
(
data
:
Partial
<
Menu
>
)
=>
post
<
Menu
>
(
'
/admin/menus
'
,
data
),
update
:
(
id
:
number
,
data
:
Partial
<
Menu
>
)
=>
put
<
Menu
>
(
`/admin/menus/
${
id
}
`
,
data
),
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) ====================
...
...
web/src/app/(main)/admin/layout.tsx
View file @
671cf8df
...
...
@@ -14,6 +14,7 @@ import {
}
from
'
@ant-design/icons
'
;
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
'
;
const
{
Sider
,
Content
}
=
Layout
;
...
...
@@ -102,12 +103,20 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
const
pathname
=
usePathname
();
const
searchParams
=
useSearchParams
();
const
isEmbed
=
searchParams
?.
get
(
'
embed
'
)
===
'
1
'
;
const
{
user
,
logout
,
menus
}
=
useUserStore
();
const
{
user
,
logout
,
menus
,
setMenus
}
=
useUserStore
();
const
[
collapsed
,
setCollapsed
]
=
useState
(
false
);
const
[
unreadCount
,
setUnreadCount
]
=
useState
(
0
);
const
[
notifications
,
setNotifications
]
=
useState
<
Notification
[]
>
([]);
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
(()
=>
{
if
(
!
user
)
return
;
const
fetchUnread
=
()
=>
{
...
...
web/src/app/(main)/admin/menus/page.tsx
View file @
671cf8df
This diff is collapsed.
Click to expand it.
web/src/app/(main)/admin/roles/page.tsx
View file @
671cf8df
This diff is collapsed.
Click to expand it.
web/src/app/(main)/doctor/layout.tsx
View file @
671cf8df
'
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
{
Layout
,
Menu
,
Avatar
,
Dropdown
,
Badge
,
Space
,
Switch
,
Typography
,
Tag
,
Popover
,
List
}
from
'
antd
'
;
import
{
...
...
@@ -8,24 +8,78 @@ import {
SettingOutlined
,
LogoutOutlined
,
BellOutlined
,
MedicineBoxOutlined
,
DollarOutlined
,
FolderOpenOutlined
,
SafetyCertificateOutlined
,
CheckCircleOutlined
,
MenuFoldOutlined
,
MenuUnfoldOutlined
,
HomeOutlined
,
FileTextOutlined
,
HeartOutlined
,
VideoCameraOutlined
,
PayCircleOutlined
,
ShoppingCartOutlined
,
SearchOutlined
,
}
from
'
@ant-design/icons
'
;
import
{
useUserStore
}
from
'
@/store/userStore
'
;
import
type
{
Menu
as
MenuType
}
from
'
@/api/rbac
'
;
import
{
myMenuApi
}
from
'
@/api/rbac
'
;
import
{
doctorPortalApi
}
from
'
@/api/doctorPortal
'
;
import
{
notificationApi
,
type
Notification
}
from
'
@/api/notification
'
;
const
{
Sider
,
Content
}
=
Layout
;
const
{
Text
}
=
Typography
;
const
menuItems
=
[
{
key
:
'
/doctor/workbench
'
,
icon
:
<
DashboardOutlined
/>,
label
:
'
工作台
'
},
{
key
:
'
/doctor/consult
'
,
icon
:
<
MessageOutlined
/>,
label
:
'
问诊大厅
'
},
{
key
:
'
/doctor/tasks
'
,
icon
:
<
CheckCircleOutlined
/>,
label
:
'
待办任务
'
},
{
key
:
'
/doctor/chronic/review
'
,
icon
:
<
SafetyCertificateOutlined
/>,
label
:
'
慢病续方
'
},
{
key
:
'
/doctor/patient
'
,
icon
:
<
FolderOpenOutlined
/>,
label
:
'
患者档案
'
},
{
key
:
'
/doctor/schedule
'
,
icon
:
<
CalendarOutlined
/>,
label
:
'
排班管理
'
},
{
key
:
'
/doctor/income
'
,
icon
:
<
DollarOutlined
/>,
label
:
'
收入统计
'
},
{
key
:
'
/doctor/profile
'
,
icon
:
<
UserOutlined
/>,
label
:
'
个人信息
'
},
];
const
ICON_MAP
:
Record
<
string
,
React
.
ReactNode
>
=
{
DashboardOutlined
:
<
DashboardOutlined
/>,
UserOutlined
:
<
UserOutlined
/>,
MessageOutlined
:
<
MessageOutlined
/>,
CalendarOutlined
:
<
CalendarOutlined
/>,
MedicineBoxOutlined
:
<
MedicineBoxOutlined
/>,
DollarOutlined
:
<
DollarOutlined
/>,
FolderOpenOutlined
:
<
FolderOpenOutlined
/>,
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
})
{
return
(
...
...
@@ -40,13 +94,23 @@ function DoctorLayoutInner({ children }: { children: React.ReactNode }) {
const
pathname
=
usePathname
();
const
searchParams
=
useSearchParams
();
const
isEmbed
=
searchParams
?.
get
(
'
embed
'
)
===
'
1
'
;
const
{
user
,
logout
}
=
useUserStore
();
const
{
user
,
logout
,
menus
,
setMenus
}
=
useUserStore
();
const
[
isOnline
,
setIsOnline
]
=
useState
(
false
);
const
[
collapsed
,
setCollapsed
]
=
useState
(
false
);
const
[
unreadCount
,
setUnreadCount
]
=
useState
(
0
);
const
[
notifications
,
setNotifications
]
=
useState
<
Notification
[]
>
([]);
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
(()
=>
{
if
(
!
user
)
return
;
const
fetchUnread
=
()
=>
{
...
...
@@ -108,10 +172,16 @@ function DoctorLayoutInner({ children }: { children: React.ReactNode }) {
else
if
(
key
===
'
settings
'
)
{
router
.
push
(
'
/doctor/profile
'
);
}
};
const
getSelectedKeys
=
()
=>
{
const
match
=
menuItems
.
find
(
item
=>
currentPath
.
startsWith
(
item
.
key
));
return
match
?
[
match
.
key
]
:
[];
};
const
getSelectedKeys
=
useCallback
(()
=>
{
const
allPaths
=
collectLeafPaths
(
menus
);
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
)
{
return
<
div
style=
{
{
minHeight
:
'
100vh
'
,
background
:
'
#f5f8ff
'
}
}
>
{
children
}
</
div
>;
...
...
@@ -161,6 +231,7 @@ function DoctorLayoutInner({ children }: { children: React.ReactNode }) {
<
Menu
mode=
"inline"
selectedKeys=
{
getSelectedKeys
()
}
defaultOpenKeys=
{
getOpenKeys
()
}
items=
{
menuItems
}
onClick=
{
handleMenuClick
}
style=
{
{
border
:
'
none
'
,
padding
:
'
8px 0
'
}
}
...
...
web/src/app/(main)/patient/consult/create/page.tsx
deleted
100644 → 0
View file @
9b315e10
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
>
);
}
web/src/app/(main)/patient/layout.tsx
View file @
671cf8df
'
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
{
Layout
,
Menu
,
Avatar
,
Dropdown
,
Button
,
Badge
,
Space
,
Typography
,
Tag
,
Popover
,
List
}
from
'
antd
'
;
import
{
...
...
@@ -8,46 +8,79 @@ import {
HeartOutlined
,
SettingOutlined
,
LogoutOutlined
,
BellOutlined
,
VideoCameraOutlined
,
PayCircleOutlined
,
ShoppingCartOutlined
,
FolderOpenOutlined
,
SearchOutlined
,
MenuFoldOutlined
,
MenuUnfoldOutlined
,
CheckOutlined
,
MenuFoldOutlined
,
MenuUnfoldOutlined
,
DashboardOutlined
,
MessageOutlined
,
CalendarOutlined
,
DollarOutlined
,
SafetyCertificateOutlined
,
CheckCircleOutlined
,
}
from
'
@ant-design/icons
'
;
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
'
;
const
{
Sider
,
Content
}
=
Layout
;
const
{
Text
}
=
Typography
;
const
menuItems
=
[
{
key
:
'
/patient/home
'
,
icon
:
<
HomeOutlined
/>,
label
:
'
首页
'
},
{
key
:
'
consult-services
'
,
icon
:
<
VideoCameraOutlined
/>,
label
:
'
问诊服务
'
,
children
:
[
{
key
:
'
/patient/doctors
'
,
icon
:
<
SearchOutlined
/>,
label
:
'
找医生
'
},
{
key
:
'
/patient/consult
'
,
icon
:
<
VideoCameraOutlined
/>,
label
:
'
我的问诊
'
},
],
},
{
key
:
'
rx-pharmacy
'
,
icon
:
<
MedicineBoxOutlined
/>,
label
:
'
处方购药
'
,
children
:
[
{
key
:
'
/patient/prescription
'
,
icon
:
<
FileTextOutlined
/>,
label
:
'
电子处方
'
},
{
key
:
'
/patient/pharmacy/order
'
,
icon
:
<
ShoppingCartOutlined
/>,
label
:
'
药品配送
'
},
{
key
:
'
/patient/payment
'
,
icon
:
<
PayCircleOutlined
/>,
label
:
'
支付管理
'
},
],
},
{
key
:
'
health-mgmt
'
,
icon
:
<
HeartOutlined
/>,
label
:
'
健康管理
'
,
children
:
[
{
key
:
'
/patient/chronic
'
,
icon
:
<
HeartOutlined
/>,
label
:
'
慢病管理
'
},
{
key
:
'
/patient/health-records
'
,
icon
:
<
FolderOpenOutlined
/>,
label
:
'
健康档案
'
},
],
},
{
key
:
'
/patient/profile
'
,
icon
:
<
UserOutlined
/>,
label
:
'
个人中心
'
},
];
// 图标名称 → React 图标组件映射
const
ICON_MAP
:
Record
<
string
,
React
.
ReactNode
>
=
{
HomeOutlined
:
<
HomeOutlined
/>,
UserOutlined
:
<
UserOutlined
/>,
MedicineBoxOutlined
:
<
MedicineBoxOutlined
/>,
FileTextOutlined
:
<
FileTextOutlined
/>,
HeartOutlined
:
<
HeartOutlined
/>,
VideoCameraOutlined
:
<
VideoCameraOutlined
/>,
PayCircleOutlined
:
<
PayCircleOutlined
/>,
ShoppingCartOutlined
:
<
ShoppingCartOutlined
/>,
FolderOpenOutlined
:
<
FolderOpenOutlined
/>,
SearchOutlined
:
<
SearchOutlined
/>,
SettingOutlined
:
<
SettingOutlined
/>,
DashboardOutlined
:
<
DashboardOutlined
/>,
MessageOutlined
:
<
MessageOutlined
/>,
CalendarOutlined
:
<
CalendarOutlined
/>,
DollarOutlined
:
<
DollarOutlined
/>,
SafetyCertificateOutlined
:
<
SafetyCertificateOutlined
/>,
CheckCircleOutlined
:
<
CheckCircleOutlined
/>,
BellOutlined
:
<
BellOutlined
/>,
};
const
allRouteKeys
=
[
'
/patient/home
'
,
'
/patient/doctors
'
,
'
/patient/consult
'
,
'
/patient/prescription
'
,
'
/patient/pharmacy/order
'
,
'
/patient/payment
'
,
'
/patient/chronic
'
,
'
/patient/health-records
'
,
'
/patient/profile
'
,
];
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
PatientLayout
({
children
}:
{
children
:
React
.
ReactNode
})
{
return
(
...
...
@@ -62,12 +95,22 @@ function PatientLayoutInner({ children }: { children: React.ReactNode }) {
const
pathname
=
usePathname
();
const
searchParams
=
useSearchParams
();
const
isEmbed
=
searchParams
?.
get
(
'
embed
'
)
===
'
1
'
;
const
{
user
,
logout
}
=
useUserStore
();
const
{
user
,
logout
,
menus
,
setMenus
}
=
useUserStore
();
const
[
collapsed
,
setCollapsed
]
=
useState
(
false
);
const
[
unreadCount
,
setUnreadCount
]
=
useState
(
0
);
const
[
notifications
,
setNotifications
]
=
useState
<
Notification
[]
>
([]);
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
(()
=>
{
if
(
!
user
)
return
;
const
fetchUnread
=
()
=>
{
...
...
@@ -116,25 +159,16 @@ function PatientLayoutInner({ children }: { children: React.ReactNode }) {
else
if
(
key
===
'
profile
'
)
router
.
push
(
'
/patient/profile
'
);
};
const
getSelectedKeys
=
()
=>
{
const
match
=
allRouteKeys
.
find
(
k
=>
currentPath
.
startsWith
(
k
));
const
getSelectedKeys
=
useCallback
(()
=>
{
const
allPaths
=
collectLeafPaths
(
menus
);
const
match
=
allPaths
.
find
(
k
=>
currentPath
.
startsWith
(
k
));
return
match
?
[
match
]
:
[];
};
}
,
[
menus
,
currentPath
])
;
const
getOpenKeys
=
()
=>
{
const
getOpenKeys
=
useCallback
(
()
=>
{
if
(
collapsed
)
return
[];
const
keys
:
string
[]
=
[];
if
([
'
/patient/doctors
'
,
'
/patient/consult
'
].
some
(
k
=>
currentPath
.
startsWith
(
k
)))
{
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
;
};
return
findOpenKeys
(
menus
,
currentPath
);
},
[
menus
,
currentPath
,
collapsed
]);
if
(
isEmbed
)
{
return
<
div
style=
{
{
minHeight
:
'
100vh
'
,
background
:
'
#f5f8ff
'
}
}
>
{
children
}
</
div
>;
...
...
web/src/components/GlobalAIFloat/FloatContainer.tsx
View file @
671cf8df
'
use client
'
;
import
React
,
{
useCallback
,
useMemo
,
useState
,
useEffect
}
from
'
react
'
;
import
{
createPortal
}
from
'
react-dom
'
;
import
{
Tooltip
,
Spin
}
from
'
antd
'
;
import
{
RobotOutlined
,
...
...
@@ -137,6 +138,9 @@ const FloatContainer: React.FC = () => {
if
(
!
mounted
||
hidden
)
return
null
;
// 使用 Portal 渲染到 document.body,确保不受父级 Layout stacking context 影响
const
content
=
(()
=>
{
// ==================== Minimized: floating circle button ====================
if
(
!
isOpen
)
{
return
(
...
...
@@ -375,6 +379,10 @@ const FloatContainer: React.FC = () => {
</
div
>
</
Rnd
>
);
})();
// end content IIFE
return
createPortal
(
content
,
document
.
body
);
};
// ---------- Small header button ----------
...
...
web/src/store/userStore.ts
View file @
671cf8df
...
...
@@ -10,6 +10,7 @@ interface UserState {
isAuthenticated
:
boolean
;
menus
:
Menu
[];
setUser
:
(
user
:
UserInfo
,
menus
?:
Menu
[])
=>
void
;
setMenus
:
(
menus
:
Menu
[])
=>
void
;
setTokens
:
(
accessToken
:
string
,
refreshToken
:
string
)
=>
void
;
logout
:
()
=>
void
;
}
...
...
@@ -25,6 +26,8 @@ export const useUserStore = create<UserState>()(
setUser
:
(
user
,
menus
)
=>
set
({
user
,
isAuthenticated
:
true
,
menus
:
menus
||
[]
}),
setMenus
:
(
menus
)
=>
set
({
menus
}),
setTokens
:
(
accessToken
,
refreshToken
)
=>
{
localStorage
.
setItem
(
'
access_token
'
,
accessToken
);
localStorage
.
setItem
(
'
refresh_token
'
,
refreshToken
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment