Commit 46cbe422 authored by yuguo's avatar yuguo

fix

parent 4815df10
{
"permissions": {
"allow": [
"Bash(go build:*)",
"Bash(npx tsc:*)",
"Bash(tasklist:*)",
"Bash(taskkill:*)",
"Bash(powershell:*)",
"Bash(curl:*)",
"Bash(netstat:*)"
]
}
}
# 问题排查指南
## 问诊功能相关问题
### 1. 创建问诊时报错:违反外键约束 `consultations_doctor_id_fkey`
**错误信息:**
```json
{
"code": 400,
"message": "错误: 插入或更新表 \"consultations\" 违反外键约束 \"consultations_doctor_id_fkey\" (SQLSTATE 23503)"
}
```
**原因:**
数据库中没有已认证的医生记录,导致创建问诊时 `doctor_id` 外键约束失败。
**解决方案:**
1. **执行测试数据初始化脚本**
```bash
# 进入server目录
cd server
# 使用psql执行
psql -U postgres -d internet_hospital -f scripts/seed_doctors.sql
```
2. **验证医生数据**
```sql
SELECT id, name, status FROM doctors WHERE status = 'approved';
```
应该看到至少3条已认证的医生记录。
3. **测试账号**
- 手机号:13800000001 / 密码:123456 / 张医生(内科)
- 手机号:13800000002 / 密码:123456 / 李医生(儿科)
- 手机号:13800000003 / 密码:123456 / 王医生(皮肤科)
**后端改进:**
已在 `CreateConsult` 方法中添加了更详细的错误提示:
- 检查医生是否存在
- 检查医生是否已认证(status = 'approved')
- 返回包含 doctor_id 的错误信息
---
### 2. Ant Design 警告:Static function can not consume context
**警告信息:**
```
Warning: [antd: message] Static function can not consume context like dynamic theme.
Please use 'App' component instead.
```
**原因:**
使用了 `message.success()` 等静态方法,无法访问 ConfigProvider 提供的动态主题上下文。
**解决方案:**
已在 `App.tsx` 中添加 `<App>` 组件包裹:
```tsx
import { ConfigProvider, App as AntdApp } from 'antd';
<ConfigProvider theme={...}>
<AntdApp>
<RouterProvider router={router} />
</AntdApp>
</ConfigProvider>
```
这样所有页面中使用的静态 `message``modal``notification` 方法都能正确访问主题上下文。
---
## 开发环境初始化清单
### 首次启动前必做
- [ ] 执行数据库迁移(自动)
- [ ] 执行测试数据初始化:`psql -U postgres -d internet_hospital -f server/scripts/seed_doctors.sql`
- [ ] 验证医生数据存在:至少3条 status='approved' 的医生记录
- [ ] 创建测试患者账号(通过注册页面)
### 功能测试流程
1. **注册患者账号**
- 访问 `/register`
- 选择"患者"角色
- 填写手机号、密码、姓名、性别、年龄
2. **AI预问诊(可选)**
- 登录后访问 `/patient/pre-consult`
- 填写症状描述
- 查看AI分析报告
3. **创建问诊**
- 访问 `/patient/doctors`
- 选择医生,点击"图文问诊"或"视频问诊"
- 填写主诉,提交
4. **医生接诊**
- 使用医生账号登录(13800000001 / 123456)
- 访问 `/doctor/consult`
- 在待接诊队列中点击"接诊"
- 开始对话
5. **问诊对话**
- 患者端:`/patient/consult/text/{id}`
- 医生端:`/doctor/consult`(接诊后自动进入对话)
- 双方可实时收发消息(3秒轮询)
---
## 常见问题
### Q: 医生列表为空?
A: 执行 `seed_doctors.sql` 脚本,或手动添加 status='approved' 的医生记录。
### Q: 接诊后看不到消息?
A: 检查浏览器控制台,确认消息API `/consult/{id}/messages` 返回正常。
### Q: 发送消息失败?
A: 检查JWT token是否有效,role字段是否正确(patient/doctor)。
### Q: AI预问诊报告不显示?
A: 确认问诊创建时传递了 `pre_consult_id` 参数。
version: '3.8'
services:
# PostgreSQL 数据库
postgres:
image: postgres:16-alpine
container_name: ih-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: internet_hospital
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# Redis 缓存
redis:
image: redis:7-alpine
container_name: ih-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# 后端 API 服务
api:
build:
context: ./server
dockerfile: Dockerfile
container_name: ih-api
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
- GIN_MODE=release
volumes:
- ./server/configs:/app/configs
volumes:
postgres_data:
redis_data:
# Internet Hospital Project Memory
## Tech Stack
- Frontend: React + TypeScript + Ant Design + Zustand + React Query + Axios
- Backend: Go + Gin + GORM + PostgreSQL
- Auth: JWT (user_id stored in gin context via `c.Get("user_id")`)
## Project Structure
- `web/src/pages/doctor/` - 医生端页面
- `web/src/api/doctorPortal.ts` - 医生端 API 层
- `server/internal/service/doctorportal/` - 后端 handler + service
- `server/internal/model/` - GORM 数据模型
## Key Models
- `model.User` - 用户(含 RealName, Phone, Gender, Age, Avatar)
- `model.Doctor` - 医生(UserID 关联 User)
- `model.PatientProfile` - 患者扩展信息(MedicalHistory, AllergyHistory, EmergencyContact)
- `model.Consultation` - 问诊记录(PatientID, DoctorID, Type, Status, ChiefComplaint)
- `model.Prescription` + `PrescriptionItem` - 处方及药品明细
## Style Conventions (Frontend)
- Card borderRadius: 12
- 统计卡片用 borderLeft 彩色边框区分类型
- 颜色:blue=#1890ff, orange=#fa8c16, green=#52c41a, purple=#722ed1
- 使用 Ant Design Tag 展示状态,颜色语义化
- 页面标题用 `<Title level={4}>` + 图标
## API Routes (doctor-portal)
- GET /doctor-portal/patients - 患者列表(keyword, page, page_size)
- GET /doctor-portal/patient/:id/detail - 患者详情(含问诊+处方记录)
- GET /doctor-portal/patient/:id/summary - 患者概要
## Notes
- 后端 handler 需要从 JWT 获取 user_id,再查 doctors 表得到 doctor.ID
- service.go 中大量方法仍为 TODO stub,需逐步实现
- 前端 PatientRecord 页面已对接真实 API,移除了 mock 数据
# Binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
build/
# Test binary
*.test
# Output of the go coverage tool
*.out
# Dependency directories
vendor/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Config files with secrets
configs/config.yaml
!configs/config.example.yaml
# Logs
*.log
logs/
# OS files
.DS_Store
Thumbs.db
# 构建阶段
FROM golang:1.22-alpine AS builder
WORKDIR /app
# 安装依赖
RUN apk add --no-cache git
# 复制 go.mod 和 go.sum
COPY go.mod go.sum ./
RUN go mod download
# 复制源代码
COPY . .
# 构建
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main cmd/api/main.go
# 运行阶段
FROM alpine:latest
WORKDIR /app
# 安装 CA 证书
RUN apk --no-cache add ca-certificates tzdata
# 设置时区
ENV TZ=Asia/Shanghai
# 从构建阶段复制二进制文件
COPY --from=builder /app/main .
COPY --from=builder /app/configs ./configs
# 暴露端口
EXPOSE 8080
# 运行
CMD ["./main"]
.PHONY: build run test clean deps
# Go 参数
GOCMD=go
GOBUILD=$(GOCMD) build
GORUN=$(GOCMD) run
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
# 二进制文件名
BINARY_NAME=internet-hospital-api
# 构建目录
BUILD_DIR=build
# 主程序入口
MAIN_PATH=cmd/api/main.go
all: deps build
build:
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PATH)
run:
$(GORUN) $(MAIN_PATH)
test:
$(GOTEST) -v ./...
clean:
rm -rf $(BUILD_DIR)
deps:
$(GOMOD) download
$(GOMOD) tidy
# 数据库迁移
migrate:
$(GORUN) $(MAIN_PATH) migrate
# Docker 构建
docker-build:
docker build -t internet-hospital-api .
# 生成 proto 文件
proto:
protoc --go_out=. --go-grpc_out=. proto/*.proto
package main
import (
"fmt"
"log"
"github.com/gin-gonic/gin"
"internet-hospital/internal/model"
"internet-hospital/internal/service/admin"
"internet-hospital/internal/service/consult"
"internet-hospital/internal/service/doctor"
"internet-hospital/internal/service/doctorportal"
"internet-hospital/internal/service/preconsult"
"internet-hospital/internal/service/chronic"
"internet-hospital/internal/service/health"
"internet-hospital/internal/service/user"
"internet-hospital/pkg/ai"
"internet-hospital/pkg/config"
"internet-hospital/pkg/database"
"internet-hospital/pkg/middleware"
)
func main() {
// 加载配置
cfg, err := config.LoadConfig("configs/config.yaml")
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// 初始化数据库
_, err = database.InitPostgres(&cfg.Database)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// 自动迁移新表
log.Println("开始执行数据库表迁移...")
db := database.GetDB()
if err := db.AutoMigrate(
// 用户相关
&model.User{},
&model.UserVerification{},
&model.PatientProfile{},
// 医生相关
&model.Department{},
&model.Doctor{},
&model.DoctorSchedule{},
&model.DoctorReview{},
// 问诊相关
&model.Consultation{},
&model.ConsultMessage{},
&model.VideoRoom{},
// 预问诊
&model.PreConsultation{},
// 处方相关
&model.Medicine{},
&model.Prescription{},
&model.PrescriptionItem{},
// AI & 系统
&model.LabReport{},
&model.FamilyMember{},
// 慢病管理
&model.ChronicRecord{},
&model.RenewalRequest{},
&model.MedicationReminder{},
&model.HealthMetric{},
&model.AIConfig{},
&model.AIUsageLog{},
&model.PromptTemplate{},
&model.SystemLog{},
); err != nil {
log.Printf("Warning: AutoMigrate failed: %v", err)
} else {
log.Println("数据库表迁移成功")
}
log.Println("数据库表检查完成")
// 初始化 Redis
_, err = database.InitRedis(&cfg.Redis)
if err != nil {
log.Fatalf("Failed to connect to redis: %v", err)
}
// 初始化AI客户端:优先从数据库加载配置,否则使用config.yaml
var dbAIConfig model.AIConfig
if err := db.First(&dbAIConfig).Error; err == nil && dbAIConfig.APIKey != "" {
log.Println("从数据库加载AI配置")
ai.UpdateClient(&ai.ClientConfig{
Provider: dbAIConfig.Provider,
APIKey: dbAIConfig.APIKey,
BaseURL: dbAIConfig.BaseURL,
Model: dbAIConfig.Model,
MaxTokens: dbAIConfig.MaxTokens,
Temperature: dbAIConfig.Temperature,
EnableAI: dbAIConfig.EnableAI,
})
} else {
log.Println("从config.yaml加载AI配置")
ai.InitClient(&cfg.AI)
}
// 初始化超级管理员账户
if err := user.InitAdminUser(); err != nil {
log.Printf("Warning: Failed to init admin user: %v", err)
}
// 初始化科室和医生数据
if err := doctor.InitDepartmentsAndDoctors(); err != nil {
log.Printf("Warning: Failed to init departments and doctors: %v", err)
}
// 设置 Gin 模式
gin.SetMode(cfg.Server.Mode)
// 创建 Gin 引擎
r := gin.Default()
// 全局中间件
r.Use(middleware.CORS())
r.Use(middleware.TraceID())
// API 路由组
api := r.Group("/api/v1")
// 注册各服务路由(公开)
userHandler := user.NewHandler()
userHandler.RegisterRoutes(api)
doctorHandler := doctor.NewHandler()
doctorHandler.RegisterRoutes(api)
consultHandler := consult.NewHandler()
consultHandler.RegisterRoutes(api)
preConsultHandler := preconsult.NewHandler()
preConsultHandler.RegisterRoutes(api)
preConsultHandler.RegisterChatRoutes(api)
// 需要认证的路由
authApi := api.Group("")
authApi.Use(middleware.JWTAuth())
authApi.GET("/user/me", userHandler.GetCurrentUser)
// 医生端路由(需要认证 + 医生角色)
doctorPortalHandler := doctorportal.NewHandler()
doctorPortalHandler.RegisterRoutes(authApi)
// 健康档案路由(需要认证)
healthHandler := health.NewHandler()
healthHandler.RegisterRoutes(authApi)
// 慢病管理路由(需要认证)
chronicHandler := chronic.NewHandler()
chronicHandler.RegisterRoutes(authApi)
// 管理端路由(需要认证 + 管理员角色)
adminHandler := admin.NewHandler()
adminHandler.RegisterRoutes(authApi)
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// 启动服务器
addr := fmt.Sprintf(":%d", cfg.Server.Port)
log.Printf("Server starting on %s", addr)
if err := r.Run(addr); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
server:
port: 8080
mode: debug # debug, release, test
name: internet-hospital-api
version: 1.0.0
database:
host: localhost
port: 5432
user: postgres
password: your_password
dbname: internet_hospital
sslmode: disable
redis:
host: localhost
port: 6379
password: ""
db: 0
jwt:
secret: your-super-secret-jwt-key-change-in-production
access_token_ttl: 7200 # 2 hours
refresh_token_ttl: 604800 # 7 days
ai:
provider: deepseek # deepseek, openai, qwen
api_key: "" # 留空则使用模拟模式(MockChat)
base_url: "https://api.deepseek.com"
model: "deepseek-chat"
max_tokens: 2048
temperature: 0.3
module internet-hospital
go 1.22.7
require (
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/redis/go-redis/v9 v9.7.0
github.com/spf13/viper v1.19.0
golang.org/x/crypto v0.29.0
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.12
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.11.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
This diff is collapsed.
package model
import (
"time"
"gorm.io/gorm"
)
// DoctorReview 医生认证审核
type DoctorReview struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;not null;index" json:"user_id"`
Name string `gorm:"type:varchar(50);not null" json:"name"`
Phone string `gorm:"type:varchar(20)" json:"phone"`
LicenseNo string `gorm:"type:varchar(50)" json:"license_no"`
Title string `gorm:"type:varchar(50)" json:"title"`
Hospital string `gorm:"type:varchar(200)" json:"hospital"`
DepartmentName string `gorm:"type:varchar(50)" json:"department_name"`
LicenseImage string `gorm:"type:varchar(500)" json:"license_image"`
QualificationImage string `gorm:"type:varchar(500)" json:"qualification_image"`
Status string `gorm:"type:varchar(20);default:'pending'" json:"status"` // pending/approved/rejected
RejectReason string `gorm:"type:text" json:"reject_reason"`
SubmittedAt time.Time `json:"submitted_at"`
ReviewedAt *time.Time `json:"reviewed_at"`
ReviewedBy *string `gorm:"type:uuid" json:"reviewed_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (DoctorReview) TableName() string {
return "doctor_reviews"
}
// SystemLog 系统操作日志
type SystemLog struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;index" json:"user_id"`
Action string `gorm:"type:varchar(50)" json:"action"`
Resource string `gorm:"type:varchar(200)" json:"resource"`
Detail string `gorm:"type:text" json:"detail"`
IP string `gorm:"type:varchar(50)" json:"ip"`
CreatedAt time.Time `json:"created_at"`
}
func (SystemLog) TableName() string {
return "system_logs"
}
package model
import (
"time"
"gorm.io/gorm"
)
// AIConfig AI模型配置(全局单例,存储在数据库中,管理端可动态修改)
type AIConfig struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Provider string `gorm:"type:varchar(50);not null;default:'deepseek'" json:"provider"`
Model string `gorm:"type:varchar(100);not null;default:'deepseek-chat'" json:"model"`
APIKey string `gorm:"type:varchar(500)" json:"api_key"`
BaseURL string `gorm:"type:varchar(500);default:'https://api.deepseek.com'" json:"base_url"`
MaxTokens int `gorm:"default:2048" json:"max_tokens"`
Temperature float64 `gorm:"type:decimal(3,2);default:0.30" json:"temperature"`
EnableAI bool `gorm:"default:true" json:"enable_ai"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (AIConfig) TableName() string {
return "ai_configs"
}
// AIUsageLog AI调用日志(用于使用统计和调用追踪)
type AIUsageLog struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Scene string `gorm:"type:varchar(50);index" json:"scene"` // pre_consult_chat, pre_consult_analysis, consult_diagnosis, consult_medication
UserID string `gorm:"type:uuid;index" json:"user_id"`
Provider string `gorm:"type:varchar(50)" json:"provider"`
Model string `gorm:"type:varchar(100)" json:"model"`
Prompt string `gorm:"type:text" json:"prompt"` // 系统提示词
RequestContent string `gorm:"type:text" json:"request_content"` // 请求内容摘要
ResponseContent string `gorm:"type:text" json:"response_content"` // 响应内容摘要
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
ResponseTimeMs int `json:"response_time_ms"`
Success bool `gorm:"default:true" json:"success"`
ErrorMessage string `gorm:"type:text" json:"error_message,omitempty"`
IsMock bool `gorm:"default:false" json:"is_mock"` // 是否为模拟调用
CreatedAt time.Time `json:"created_at"`
}
func (AIUsageLog) TableName() string {
return "ai_usage_logs"
}
package model
import (
"time"
"gorm.io/gorm"
)
// ChronicRecord 慢病档案
type ChronicRecord struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
DiseaseName string `gorm:"type:varchar(100);not null" json:"disease_name"`
DiagnosisDate *time.Time `json:"diagnosis_date"`
Hospital string `gorm:"type:varchar(200)" json:"hospital"`
DoctorName string `gorm:"type:varchar(50)" json:"doctor_name"`
CurrentMeds string `gorm:"type:text" json:"current_meds"`
ControlStatus string `gorm:"type:varchar(20);default:'stable'" json:"control_status"` // stable|unstable|worsening
Notes string `gorm:"type:text" json:"notes"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (ChronicRecord) TableName() string { return "chronic_records" }
// RenewalRequest 续方申请
type RenewalRequest struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
ChronicID string `gorm:"type:uuid;index" json:"chronic_id"`
DiseaseName string `gorm:"type:varchar(100)" json:"disease_name"`
Medicines string `gorm:"type:text" json:"medicines"` // JSON array
Reason string `gorm:"type:text" json:"reason"`
Status string `gorm:"type:varchar(20);default:'pending'" json:"status"` // pending|approved|rejected
DoctorID string `gorm:"type:uuid" json:"doctor_id"`
DoctorName string `gorm:"type:varchar(50)" json:"doctor_name"`
DoctorNote string `gorm:"type:text" json:"doctor_note"`
PrescriptionID string `gorm:"type:uuid" json:"prescription_id"`
AIAdvice string `gorm:"type:text" json:"ai_advice"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (RenewalRequest) TableName() string { return "renewal_requests" }
// MedicationReminder 用药提醒
type MedicationReminder struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
MedicineName string `gorm:"type:varchar(100);not null" json:"medicine_name"`
Dosage string `gorm:"type:varchar(50)" json:"dosage"`
Frequency string `gorm:"type:varchar(50)" json:"frequency"` // 每日一次|每日两次|每日三次
RemindTimes string `gorm:"type:varchar(200)" json:"remind_times"` // JSON ["08:00","20:00"]
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
IsActive bool `gorm:"default:true" json:"is_active"`
Notes string `gorm:"type:text" json:"notes"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (MedicationReminder) TableName() string { return "medication_reminders" }
// HealthMetric 健康指标记录
type HealthMetric struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
MetricType string `gorm:"type:varchar(50);not null" json:"metric_type"` // blood_pressure|blood_sugar|weight|heart_rate|temperature
Value1 float64 `json:"value1"` // 主值(收缩压/血糖/体重/心率/体温)
Value2 float64 `json:"value2"` // 副值(舒张压)
Unit string `gorm:"type:varchar(20)" json:"unit"`
RecordedAt time.Time `json:"recorded_at"`
Notes string `gorm:"type:text" json:"notes"`
CreatedAt time.Time `json:"created_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (HealthMetric) TableName() string { return "health_metrics" }
package model
import (
"time"
"gorm.io/gorm"
)
type Consultation struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
PatientID string `gorm:"type:uuid;index;not null" json:"patient_id"`
DoctorID string `gorm:"type:uuid;index;not null" json:"doctor_id"`
Type string `gorm:"type:varchar(20);not null" json:"type"`
Status string `gorm:"type:varchar(20);default:'pending'" json:"status"`
ChiefComplaint string `gorm:"type:text" json:"chief_complaint"`
MedicalHistory string `gorm:"type:text" json:"medical_history"`
AISessionID string `gorm:"type:varchar(100)" json:"ai_session_id"`
PrescriptionID *string `gorm:"type:uuid" json:"prescription_id"`
StartedAt *time.Time `json:"started_at"`
EndedAt *time.Time `json:"ended_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (Consultation) TableName() string {
return "consultations"
}
type ConsultMessage struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
ConsultID string `gorm:"type:uuid;index;not null" json:"consult_id"`
SenderType string `gorm:"type:varchar(20);not null" json:"sender_type"`
SenderID string `gorm:"type:uuid" json:"sender_id"`
Content string `gorm:"type:text;not null" json:"content"`
ContentType string `gorm:"type:varchar(20);default:'text'" json:"content_type"`
CreatedAt time.Time `json:"created_at"`
}
func (ConsultMessage) TableName() string {
return "consult_messages"
}
type VideoRoom struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
ConsultID string `gorm:"type:uuid;uniqueIndex;not null" json:"consult_id"`
RoomID string `gorm:"type:varchar(100);not null" json:"room_id"`
SDKAppID int `gorm:"not null" json:"sdk_app_id"`
Status string `gorm:"type:varchar(20);default:'waiting'" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (VideoRoom) TableName() string {
return "video_rooms"
}
package model
import (
"time"
"github.com/lib/pq"
"gorm.io/gorm"
)
type Doctor struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;uniqueIndex;not null" json:"user_id"`
Name string `gorm:"type:varchar(50);not null" json:"name"`
Avatar string `gorm:"type:varchar(500)" json:"avatar"`
LicenseNo string `gorm:"type:varchar(50);uniqueIndex" json:"license_no"`
Title string `gorm:"type:varchar(50)" json:"title"`
DepartmentID string `gorm:"type:uuid;index" json:"department_id"`
Hospital string `gorm:"type:varchar(200)" json:"hospital"`
Introduction string `gorm:"type:text" json:"introduction"`
Specialties pq.StringArray `gorm:"type:text[]" json:"specialties"`
Rating float64 `gorm:"type:decimal(3,2);default:5.0" json:"rating"`
ConsultCount int `gorm:"default:0" json:"consult_count"`
Price int `gorm:"default:0" json:"price"`
IsOnline bool `gorm:"default:false" json:"is_online"`
AIAssistEnabled bool `gorm:"default:true" json:"ai_assist_enabled"`
Status string `gorm:"type:varchar(20);default:'pending'" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (Doctor) TableName() string {
return "doctors"
}
type Department struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
Name string `gorm:"type:varchar(50);not null" json:"name"`
Icon string `gorm:"type:varchar(200)" json:"icon"`
ParentID *string `gorm:"type:uuid;index" json:"parent_id"`
SortOrder int `gorm:"default:0" json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (Department) TableName() string {
return "departments"
}
type DoctorSchedule struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
DoctorID string `gorm:"type:uuid;index;not null" json:"doctor_id"`
Date string `gorm:"type:date;not null" json:"date"`
StartTime string `gorm:"type:time;not null" json:"start_time"`
EndTime string `gorm:"type:time;not null" json:"end_time"`
MaxCount int `gorm:"default:10" json:"max_count"`
Remaining int `gorm:"default:10" json:"remaining"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (DoctorSchedule) TableName() string {
return "doctor_schedules"
}
package model
import (
"time"
"gorm.io/gorm"
)
// LabReport 检验报告
type LabReport struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
Title string `gorm:"type:varchar(200);not null" json:"title"`
ReportDate *time.Time `json:"report_date"`
FileURL string `gorm:"type:varchar(500)" json:"file_url"`
FileType string `gorm:"type:varchar(20)" json:"file_type"` // image | pdf
AIInterpret string `gorm:"type:text" json:"ai_interpret"`
Category string `gorm:"type:varchar(50)" json:"category"` // 血常规|尿常规|生化|影像|其他
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (LabReport) TableName() string { return "lab_reports" }
// FamilyMember 家庭成员
type FamilyMember struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
Name string `gorm:"type:varchar(50);not null" json:"name"`
Relation string `gorm:"type:varchar(20);not null" json:"relation"` // 配偶|父亲|母亲|子女|其他
Gender string `gorm:"type:varchar(10)" json:"gender"`
BirthDate *time.Time `json:"birth_date"`
Phone string `gorm:"type:varchar(20)" json:"phone"`
MedicalHistory string `gorm:"type:text" json:"medical_history"`
AllergyHistory string `gorm:"type:text" json:"allergy_history"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (FamilyMember) TableName() string { return "family_members" }
package model
import (
"time"
"gorm.io/gorm"
)
// PreConsultation AI预问诊会话(仅对话式)
// ChatHistory 格式: [{"role":"user","content":"..."},{"role":"assistant","content":"..."}]
type PreConsultation struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
PatientID string `gorm:"type:uuid;index;not null" json:"patient_id"`
Status string `gorm:"type:varchar(20);default:'in_progress'" json:"status"` // in_progress, completed, expired
PatientName string `gorm:"type:varchar(50)" json:"patient_name"`
PatientGender string `gorm:"type:varchar(10)" json:"patient_gender"`
PatientAge int `json:"patient_age"`
ChiefComplaint string `gorm:"type:text" json:"chief_complaint"` // 主诉(首条用户消息)
ChatHistory string `gorm:"type:text" json:"chat_history"` // 对话历史 JSON: [{role,content}]
AIAnalysis string `gorm:"type:text" json:"ai_analysis"` // AI分析报告(markdown)
AIDepartment string `gorm:"type:varchar(50)" json:"ai_department"` // AI推荐科室
AISeverity string `gorm:"type:varchar(20)" json:"ai_severity"` // 严重程度: mild, moderate, severe
ConsultationID *string `gorm:"type:uuid" json:"consultation_id"` // 关联的正式问诊ID
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (PreConsultation) TableName() string {
return "pre_consultations"
}
package model
import (
"time"
"gorm.io/gorm"
)
// Medicine 药品表
type Medicine struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
Name string `gorm:"type:varchar(200);not null" json:"name"`
GenericName string `gorm:"type:varchar(200)" json:"generic_name"`
Specification string `gorm:"type:varchar(200)" json:"specification"`
Manufacturer string `gorm:"type:varchar(200)" json:"manufacturer"`
Category string `gorm:"type:varchar(50)" json:"category"`
Unit string `gorm:"type:varchar(20)" json:"unit"`
Price int `gorm:"type:int;not null;default:0" json:"price"` // 单位:分
Stock int `gorm:"type:int;not null;default:0" json:"stock"`
StockWarning int `gorm:"type:int;default:50" json:"stock_warning"`
Status string `gorm:"type:varchar(20);default:'normal'" json:"status"` // normal, low_stock, out_of_stock
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (Medicine) TableName() string {
return "medicines"
}
// Prescription 处方表
type Prescription struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
PrescriptionNo string `gorm:"type:varchar(50);uniqueIndex;not null" json:"prescription_no"`
ConsultID string `gorm:"type:uuid;index" json:"consult_id"`
PatientID string `gorm:"type:uuid;index;not null" json:"patient_id"`
DoctorID string `gorm:"type:uuid;index;not null" json:"doctor_id"`
PatientName string `gorm:"type:varchar(50)" json:"patient_name"`
PatientGender string `gorm:"type:varchar(10)" json:"patient_gender"`
PatientAge int `gorm:"type:int" json:"patient_age"`
DoctorName string `gorm:"type:varchar(50)" json:"doctor_name"`
Diagnosis string `gorm:"type:text" json:"diagnosis"`
AllergyHistory string `gorm:"type:text" json:"allergy_history"`
Remark string `gorm:"type:text" json:"remark"`
TotalAmount int `gorm:"type:int;default:0" json:"total_amount"` // 单位:分
Status string `gorm:"type:varchar(20);default:'pending'" json:"status"` // pending, signed, approved, rejected, dispensed, completed
WarningLevel string `gorm:"type:varchar(20);default:'normal'" json:"warning_level"` // normal, warning, rejected
WarningReason string `gorm:"type:text" json:"warning_reason"`
ReviewedBy string `gorm:"type:uuid" json:"reviewed_by"`
ReviewedAt *time.Time `json:"reviewed_at"`
SignedAt *time.Time `json:"signed_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Items []PrescriptionItem `gorm:"foreignKey:PrescriptionID" json:"items"`
}
func (Prescription) TableName() string {
return "prescriptions"
}
// PrescriptionItem 处方药品明细
type PrescriptionItem struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
PrescriptionID string `gorm:"type:uuid;index;not null" json:"prescription_id"`
MedicineID string `gorm:"type:uuid;index;not null" json:"medicine_id"`
MedicineName string `gorm:"type:varchar(200);not null" json:"medicine_name"`
Specification string `gorm:"type:varchar(200)" json:"specification"`
Usage string `gorm:"type:varchar(50)" json:"usage"`
Dosage string `gorm:"type:varchar(50)" json:"dosage"`
Frequency string `gorm:"type:varchar(50)" json:"frequency"`
Days int `gorm:"type:int;default:3" json:"days"`
Quantity int `gorm:"type:int;default:1" json:"quantity"`
Unit string `gorm:"type:varchar(20)" json:"unit"`
Price int `gorm:"type:int;default:0" json:"price"` // 单价,分
Amount int `gorm:"type:int;default:0" json:"amount"` // 小计,分
Note string `gorm:"type:text" json:"note"`
CreatedAt time.Time `json:"created_at"`
}
func (PrescriptionItem) TableName() string {
return "prescription_items"
}
package model
import (
"time"
"gorm.io/gorm"
)
// PromptTemplate AI提示词模板
type PromptTemplate struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
TemplateKey string `gorm:"type:varchar(100);uniqueIndex;not null" json:"template_key"` // 模板key,用于代码中取值
Name string `gorm:"type:varchar(100);not null" json:"name"` // 模板名称
Scene string `gorm:"type:varchar(50)" json:"scene"` // 应用场景
Content string `gorm:"type:text;not null" json:"content"` // Prompt内容
Status string `gorm:"type:varchar(20);default:'active'" json:"status"` // active | disabled
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (PromptTemplate) TableName() string {
return "prompt_templates"
}
package model
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
Username *string `gorm:"type:varchar(50);uniqueIndex" json:"username,omitempty"`
Phone string `gorm:"type:varchar(20);uniqueIndex" json:"phone"`
Password string `gorm:"type:varchar(255)" json:"-"`
RealName string `gorm:"type:varchar(50)" json:"real_name"`
Avatar string `gorm:"type:varchar(500)" json:"avatar"`
Role string `gorm:"type:varchar(20);default:'patient'" json:"role"`
IsVerified bool `gorm:"default:false" json:"is_verified"`
Status string `gorm:"type:varchar(20);default:'active'" json:"status"` // active | disabled
Gender string `gorm:"type:varchar(10)" json:"gender,omitempty"`
Age int `gorm:"type:int" json:"age,omitempty"`
CACertID string `gorm:"type:varchar(100)" json:"ca_cert_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (User) TableName() string {
return "users"
}
type UserVerification struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
RealName string `gorm:"type:varchar(50);not null" json:"real_name"`
IDCard string `gorm:"type:varchar(255);not null" json:"-"`
Status string `gorm:"type:varchar(20);default:'pending'" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (UserVerification) TableName() string {
return "user_verifications"
}
// PatientProfile 患者扩展信息
type PatientProfile struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;uniqueIndex;not null" json:"user_id"`
Gender string `gorm:"type:varchar(10)" json:"gender"` // male | female
BirthDate *time.Time `json:"birth_date"`
IDCardHash string `gorm:"type:varchar(255)" json:"-"` // 身份证号哈希
MedicalHistory string `gorm:"type:text" json:"medical_history"` // 既往病史
AllergyHistory string `gorm:"type:text" json:"allergy_history"` // 过敏史
EmergencyContact string `gorm:"type:varchar(20)" json:"emergency_contact"` // 紧急联系人电话
InsuranceType string `gorm:"type:varchar(50)" json:"insurance_type"` // 医保类型
InsuranceNo string `gorm:"type:varchar(50)" json:"insurance_no"` // 医保号
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (PatientProfile) TableName() string {
return "patient_profiles"
}
package admin
import (
"context"
"time"
"internet-hospital/internal/model"
"internet-hospital/pkg/ai"
"internet-hospital/pkg/database"
)
// GetAIConfig 获取当前AI配置(单例,只有一条记录)
func (s *Service) GetAIConfig(ctx context.Context) (*model.AIConfig, error) {
var cfg model.AIConfig
err := s.db.First(&cfg).Error
if err != nil {
// 不存在则返回默认配置
return &model.AIConfig{
Provider: "deepseek",
Model: "deepseek-chat",
BaseURL: "https://api.deepseek.com",
MaxTokens: 2048,
Temperature: 0.3,
EnableAI: true,
}, nil
}
return &cfg, nil
}
// SaveAIConfig 保存AI配置(upsert)
func (s *Service) SaveAIConfig(ctx context.Context, req *SaveAIConfigRequest) (*model.AIConfig, error) {
var cfg model.AIConfig
err := s.db.First(&cfg).Error
if err != nil {
// 不存在,创建
cfg = model.AIConfig{
Provider: req.Provider,
Model: req.Model,
APIKey: req.APIKey,
BaseURL: req.BaseURL,
MaxTokens: req.MaxTokens,
Temperature: req.Temperature,
EnableAI: req.EnableAI,
}
if err := s.db.Create(&cfg).Error; err != nil {
return nil, err
}
} else {
// 存在,更新
updates := map[string]interface{}{
"provider": req.Provider,
"model": req.Model,
"base_url": req.BaseURL,
"max_tokens": req.MaxTokens,
"temperature": req.Temperature,
"enable_ai": req.EnableAI,
"updated_at": time.Now(),
}
// 只有非空时才更新API Key(前端可能不传以保留原值)
if req.APIKey != "" {
updates["api_key"] = req.APIKey
}
if err := s.db.Model(&cfg).Updates(updates).Error; err != nil {
return nil, err
}
// 重新读取
s.db.First(&cfg)
}
// 动态更新全局AI客户端
ai.UpdateClient(&ai.ClientConfig{
Provider: cfg.Provider,
APIKey: cfg.APIKey,
BaseURL: cfg.BaseURL,
Model: cfg.Model,
MaxTokens: cfg.MaxTokens,
Temperature: cfg.Temperature,
EnableAI: cfg.EnableAI,
})
return &cfg, nil
}
// GetAIUsageStats 获取AI使用统计
func (s *Service) GetAIUsageStats(ctx context.Context) (*AIUsageStats, error) {
db := database.GetDB()
var stats AIUsageStats
// 今日调用次数
today := time.Now().Format("2006-01-02")
db.Model(&model.AIUsageLog{}).
Where("DATE(created_at) = ?", today).
Count(&stats.TodayCount)
// 本月Token消耗
monthStart := time.Now().Format("2006-01") + "-01"
db.Model(&model.AIUsageLog{}).
Where("created_at >= ?", monthStart).
Select("COALESCE(SUM(total_tokens), 0)").
Scan(&stats.MonthTokens)
// 平均响应时间(最近100条)
db.Model(&model.AIUsageLog{}).
Where("success = true").
Order("created_at DESC").
Limit(100).
Select("COALESCE(AVG(response_time_ms), 0)").
Scan(&stats.AvgResponseMs)
return &stats, nil
}
// SaveAIConfigRequest 保存配置请求
type SaveAIConfigRequest struct {
Provider string `json:"provider" binding:"required"`
Model string `json:"model" binding:"required"`
APIKey string `json:"api_key"`
BaseURL string `json:"base_url" binding:"required"`
MaxTokens int `json:"max_tokens"`
Temperature float64 `json:"temperature"`
EnableAI bool `json:"enable_ai"`
}
// AIUsageStats 使用统计
type AIUsageStats struct {
TodayCount int64 `json:"today_count"`
MonthTokens int64 `json:"month_tokens"`
AvgResponseMs int64 `json:"avg_response_ms"`
}
This diff is collapsed.
package admin
import (
"github.com/gin-gonic/gin"
"internet-hospital/pkg/response"
)
// ==================== 药品库管理 Handler ====================
// GetMedicineStats 药品统计
func (h *Handler) GetMedicineStats(c *gin.Context) {
stats, err := h.service.GetMedicineStats(c.Request.Context())
if err != nil {
response.Error(c, 500, "获取药品统计失败")
return
}
response.Success(c, stats)
}
// GetMedicineList 药品列表
func (h *Handler) GetMedicineList(c *gin.Context) {
var params MedicineListParams
if err := c.ShouldBindQuery(&params); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
result, err := h.service.GetMedicineList(c.Request.Context(), &params)
if err != nil {
response.Error(c, 500, "获取药品列表失败")
return
}
response.Success(c, result)
}
// CreateMedicine 创建药品
func (h *Handler) CreateMedicine(c *gin.Context) {
var req CreateMedicineReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误: "+err.Error())
return
}
medicine, err := h.service.CreateMedicine(c.Request.Context(), &req)
if err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, medicine)
}
// UpdateMedicine 更新药品
func (h *Handler) UpdateMedicine(c *gin.Context) {
id := c.Param("id")
var req UpdateMedicineReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
medicine, err := h.service.UpdateMedicine(c.Request.Context(), id, &req)
if err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, medicine)
}
// DeleteMedicine 删除药品
func (h *Handler) DeleteMedicine(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteMedicine(c.Request.Context(), id); err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, nil)
}
// SearchMedicines 搜索药品(医生开处方用)
func (h *Handler) SearchMedicines(c *gin.Context) {
keyword := c.Query("keyword")
medicines, err := h.service.SearchMedicines(c.Request.Context(), keyword)
if err != nil {
response.Error(c, 500, "搜索药品失败")
return
}
response.Success(c, medicines)
}
package admin
import (
"context"
"fmt"
"github.com/google/uuid"
"internet-hospital/internal/model"
)
// ==================== 药品管理请求/响应 ====================
type MedicineListParams struct {
Keyword string `form:"keyword"`
Category string `form:"category"`
Status string `form:"status"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type CreateMedicineReq struct {
Name string `json:"name" binding:"required"`
GenericName string `json:"generic_name"`
Specification string `json:"specification" binding:"required"`
Manufacturer string `json:"manufacturer"`
Category string `json:"category" binding:"required"`
Unit string `json:"unit"`
Price int `json:"price"`
Stock int `json:"stock"`
StockWarning int `json:"stock_warning"`
}
type UpdateMedicineReq struct {
Name string `json:"name"`
GenericName string `json:"generic_name"`
Specification string `json:"specification"`
Manufacturer string `json:"manufacturer"`
Category string `json:"category"`
Unit string `json:"unit"`
Price int `json:"price"`
Stock int `json:"stock"`
StockWarning int `json:"stock_warning"`
}
type MedicineStatsResp struct {
TotalCount int `json:"total_count"`
NormalCount int `json:"normal_count"`
LowStockCount int `json:"low_stock_count"`
OutOfStockCount int `json:"out_of_stock_count"`
}
// ==================== 药品管理服务 ====================
func (s *Service) GetMedicineStats(ctx context.Context) (*MedicineStatsResp, error) {
var stats MedicineStatsResp
s.db.Model(&model.Medicine{}).Count(new(int64))
var total int64
s.db.Model(&model.Medicine{}).Count(&total)
stats.TotalCount = int(total)
var normal int64
s.db.Model(&model.Medicine{}).Where("status = ?", "normal").Count(&normal)
stats.NormalCount = int(normal)
var low int64
s.db.Model(&model.Medicine{}).Where("status = ?", "low_stock").Count(&low)
stats.LowStockCount = int(low)
var out int64
s.db.Model(&model.Medicine{}).Where("status = ?", "out_of_stock").Count(&out)
stats.OutOfStockCount = int(out)
return &stats, nil
}
func (s *Service) GetMedicineList(ctx context.Context, params *MedicineListParams) (map[string]interface{}, error) {
if params.Page <= 0 {
params.Page = 1
}
if params.PageSize <= 0 {
params.PageSize = 10
}
query := s.db.Model(&model.Medicine{})
if params.Keyword != "" {
kw := "%" + params.Keyword + "%"
query = query.Where("name ILIKE ? OR generic_name ILIKE ?", kw, kw)
}
if params.Category != "" {
query = query.Where("category = ?", params.Category)
}
if params.Status != "" {
query = query.Where("status = ?", params.Status)
}
var total int64
query.Count(&total)
var medicines []model.Medicine
query.Order("created_at DESC").
Offset((params.Page - 1) * params.PageSize).
Limit(params.PageSize).
Find(&medicines)
return map[string]interface{}{
"list": medicines,
"total": total,
}, nil
}
func (s *Service) CreateMedicine(ctx context.Context, req *CreateMedicineReq) (*model.Medicine, error) {
medicine := &model.Medicine{
ID: uuid.New().String(),
Name: req.Name,
GenericName: req.GenericName,
Specification: req.Specification,
Manufacturer: req.Manufacturer,
Category: req.Category,
Unit: req.Unit,
Price: req.Price,
Stock: req.Stock,
StockWarning: req.StockWarning,
}
medicine.Status = calcStockStatus(medicine.Stock, medicine.StockWarning)
if err := s.db.Create(medicine).Error; err != nil {
return nil, fmt.Errorf("创建药品失败: %v", err)
}
return medicine, nil
}
func (s *Service) UpdateMedicine(ctx context.Context, id string, req *UpdateMedicineReq) (*model.Medicine, error) {
var medicine model.Medicine
if err := s.db.First(&medicine, "id = ?", id).Error; err != nil {
return nil, fmt.Errorf("药品不存在")
}
updates := make(map[string]interface{})
if req.Name != "" {
updates["name"] = req.Name
}
if req.GenericName != "" {
updates["generic_name"] = req.GenericName
}
if req.Specification != "" {
updates["specification"] = req.Specification
}
if req.Manufacturer != "" {
updates["manufacturer"] = req.Manufacturer
}
if req.Category != "" {
updates["category"] = req.Category
}
if req.Unit != "" {
updates["unit"] = req.Unit
}
if req.Price > 0 {
updates["price"] = req.Price
}
if req.Stock >= 0 {
updates["stock"] = req.Stock
updates["status"] = calcStockStatus(req.Stock, medicine.StockWarning)
}
if req.StockWarning >= 0 {
updates["stock_warning"] = req.StockWarning
stock := medicine.Stock
if req.Stock >= 0 {
stock = req.Stock
}
updates["status"] = calcStockStatus(stock, req.StockWarning)
}
if err := s.db.Model(&medicine).Updates(updates).Error; err != nil {
return nil, fmt.Errorf("更新药品失败: %v", err)
}
s.db.First(&medicine, "id = ?", id)
return &medicine, nil
}
func (s *Service) DeleteMedicine(ctx context.Context, id string) error {
result := s.db.Where("id = ?", id).Delete(&model.Medicine{})
if result.RowsAffected == 0 {
return fmt.Errorf("药品不存在")
}
return result.Error
}
func (s *Service) SearchMedicines(ctx context.Context, keyword string) ([]model.Medicine, error) {
var medicines []model.Medicine
query := s.db.Model(&model.Medicine{}).Where("stock > 0")
if keyword != "" {
kw := "%" + keyword + "%"
query = query.Where("name ILIKE ? OR generic_name ILIKE ?", kw, kw)
}
query.Limit(20).Find(&medicines)
return medicines, nil
}
func calcStockStatus(stock, warning int) string {
if stock <= 0 {
return "out_of_stock"
}
if stock <= warning {
return "low_stock"
}
return "normal"
}
package admin
import (
"github.com/gin-gonic/gin"
"internet-hospital/pkg/response"
)
// ==================== 处方监管 Handler ====================
// GetPrescriptionStats 处方统计
func (h *Handler) GetPrescriptionStats(c *gin.Context) {
stats, err := h.service.GetPrescriptionStats(c.Request.Context())
if err != nil {
response.Error(c, 500, "获取处方统计失败")
return
}
response.Success(c, stats)
}
// GetPrescriptionList 处方列表
func (h *Handler) GetPrescriptionList(c *gin.Context) {
var params PrescriptionListParams
if err := c.ShouldBindQuery(&params); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
result, err := h.service.GetPrescriptionList(c.Request.Context(), &params)
if err != nil {
response.Error(c, 500, "获取处方列表失败")
return
}
response.Success(c, result)
}
// GetPrescriptionDetailAdmin 处方详情(管理端)
func (h *Handler) GetPrescriptionDetailAdmin(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetPrescriptionDetail(c.Request.Context(), id)
if err != nil {
response.Error(c, 404, err.Error())
return
}
response.Success(c, result)
}
// ReviewPrescription 审核处方
func (h *Handler) ReviewPrescription(c *gin.Context) {
id := c.Param("id")
userID, _ := c.Get("user_id")
var req struct {
Action string `json:"action" binding:"required"` // approve, reject
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
reviewerID := ""
if uid, ok := userID.(string); ok {
reviewerID = uid
}
if err := h.service.ReviewPrescription(c.Request.Context(), id, req.Action, req.Reason, reviewerID); err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, nil)
}
package admin
import (
"context"
"fmt"
"time"
"internet-hospital/internal/model"
)
// ==================== 处方监管请求/响应 ====================
type PrescriptionListParams struct {
Keyword string `form:"keyword"`
Status string `form:"status"`
WarningLevel string `form:"warning_level"`
StartDate string `form:"start_date"`
EndDate string `form:"end_date"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type PrescriptionStatsResp struct {
TotalCount int `json:"total_count"`
NormalCount int `json:"normal_count"`
WarningCount int `json:"warning_count"`
RejectedCount int `json:"rejected_count"`
}
type PrescriptionDetailResp struct {
model.Prescription
Items []model.PrescriptionItem `json:"items"`
}
// ==================== 处方监管服务 ====================
func (s *Service) GetPrescriptionStats(ctx context.Context) (*PrescriptionStatsResp, error) {
var stats PrescriptionStatsResp
today := time.Now().Format("2006-01-02")
baseQuery := s.db.Model(&model.Prescription{}).Where("DATE(created_at) = ?", today)
var total int64
baseQuery.Count(&total)
stats.TotalCount = int(total)
var normal int64
s.db.Model(&model.Prescription{}).Where("DATE(created_at) = ? AND warning_level = ?", today, "normal").Count(&normal)
stats.NormalCount = int(normal)
var warning int64
s.db.Model(&model.Prescription{}).Where("DATE(created_at) = ? AND warning_level = ?", today, "warning").Count(&warning)
stats.WarningCount = int(warning)
var rejected int64
s.db.Model(&model.Prescription{}).Where("DATE(created_at) = ? AND warning_level = ?", today, "rejected").Count(&rejected)
stats.RejectedCount = int(rejected)
return &stats, nil
}
func (s *Service) GetPrescriptionList(ctx context.Context, params *PrescriptionListParams) (map[string]interface{}, error) {
if params.Page <= 0 {
params.Page = 1
}
if params.PageSize <= 0 {
params.PageSize = 10
}
query := s.db.Model(&model.Prescription{})
if params.Keyword != "" {
kw := "%" + params.Keyword + "%"
query = query.Where("prescription_no ILIKE ? OR patient_name ILIKE ? OR doctor_name ILIKE ?", kw, kw, kw)
}
if params.Status != "" {
query = query.Where("status = ?", params.Status)
}
if params.WarningLevel != "" {
query = query.Where("warning_level = ?", params.WarningLevel)
}
if params.StartDate != "" {
query = query.Where("created_at >= ?", params.StartDate)
}
if params.EndDate != "" {
query = query.Where("created_at <= ?", params.EndDate+" 23:59:59")
}
var total int64
query.Count(&total)
var prescriptions []model.Prescription
query.Preload("Items").Order("created_at DESC").
Offset((params.Page - 1) * params.PageSize).
Limit(params.PageSize).
Find(&prescriptions)
return map[string]interface{}{
"list": prescriptions,
"total": total,
}, nil
}
func (s *Service) GetPrescriptionDetail(ctx context.Context, id string) (*model.Prescription, error) {
var prescription model.Prescription
if err := s.db.Preload("Items").First(&prescription, "id = ?", id).Error; err != nil {
return nil, fmt.Errorf("处方不存在")
}
return &prescription, nil
}
func (s *Service) ReviewPrescription(ctx context.Context, id string, action string, reason string, reviewerID string) error {
var prescription model.Prescription
if err := s.db.First(&prescription, "id = ?", id).Error; err != nil {
return fmt.Errorf("处方不存在")
}
now := time.Now()
updates := map[string]interface{}{
"reviewed_by": reviewerID,
"reviewed_at": &now,
}
switch action {
case "approve":
updates["status"] = "approved"
updates["warning_level"] = "normal"
case "reject":
updates["status"] = "rejected"
updates["warning_level"] = "rejected"
updates["warning_reason"] = reason
default:
return fmt.Errorf("无效的审核操作")
}
return s.db.Model(&prescription).Updates(updates).Error
}
package admin
import (
"github.com/gin-gonic/gin"
"internet-hospital/internal/model"
"internet-hospital/pkg/response"
)
// GetPromptTemplates 获取所有模板列表
func (h *Handler) GetPromptTemplates(c *gin.Context) {
var templates []model.PromptTemplate
if err := h.service.db.Order("id desc").Find(&templates).Error; err != nil {
response.Error(c, 500, "获取模板列表失败")
return
}
response.Success(c, templates)
}
// GetPromptTemplateByKey 根据key获取模板
func (h *Handler) GetPromptTemplateByKey(c *gin.Context) {
key := c.Param("key")
var template model.PromptTemplate
if err := h.service.db.Where("template_key = ? AND status = ?", key, "active").First(&template).Error; err != nil {
response.Error(c, 404, "模板不存在")
return
}
response.Success(c, template)
}
// CreatePromptTemplate 创建模板
func (h *Handler) CreatePromptTemplate(c *gin.Context) {
var req struct {
TemplateKey string `json:"template_key" binding:"required"`
Name string `json:"name" binding:"required"`
Scene string `json:"scene"`
Content string `json:"content" binding:"required"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误: "+err.Error())
return
}
// 检查key是否重复
var count int64
h.service.db.Model(&model.PromptTemplate{}).Where("template_key = ?", req.TemplateKey).Count(&count)
if count > 0 {
response.Error(c, 400, "模板Key已存在")
return
}
status := req.Status
if status == "" {
status = "active"
}
template := &model.PromptTemplate{
TemplateKey: req.TemplateKey,
Name: req.Name,
Scene: req.Scene,
Content: req.Content,
Status: status,
}
if err := h.service.db.Create(template).Error; err != nil {
response.Error(c, 500, "创建模板失败: "+err.Error())
return
}
response.Success(c, template)
}
// UpdatePromptTemplate 更新模板
func (h *Handler) UpdatePromptTemplate(c *gin.Context) {
id := c.Param("id")
var req struct {
TemplateKey string `json:"template_key"`
Name string `json:"name"`
Scene string `json:"scene"`
Content string `json:"content"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
var template model.PromptTemplate
if err := h.service.db.First(&template, id).Error; err != nil {
response.Error(c, 404, "模板不存在")
return
}
updates := make(map[string]interface{})
if req.TemplateKey != "" {
// 检查新key是否与其他模板冲突
var count int64
h.service.db.Model(&model.PromptTemplate{}).Where("template_key = ? AND id != ?", req.TemplateKey, id).Count(&count)
if count > 0 {
response.Error(c, 400, "模板Key已被其他模板使用")
return
}
updates["template_key"] = req.TemplateKey
}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Scene != "" {
updates["scene"] = req.Scene
}
if req.Content != "" {
updates["content"] = req.Content
}
if req.Status != "" {
updates["status"] = req.Status
}
// 如果激活该模板,自动停用同场景的其他模板
scene := req.Scene
if scene == "" {
scene = template.Scene
}
if req.Status == "active" && scene != "" {
h.service.db.Model(&model.PromptTemplate{}).
Where("scene = ? AND id != ? AND status = ?", scene, template.ID, "active").
Update("status", "disabled")
}
if err := h.service.db.Model(&template).Updates(updates).Error; err != nil {
response.Error(c, 500, "更新模板失败")
return
}
response.Success(c, nil)
}
// ActivatePromptTemplate 激活指定模板(同场景其他模板自动停用)
func (h *Handler) ActivatePromptTemplate(c *gin.Context) {
id := c.Param("id")
var template model.PromptTemplate
if err := h.service.db.First(&template, id).Error; err != nil {
response.Error(c, 404, "模板不存在")
return
}
// 停用同场景其他模板
if template.Scene != "" {
h.service.db.Model(&model.PromptTemplate{}).
Where("scene = ? AND id != ?", template.Scene, template.ID).
Update("status", "disabled")
}
// 激活当前模板
h.service.db.Model(&template).Update("status", "active")
response.Success(c, nil)
}
// DeletePromptTemplate 删除模板
func (h *Handler) DeletePromptTemplate(c *gin.Context) {
id := c.Param("id")
if err := h.service.db.Delete(&model.PromptTemplate{}, id).Error; err != nil {
response.Error(c, 500, "删除模板失败")
return
}
response.Success(c, nil)
}
This diff is collapsed.
package admin
import (
"github.com/gin-gonic/gin"
"internet-hospital/pkg/response"
)
// UpdateUser 更新用户信息(通用)
func (h *Handler) UpdateUser(c *gin.Context) {
id := c.Param("id")
var req UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误: "+err.Error())
return
}
if err := h.service.UpdateUser(c.Request.Context(), id, &req); err != nil {
response.Error(c, 500, "更新失败: "+err.Error())
return
}
response.Success(c, nil)
}
// DeleteUser 删除用户(通用)
func (h *Handler) DeleteUser(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteUser(c.Request.Context(), id); err != nil {
response.Error(c, 500, "删除失败: "+err.Error())
return
}
response.Success(c, nil)
}
// CreatePatient 创建患者
func (h *Handler) CreatePatient(c *gin.Context) {
var req CreatePatientRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误: "+err.Error())
return
}
if err := h.service.CreatePatient(c.Request.Context(), &req); err != nil {
response.Error(c, 500, "创建失败: "+err.Error())
return
}
response.Success(c, nil)
}
// UpdatePatient 更新患者信息
func (h *Handler) UpdatePatient(c *gin.Context) {
id := c.Param("id")
var req UpdatePatientRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误: "+err.Error())
return
}
if err := h.service.UpdatePatient(c.Request.Context(), id, &req); err != nil {
response.Error(c, 500, "更新失败: "+err.Error())
return
}
response.Success(c, nil)
}
// DeletePatient 删除患者
func (h *Handler) DeletePatient(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeletePatient(c.Request.Context(), id); err != nil {
response.Error(c, 500, "删除失败: "+err.Error())
return
}
response.Success(c, nil)
}
// UpdateDoctor 更新医生信息
func (h *Handler) UpdateDoctor(c *gin.Context) {
id := c.Param("id")
var req UpdateDoctorRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误: "+err.Error())
return
}
if err := h.service.UpdateDoctor(c.Request.Context(), id, &req); err != nil {
response.Error(c, 500, "更新失败: "+err.Error())
return
}
response.Success(c, nil)
}
// DeleteDoctor 删除医生
func (h *Handler) DeleteDoctor(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteDoctor(c.Request.Context(), id); err != nil {
response.Error(c, 500, "删除失败: "+err.Error())
return
}
response.Success(c, nil)
}
// UpdateAdmin 更新管理员信息
func (h *Handler) UpdateAdmin(c *gin.Context) {
id := c.Param("id")
var req UpdateAdminRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误: "+err.Error())
return
}
if err := h.service.UpdateAdmin(c.Request.Context(), id, &req); err != nil {
response.Error(c, 500, "更新失败: "+err.Error())
return
}
response.Success(c, nil)
}
// DeleteAdmin 删除管理员
func (h *Handler) DeleteAdmin(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteAdmin(c.Request.Context(), id); err != nil {
response.Error(c, 500, "删除失败: "+err.Error())
return
}
response.Success(c, nil)
}
// ========== 请求结构体 ==========
type UpdateUserRequest struct {
RealName string `json:"real_name"`
Phone string `json:"phone"`
Gender string `json:"gender"`
Age int `json:"age"`
}
type CreatePatientRequest struct {
Phone string `json:"phone" binding:"required"`
Password string `json:"password" binding:"required"`
RealName string `json:"real_name" binding:"required"`
Gender string `json:"gender"`
Age int `json:"age"`
}
type UpdatePatientRequest struct {
RealName string `json:"real_name"`
Phone string `json:"phone"`
Gender string `json:"gender"`
Age int `json:"age"`
}
type UpdateDoctorRequest struct {
RealName string `json:"real_name"`
Phone string `json:"phone"`
Title string `json:"title"`
DepartmentID string `json:"department_id"`
Hospital string `json:"hospital"`
Introduction string `json:"introduction"`
Price int `json:"price"`
}
type UpdateAdminRequest struct {
RealName string `json:"real_name"`
Username string `json:"username"`
}
package admin
import (
"context"
"fmt"
"golang.org/x/crypto/bcrypt"
"internet-hospital/internal/model"
)
// UpdateUser 更新用户信息
func (s *Service) UpdateUser(ctx context.Context, userID string, req *UpdateUserRequest) error {
updates := make(map[string]interface{})
if req.RealName != "" {
updates["real_name"] = req.RealName
}
if req.Phone != "" {
updates["phone"] = req.Phone
}
if req.Gender != "" {
updates["gender"] = req.Gender
}
if req.Age > 0 {
updates["age"] = req.Age
}
return s.db.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error
}
// DeleteUser 删除用户(软删除)
func (s *Service) DeleteUser(ctx context.Context, userID string) error {
return s.db.Where("id = ?", userID).Delete(&model.User{}).Error
}
// CreatePatient 创建患者
func (s *Service) CreatePatient(ctx context.Context, req *CreatePatientRequest) error {
// 检查手机号是否已存在
var count int64
s.db.Model(&model.User{}).Where("phone = ?", req.Phone).Count(&count)
if count > 0 {
return fmt.Errorf("手机号已存在")
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("密码加密失败: %w", err)
}
user := &model.User{
Phone: req.Phone,
Password: string(hashedPassword),
RealName: req.RealName,
Role: "patient",
Status: "active",
Gender: req.Gender,
Age: req.Age,
}
return s.db.Create(user).Error
}
// UpdatePatient 更新患者信息
func (s *Service) UpdatePatient(ctx context.Context, patientID string, req *UpdatePatientRequest) error {
updates := make(map[string]interface{})
if req.RealName != "" {
updates["real_name"] = req.RealName
}
if req.Phone != "" {
updates["phone"] = req.Phone
}
if req.Gender != "" {
updates["gender"] = req.Gender
}
if req.Age > 0 {
updates["age"] = req.Age
}
return s.db.Model(&model.User{}).Where("id = ? AND role = ?", patientID, "patient").Updates(updates).Error
}
// DeletePatient 删除患者
func (s *Service) DeletePatient(ctx context.Context, patientID string) error {
return s.db.Where("id = ? AND role = ?", patientID, "patient").Delete(&model.User{}).Error
}
// UpdateDoctor 更新医生信息
func (s *Service) UpdateDoctor(ctx context.Context, doctorID string, req *UpdateDoctorRequest) error {
// 更新用户表
userUpdates := make(map[string]interface{})
if req.RealName != "" {
userUpdates["real_name"] = req.RealName
}
if req.Phone != "" {
userUpdates["phone"] = req.Phone
}
if len(userUpdates) > 0 {
if err := s.db.Model(&model.User{}).Where("id = ?", doctorID).Updates(userUpdates).Error; err != nil {
return err
}
}
// 更新医生表
doctorUpdates := make(map[string]interface{})
if req.Title != "" {
doctorUpdates["title"] = req.Title
}
if req.DepartmentID != "" {
doctorUpdates["department_id"] = req.DepartmentID
}
if req.Hospital != "" {
doctorUpdates["hospital"] = req.Hospital
}
if req.Introduction != "" {
doctorUpdates["introduction"] = req.Introduction
}
if req.Price > 0 {
doctorUpdates["price"] = req.Price
}
if len(doctorUpdates) > 0 {
return s.db.Model(&model.Doctor{}).Where("user_id = ?", doctorID).Updates(doctorUpdates).Error
}
return nil
}
// DeleteDoctor 删除医生
func (s *Service) DeleteDoctor(ctx context.Context, doctorID string) error {
// 软删除用户和医生记录
tx := s.db.Begin()
if err := tx.Where("user_id = ?", doctorID).Delete(&model.Doctor{}).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Where("id = ?", doctorID).Delete(&model.User{}).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
// UpdateAdmin 更新管理员信息
func (s *Service) UpdateAdmin(ctx context.Context, adminID string, req *UpdateAdminRequest) error {
updates := make(map[string]interface{})
if req.RealName != "" {
updates["real_name"] = req.RealName
}
if req.Username != "" {
updates["username"] = req.Username
}
return s.db.Model(&model.User{}).Where("id = ? AND role = ?", adminID, "admin").Updates(updates).Error
}
// DeleteAdmin 删除管理员
func (s *Service) DeleteAdmin(ctx context.Context, adminID string) error {
return s.db.Where("id = ? AND role = ?", adminID, "admin").Delete(&model.User{}).Error
}
package chronic
import (
"github.com/gin-gonic/gin"
"internet-hospital/pkg/response"
)
type Handler struct {
service *Service
}
func NewHandler() *Handler {
return &Handler{service: NewService()}
}
func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
g := r.Group("/chronic")
{
g.GET("/records", h.ListRecords)
g.POST("/records", h.CreateRecord)
g.PUT("/records/:id", h.UpdateRecord)
g.DELETE("/records/:id", h.DeleteRecord)
g.GET("/renewals", h.ListRenewals)
g.POST("/renewals", h.CreateRenewal)
g.POST("/renewals/:id/ai-advice", h.GetAIAdvice)
g.GET("/reminders", h.ListReminders)
g.POST("/reminders", h.CreateReminder)
g.PUT("/reminders/:id", h.UpdateReminder)
g.PUT("/reminders/:id/toggle", h.ToggleReminder)
g.DELETE("/reminders/:id", h.DeleteReminder)
g.GET("/metrics", h.ListMetrics)
g.POST("/metrics", h.CreateMetric)
g.DELETE("/metrics/:id", h.DeleteMetric)
}
}
func (h *Handler) ListRecords(c *gin.Context) {
userID, _ := c.Get("user_id")
list, err := h.service.ListChronicRecords(c.Request.Context(), userID.(string))
if err != nil {
response.Error(c, 500, "获取慢病档案失败")
return
}
response.Success(c, list)
}
func (h *Handler) CreateRecord(c *gin.Context) {
userID, _ := c.Get("user_id")
var req ChronicRecordReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
r, err := h.service.CreateChronicRecord(c.Request.Context(), userID.(string), &req)
if err != nil {
response.Error(c, 500, "创建慢病档案失败")
return
}
response.Success(c, r)
}
func (h *Handler) UpdateRecord(c *gin.Context) {
userID, _ := c.Get("user_id")
var req ChronicRecordReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
r, err := h.service.UpdateChronicRecord(c.Request.Context(), userID.(string), c.Param("id"), &req)
if err != nil {
response.Error(c, 500, "更新慢病档案失败")
return
}
response.Success(c, r)
}
func (h *Handler) DeleteRecord(c *gin.Context) {
userID, _ := c.Get("user_id")
if err := h.service.DeleteChronicRecord(c.Request.Context(), userID.(string), c.Param("id")); err != nil {
response.Error(c, 500, "删除失败")
return
}
response.Success(c, nil)
}
func (h *Handler) ListRenewals(c *gin.Context) {
userID, _ := c.Get("user_id")
list, err := h.service.ListRenewals(c.Request.Context(), userID.(string))
if err != nil {
response.Error(c, 500, "获取续方列表失败")
return
}
response.Success(c, list)
}
func (h *Handler) CreateRenewal(c *gin.Context) {
userID, _ := c.Get("user_id")
var req RenewalReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
r, err := h.service.CreateRenewal(c.Request.Context(), userID.(string), &req)
if err != nil {
response.Error(c, 500, "创建续方申请失败")
return
}
response.Success(c, r)
}
func (h *Handler) GetAIAdvice(c *gin.Context) {
userID, _ := c.Get("user_id")
advice, err := h.service.GetAIRenewalAdvice(c.Request.Context(), userID.(string), c.Param("id"))
if err != nil {
response.Error(c, 500, "获取AI建议失败")
return
}
response.Success(c, gin.H{"advice": advice})
}
func (h *Handler) ListReminders(c *gin.Context) {
userID, _ := c.Get("user_id")
list, err := h.service.ListReminders(c.Request.Context(), userID.(string))
if err != nil {
response.Error(c, 500, "获取用药提醒失败")
return
}
response.Success(c, list)
}
func (h *Handler) CreateReminder(c *gin.Context) {
userID, _ := c.Get("user_id")
var req ReminderReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
r, err := h.service.CreateReminder(c.Request.Context(), userID.(string), &req)
if err != nil {
response.Error(c, 500, "创建用药提醒失败")
return
}
response.Success(c, r)
}
func (h *Handler) UpdateReminder(c *gin.Context) {
userID, _ := c.Get("user_id")
var req ReminderReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
r, err := h.service.UpdateReminder(c.Request.Context(), userID.(string), c.Param("id"), &req)
if err != nil {
response.Error(c, 500, "更新用药提醒失败")
return
}
response.Success(c, r)
}
func (h *Handler) ToggleReminder(c *gin.Context) {
userID, _ := c.Get("user_id")
if err := h.service.ToggleReminder(c.Request.Context(), userID.(string), c.Param("id")); err != nil {
response.Error(c, 500, "操作失败")
return
}
response.Success(c, nil)
}
func (h *Handler) DeleteReminder(c *gin.Context) {
userID, _ := c.Get("user_id")
if err := h.service.DeleteReminder(c.Request.Context(), userID.(string), c.Param("id")); err != nil {
response.Error(c, 500, "删除失败")
return
}
response.Success(c, nil)
}
func (h *Handler) ListMetrics(c *gin.Context) {
userID, _ := c.Get("user_id")
metricType := c.Query("type")
list, err := h.service.ListMetrics(c.Request.Context(), userID.(string), metricType)
if err != nil {
response.Error(c, 500, "获取健康指标失败")
return
}
response.Success(c, list)
}
func (h *Handler) CreateMetric(c *gin.Context) {
userID, _ := c.Get("user_id")
var req MetricReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
r, err := h.service.CreateMetric(c.Request.Context(), userID.(string), &req)
if err != nil {
response.Error(c, 500, "记录健康指标失败")
return
}
response.Success(c, r)
}
func (h *Handler) DeleteMetric(c *gin.Context) {
userID, _ := c.Get("user_id")
if err := h.service.DeleteMetric(c.Request.Context(), userID.(string), c.Param("id")); err != nil {
response.Error(c, 500, "删除失败")
return
}
response.Success(c, nil)
}
package chronic
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
"internet-hospital/internal/model"
"internet-hospital/pkg/ai"
"internet-hospital/pkg/database"
)
type Service struct {
db *gorm.DB
}
func NewService() *Service {
return &Service{db: database.GetDB()}
}
// ========== 慢病档案 ==========
type ChronicRecordReq struct {
DiseaseName string `json:"disease_name"`
DiagnosisDate *time.Time `json:"diagnosis_date"`
Hospital string `json:"hospital"`
DoctorName string `json:"doctor_name"`
CurrentMeds string `json:"current_meds"`
ControlStatus string `json:"control_status"`
Notes string `json:"notes"`
}
func (s *Service) ListChronicRecords(ctx context.Context, userID string) ([]model.ChronicRecord, error) {
var list []model.ChronicRecord
err := s.db.WithContext(ctx).Where("user_id = ?", userID).Order("created_at desc").Find(&list).Error
return list, err
}
func (s *Service) CreateChronicRecord(ctx context.Context, userID string, req *ChronicRecordReq) (*model.ChronicRecord, error) {
r := &model.ChronicRecord{
ID: uuid.New().String(), UserID: userID,
DiseaseName: req.DiseaseName, DiagnosisDate: req.DiagnosisDate,
Hospital: req.Hospital, DoctorName: req.DoctorName,
CurrentMeds: req.CurrentMeds, ControlStatus: req.ControlStatus,
Notes: req.Notes,
}
if r.ControlStatus == "" {
r.ControlStatus = "stable"
}
return r, s.db.WithContext(ctx).Create(r).Error
}
func (s *Service) UpdateChronicRecord(ctx context.Context, userID, id string, req *ChronicRecordReq) (*model.ChronicRecord, error) {
var r model.ChronicRecord
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&r).Error; err != nil {
return nil, err
}
r.DiseaseName = req.DiseaseName
r.DiagnosisDate = req.DiagnosisDate
r.Hospital = req.Hospital
r.DoctorName = req.DoctorName
r.CurrentMeds = req.CurrentMeds
r.ControlStatus = req.ControlStatus
r.Notes = req.Notes
return &r, s.db.WithContext(ctx).Save(&r).Error
}
func (s *Service) DeleteChronicRecord(ctx context.Context, userID, id string) error {
return s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).Delete(&model.ChronicRecord{}).Error
}
// ========== 续方申请 ==========
type RenewalReq struct {
ChronicID string `json:"chronic_id"`
DiseaseName string `json:"disease_name"`
Medicines []string `json:"medicines"`
Reason string `json:"reason"`
}
func (s *Service) ListRenewals(ctx context.Context, userID string) ([]model.RenewalRequest, error) {
var list []model.RenewalRequest
err := s.db.WithContext(ctx).Where("user_id = ?", userID).Order("created_at desc").Find(&list).Error
return list, err
}
func (s *Service) CreateRenewal(ctx context.Context, userID string, req *RenewalReq) (*model.RenewalRequest, error) {
medsJSON, _ := json.Marshal(req.Medicines)
r := &model.RenewalRequest{
ID: uuid.New().String(), UserID: userID,
ChronicID: req.ChronicID, DiseaseName: req.DiseaseName,
Medicines: string(medsJSON), Reason: req.Reason, Status: "pending",
}
return r, s.db.WithContext(ctx).Create(r).Error
}
func (s *Service) GetAIRenewalAdvice(ctx context.Context, userID, renewalID string) (string, error) {
var r model.RenewalRequest
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", renewalID, userID).First(&r).Error; err != nil {
return "", err
}
prompt := fmt.Sprintf("患者患有%s,申请续方药品:%s,续方原因:%s。请从医学角度给出续方建议,包括用药注意事项、可能的药物相互作用和生活方式建议。", r.DiseaseName, r.Medicines, r.Reason)
result := ai.Call(ctx, ai.CallParams{
Scene: "renewal_advice",
UserID: userID,
Messages: []ai.ChatMessage{{Role: "user", Content: prompt}},
RequestSummary: r.DiseaseName,
})
if result.Error != nil {
return "", result.Error
}
advice := result.Content
s.db.WithContext(ctx).Model(&r).Update("ai_advice", advice)
return advice, nil
}
// ========== 用药提醒 ==========
type ReminderReq struct {
MedicineName string `json:"medicine_name"`
Dosage string `json:"dosage"`
Frequency string `json:"frequency"`
RemindTimes []string `json:"remind_times"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
Notes string `json:"notes"`
}
func (s *Service) ListReminders(ctx context.Context, userID string) ([]model.MedicationReminder, error) {
var list []model.MedicationReminder
err := s.db.WithContext(ctx).Where("user_id = ?", userID).Order("created_at desc").Find(&list).Error
return list, err
}
func (s *Service) CreateReminder(ctx context.Context, userID string, req *ReminderReq) (*model.MedicationReminder, error) {
timesJSON, _ := json.Marshal(req.RemindTimes)
r := &model.MedicationReminder{
ID: uuid.New().String(), UserID: userID,
MedicineName: req.MedicineName, Dosage: req.Dosage,
Frequency: req.Frequency, RemindTimes: string(timesJSON),
StartDate: req.StartDate, EndDate: req.EndDate,
Notes: req.Notes, IsActive: true,
}
return r, s.db.WithContext(ctx).Create(r).Error
}
func (s *Service) UpdateReminder(ctx context.Context, userID, id string, req *ReminderReq) (*model.MedicationReminder, error) {
var r model.MedicationReminder
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&r).Error; err != nil {
return nil, err
}
timesJSON, _ := json.Marshal(req.RemindTimes)
r.MedicineName = req.MedicineName
r.Dosage = req.Dosage
r.Frequency = req.Frequency
r.RemindTimes = string(timesJSON)
r.StartDate = req.StartDate
r.EndDate = req.EndDate
r.Notes = req.Notes
return &r, s.db.WithContext(ctx).Save(&r).Error
}
func (s *Service) ToggleReminder(ctx context.Context, userID, id string) error {
var r model.MedicationReminder
if err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&r).Error; err != nil {
return err
}
return s.db.WithContext(ctx).Model(&r).Update("is_active", !r.IsActive).Error
}
func (s *Service) DeleteReminder(ctx context.Context, userID, id string) error {
return s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).Delete(&model.MedicationReminder{}).Error
}
// ========== 健康指标 ==========
type MetricReq struct {
MetricType string `json:"metric_type"`
Value1 float64 `json:"value1"`
Value2 float64 `json:"value2"`
Unit string `json:"unit"`
RecordedAt time.Time `json:"recorded_at"`
Notes string `json:"notes"`
}
func (s *Service) ListMetrics(ctx context.Context, userID, metricType string) ([]model.HealthMetric, error) {
var list []model.HealthMetric
q := s.db.WithContext(ctx).Where("user_id = ?", userID)
if metricType != "" {
q = q.Where("metric_type = ?", metricType)
}
err := q.Order("recorded_at desc").Limit(100).Find(&list).Error
return list, err
}
func (s *Service) CreateMetric(ctx context.Context, userID string, req *MetricReq) (*model.HealthMetric, error) {
recordedAt := req.RecordedAt
if recordedAt.IsZero() {
recordedAt = time.Now()
}
r := &model.HealthMetric{
ID: uuid.New().String(), UserID: userID,
MetricType: req.MetricType, Value1: req.Value1, Value2: req.Value2,
Unit: req.Unit, RecordedAt: recordedAt, Notes: req.Notes,
}
return r, s.db.WithContext(ctx).Create(r).Error
}
func (s *Service) DeleteMetric(ctx context.Context, userID, id string) error {
return s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).Delete(&model.HealthMetric{}).Error
}
package consult
import (
"fmt"
"github.com/gin-gonic/gin"
"internet-hospital/pkg/middleware"
"internet-hospital/pkg/response"
)
type Handler struct {
service *Service
}
func NewHandler() *Handler {
return &Handler{
service: NewService(),
}
}
func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
consult := r.Group("/consult")
consult.Use(middleware.JWTAuth())
{
consult.POST("/create", h.CreateConsult)
consult.GET("/list", h.GetConsultList)
consult.GET("/doctor/list", h.GetDoctorConsultList)
consult.GET("/doctor/waiting", h.GetWaitingList)
consult.GET("/doctor/patients", h.GetPatientList)
consult.GET("/doctor/workbench-stats", h.GetDoctorWorkbenchStats)
consult.POST("/:id/accept", h.AcceptConsult)
consult.POST("/:id/reject", h.RejectConsult)
consult.GET("/:id", h.GetConsultDetail)
consult.GET("/:id/messages", h.GetConsultMessages)
consult.POST("/:id/message", h.SendMessage)
consult.GET("/:id/video-room", h.GetVideoRoomInfo)
consult.POST("/:id/end", h.EndConsult)
consult.POST("/:id/cancel", h.CancelConsult)
consult.POST("/:id/ai-assist", h.AIAssist)
// 患者端处方
consult.GET("/patient/prescriptions", h.GetPatientPrescriptions)
consult.GET("/prescription/:id", h.GetPrescriptionDetail)
}
}
func (h *Handler) CreateConsult(c *gin.Context) {
userID, _ := c.Get("user_id")
var req CreateConsultRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
result, err := h.service.CreateConsult(c.Request.Context(), userID.(string), &req)
if err != nil {
response.Error(c, 400, err.Error())
return
}
response.Success(c, result)
}
func (h *Handler) GetConsultList(c *gin.Context) {
userID, _ := c.Get("user_id")
status := c.Query("status")
result, err := h.service.GetConsultList(c.Request.Context(), userID.(string), status)
if err != nil {
response.Error(c, 500, "获取问诊列表失败")
return
}
response.Success(c, result)
}
func (h *Handler) GetConsultDetail(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetConsultByID(c.Request.Context(), id)
if err != nil {
response.Error(c, 404, "问诊不存在")
return
}
response.Success(c, result)
}
func (h *Handler) GetPatientList(c *gin.Context) {
userID, _ := c.Get("user_id")
list, err := h.service.GetPatientList(c.Request.Context(), userID.(string))
if err != nil {
response.Error(c, 500, "获取患者列表失败")
return
}
response.Success(c, list)
}
func (h *Handler) GetDoctorWorkbenchStats(c *gin.Context) {
userID, _ := c.Get("user_id")
stats, err := h.service.GetDoctorWorkbenchStats(c.Request.Context(), userID.(string))
if err != nil {
response.Error(c, 500, "获取工作台统计失败")
return
}
response.Success(c, stats)
}
func (h *Handler) GetConsultMessages(c *gin.Context) {
id := c.Param("id")
messages, err := h.service.GetConsultMessages(c.Request.Context(), id)
if err != nil {
response.Error(c, 500, "获取消息失败")
return
}
response.Success(c, messages)
}
type SendMessageRequest struct {
Content string `json:"content" binding:"required"`
ContentType string `json:"content_type"`
}
func (h *Handler) SendMessage(c *gin.Context) {
id := c.Param("id")
userID, _ := c.Get("user_id")
userRole, _ := c.Get("role")
var req SendMessageRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
contentType := req.ContentType
if contentType == "" {
contentType = "text"
}
// 自动判断 sender_type
senderType := "patient"
if role, ok := userRole.(string); ok && role == "doctor" {
senderType = "doctor"
}
message, err := h.service.SendMessage(c.Request.Context(), id, userID.(string), senderType, req.Content, contentType)
if err != nil {
response.Error(c, 500, "发送消息失败")
return
}
response.Success(c, message)
}
func (h *Handler) GetVideoRoomInfo(c *gin.Context) {
id := c.Param("id")
roomInfo, err := h.service.GetVideoRoomInfo(c.Request.Context(), id)
if err != nil {
response.Error(c, 404, err.Error())
return
}
response.Success(c, roomInfo)
}
func (h *Handler) EndConsult(c *gin.Context) {
id := c.Param("id")
if err := h.service.EndConsult(c.Request.Context(), id); err != nil {
response.Error(c, 400, err.Error())
return
}
response.Success(c, nil)
}
type CancelRequest struct {
Reason string `json:"reason"`
}
func (h *Handler) CancelConsult(c *gin.Context) {
id := c.Param("id")
if err := h.service.CancelConsult(c.Request.Context(), id); err != nil {
response.Error(c, 500, "取消问诊失败")
return
}
response.Success(c, nil)
}
// AIAssist AI辅助诊断(SSE流式返回)
func (h *Handler) AIAssist(c *gin.Context) {
id := c.Param("id")
var req struct {
Scene string `json:"scene" binding:"required"` // consult_diagnosis | consult_medication
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误,scene 必填")
return
}
result, err := h.service.AIAssist(c.Request.Context(), id, req.Scene)
if err != nil {
response.Error(c, 500, err.Error())
return
}
response.Success(c, result)
}
// ========== 患者端处方 API ==========
// GetPatientPrescriptions 患者获取处方列表
func (h *Handler) GetPatientPrescriptions(c *gin.Context) {
userID, _ := c.Get("user_id")
page := 1
pageSize := 10
if p := c.Query("page"); p != "" {
fmt.Sscanf(p, "%d", &page)
}
if ps := c.Query("page_size"); ps != "" {
fmt.Sscanf(ps, "%d", &pageSize)
}
result, err := h.service.GetPatientPrescriptions(c.Request.Context(), userID.(string), page, pageSize)
if err != nil {
response.Error(c, 500, "获取处方列表失败")
return
}
response.Success(c, result)
}
// GetPrescriptionDetail 获取处方详情
func (h *Handler) GetPrescriptionDetail(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetPrescriptionByID(c.Request.Context(), id)
if err != nil {
response.Error(c, 404, err.Error())
return
}
response.Success(c, result)
}
// ========== 医生端 API ==========
// GetDoctorConsultList 医生端获取自己的问诊列表
func (h *Handler) GetDoctorConsultList(c *gin.Context) {
userID, _ := c.Get("user_id")
status := c.Query("status")
result, err := h.service.GetDoctorConsultList(c.Request.Context(), userID.(string), status)
if err != nil {
response.Error(c, 500, "获取问诊列表失败")
return
}
response.Success(c, result)
}
// GetWaitingList 医生端获取待接诊队列
func (h *Handler) GetWaitingList(c *gin.Context) {
userID, _ := c.Get("user_id")
result, err := h.service.GetWaitingList(c.Request.Context(), userID.(string))
if err != nil {
response.Error(c, 500, "获取待接诊列表失败")
return
}
response.Success(c, result)
}
// AcceptConsult 医生接诊
func (h *Handler) AcceptConsult(c *gin.Context) {
id := c.Param("id")
userID, _ := c.Get("user_id")
if err := h.service.AcceptConsult(c.Request.Context(), id, userID.(string)); err != nil {
response.Error(c, 400, err.Error())
return
}
response.Success(c, nil)
}
// RejectConsult 医生拒诊
func (h *Handler) RejectConsult(c *gin.Context) {
id := c.Param("id")
var req struct {
Reason string `json:"reason"`
}
c.ShouldBindJSON(&req)
if err := h.service.RejectConsult(c.Request.Context(), id, req.Reason); err != nil {
response.Error(c, 400, err.Error())
return
}
response.Success(c, nil)
}
package consult
import (
"context"
"fmt"
"internet-hospital/internal/model"
)
// GetPatientPrescriptions 患者获取自己的处方列表
func (s *Service) GetPatientPrescriptions(ctx context.Context, patientID string, page, pageSize int) (map[string]interface{}, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
var total int64
s.db.Model(&model.Prescription{}).Where("patient_id = ?", patientID).Count(&total)
var prescriptions []model.Prescription
s.db.Preload("Items").Where("patient_id = ?", patientID).
Order("created_at DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&prescriptions)
return map[string]interface{}{
"list": prescriptions,
"total": total,
}, nil
}
// GetPrescriptionByID 获取处方详情
func (s *Service) GetPrescriptionByID(ctx context.Context, id string) (*model.Prescription, error) {
var prescription model.Prescription
if err := s.db.Preload("Items").First(&prescription, "id = ?", id).Error; err != nil {
return nil, fmt.Errorf("处方不存在")
}
return &prescription, nil
}
This diff is collapsed.
package doctor
import (
"github.com/gin-gonic/gin"
"internet-hospital/pkg/response"
)
type Handler struct {
service *Service
}
func NewHandler() *Handler {
return &Handler{
service: NewService(),
}
}
func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
doctor := r.Group("/doctor")
{
doctor.GET("/departments", h.GetDepartments)
doctor.GET("/list", h.GetDoctorList)
doctor.GET("/:id", h.GetDoctorDetail)
doctor.GET("/:id/schedule", h.GetDoctorSchedule)
}
}
func (h *Handler) GetDepartments(c *gin.Context) {
departments, err := h.service.GetDepartments(c.Request.Context())
if err != nil {
response.Error(c, 500, "获取科室列表失败")
return
}
response.Success(c, departments)
}
func (h *Handler) GetDoctorList(c *gin.Context) {
var params DoctorListParams
if err := c.ShouldBindQuery(&params); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
result, err := h.service.GetDoctorList(c.Request.Context(), &params)
if err != nil {
response.Error(c, 500, "获取医生列表失败")
return
}
response.Success(c, result)
}
func (h *Handler) GetDoctorDetail(c *gin.Context) {
id := c.Param("id")
doctor, err := h.service.GetDoctorByID(c.Request.Context(), id)
if err != nil {
response.Error(c, 404, "医生不存在")
return
}
response.Success(c, doctor)
}
type ScheduleQuery struct {
StartDate string `form:"start_date" binding:"required"`
EndDate string `form:"end_date" binding:"required"`
}
func (h *Handler) GetDoctorSchedule(c *gin.Context) {
id := c.Param("id")
var query ScheduleQuery
if err := c.ShouldBindQuery(&query); err != nil {
response.BadRequest(c, "请求参数错误")
return
}
schedules, err := h.service.GetDoctorSchedule(c.Request.Context(), id, query.StartDate, query.EndDate)
if err != nil {
response.Error(c, 500, "获取排班信息失败")
return
}
response.Success(c, schedules)
}
package doctor
import (
"log"
"github.com/google/uuid"
"internet-hospital/internal/model"
"internet-hospital/pkg/database"
)
// InitDepartmentsAndDoctors 初始化科室和医生数据
func InitDepartmentsAndDoctors() error {
db := database.GetDB()
if db == nil {
log.Println("数据库未初始化,跳过科室和医生数据初始化")
return nil
}
// 检查是否已有科室数据
var deptCount int64
db.Model(&model.Department{}).Count(&deptCount)
var departments []model.Department
if deptCount > 0 {
log.Println("科室数据已存在")
// 获取现有科室
db.Find(&departments)
} else {
// 创建科室数据
departments = []model.Department{
{ID: uuid.New().String(), Name: "内科", Icon: "heart", SortOrder: 1},
{ID: uuid.New().String(), Name: "外科", Icon: "medicine-box", SortOrder: 2},
{ID: uuid.New().String(), Name: "儿科", Icon: "smile", SortOrder: 3},
{ID: uuid.New().String(), Name: "妇产科", Icon: "woman", SortOrder: 4},
{ID: uuid.New().String(), Name: "皮肤科", Icon: "skin", SortOrder: 5},
{ID: uuid.New().String(), Name: "眼科", Icon: "eye", SortOrder: 6},
{ID: uuid.New().String(), Name: "耳鼻喉科", Icon: "audio", SortOrder: 7},
{ID: uuid.New().String(), Name: "口腔科", Icon: "smile", SortOrder: 8},
}
for _, dept := range departments {
if err := db.Create(&dept).Error; err != nil {
log.Printf("创建科室失败: %v", err)
return err
}
}
log.Println("科室数据初始化成功")
}
// 检查是否已有医生数据
var doctorCount int64
db.Model(&model.Doctor{}).Where("status = ?", "approved").Count(&doctorCount)
if doctorCount > 0 {
log.Printf("医生数据已存在 (%d 位医生),跳过初始化", doctorCount)
return nil
}
// 确保有足够的科室数据用于医生创建
if len(departments) < 6 {
log.Println("科室数据不足,无法创建医生")
return nil
}
// 创建示例医生数据
type DoctorInit struct {
Name string
Phone string
LicenseNo string
Title string
DepartmentID string
Hospital string
Introduction string
Rating float64
ConsultCount int
Price int
IsOnline bool
}
doctorInits := []DoctorInit{
{
Name: "刘医生",
Phone: "13800000001",
LicenseNo: "LIC20260001",
Title: "主治医师",
DepartmentID: departments[0].ID,
Hospital: "北京协和医院",
Introduction: "从事内科临床工作20年,擅长心血管疾病、高血压、糖尿病的诊治",
Rating: 4.8,
ConsultCount: 1256,
Price: 5000,
IsOnline: true,
},
{
Name: "陈医生",
Phone: "13800000002",
LicenseNo: "LIC20260002",
Title: "副主任医师",
DepartmentID: departments[1].ID,
Hospital: "上海华山医院",
Introduction: "外科手术经验丰富,擅长普外科、肝胆外科手术",
Rating: 4.9,
ConsultCount: 856,
Price: 8000,
IsOnline: true,
},
{
Name: "赵医生",
Phone: "13800000003",
LicenseNo: "LIC20260003",
Title: "主治医师",
DepartmentID: departments[2].ID,
Hospital: "广州中山医院",
Introduction: "儿科临床经验15年,擅长儿童常见病、多发病的诊治",
Rating: 4.7,
ConsultCount: 2134,
Price: 4500,
IsOnline: false,
},
{
Name: "孙医生",
Phone: "13800000004",
LicenseNo: "LIC20260004",
Title: "住院医师",
DepartmentID: departments[4].ID,
Hospital: "深圳北大医院",
Introduction: "皮肤科专业,擅长痤疮、湿疹、皮炎等常见皮肤病",
Rating: 4.6,
ConsultCount: 678,
Price: 3500,
IsOnline: true,
},
{
Name: "周医生",
Phone: "13800000005",
LicenseNo: "LIC20260005",
Title: "主任医师",
DepartmentID: departments[5].ID,
Hospital: "杭州市第一医院",
Introduction: "眼科专家,擅长白内障、青光眼、近视矫正",
Rating: 5.0,
ConsultCount: 1890,
Price: 10000,
IsOnline: true,
},
}
for _, doctorInit := range doctorInits {
// 1. 先创建 user 记录
userID := uuid.New().String()
if err := db.Exec(`
INSERT INTO users (id, phone, password, real_name, role, is_verified, status, created_at, updated_at)
VALUES (?, ?, '$2a$10$defaultpasswordhash', ?, 'doctor', true, 'active', NOW(), NOW())
`, userID, doctorInit.Phone, doctorInit.Name).Error; err != nil {
log.Printf("创建医生用户失败: %v", err)
return err
}
// 2. 再创建 doctor 记录
doctorID := uuid.New().String()
if err := db.Exec(`
INSERT INTO doctors (id, user_id, name, license_no, title, department_id, hospital, introduction, specialties, rating, consult_count, price, is_online, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ARRAY[]::text[], ?, ?, ?, ?, 'approved', NOW(), NOW())
`, doctorID, userID, doctorInit.Name, doctorInit.LicenseNo, doctorInit.Title, doctorInit.DepartmentID, doctorInit.Hospital, doctorInit.Introduction, doctorInit.Rating, doctorInit.ConsultCount, doctorInit.Price, doctorInit.IsOnline).Error; err != nil {
log.Printf("创建医生记录失败: %v", err)
return err
}
}
log.Println("========================================")
log.Println("医生数据初始化成功:")
log.Printf(" 创建了 %d 个科室", len(departments))
log.Printf(" 创建了 %d 位医生", len(doctorInits))
log.Println("========================================")
return nil
}
package doctor
import (
"context"
"gorm.io/gorm"
"internet-hospital/internal/model"
"internet-hospital/pkg/database"
)
type Service struct {
db *gorm.DB
}
func NewService() *Service {
return &Service{
db: database.GetDB(),
}
}
type DoctorListParams struct {
DepartmentID string `form:"department_id"`
Keyword string `form:"keyword"`
SortBy string `form:"sort_by"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type PaginatedResponse struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func (s *Service) GetDepartments(ctx context.Context) ([]model.Department, error) {
var departments []model.Department
err := s.db.Order("sort_order ASC").Find(&departments).Error
return departments, err
}
type DoctorWithDepartment struct {
model.Doctor
DepartmentName string `json:"department_name"`
}
func (s *Service) GetDoctorList(ctx context.Context, params *DoctorListParams) (*PaginatedResponse, error) {
if params.Page <= 0 {
params.Page = 1
}
if params.PageSize <= 0 {
params.PageSize = 20
}
// 先查询医生列表
query := s.db.Model(&model.Doctor{}).Where("status = ?", "approved")
if params.DepartmentID != "" {
query = query.Where("department_id = ?", params.DepartmentID)
}
if params.Keyword != "" {
query = query.Where("name LIKE ?", "%"+params.Keyword+"%")
}
var total int64
query.Count(&total)
switch params.SortBy {
case "rating":
query = query.Order("rating DESC")
case "consult_count":
query = query.Order("consult_count DESC")
case "price":
query = query.Order("price ASC")
default:
query = query.Order("rating DESC")
}
var doctors []model.Doctor
offset := (params.Page - 1) * params.PageSize
if err := query.Offset(offset).Limit(params.PageSize).Find(&doctors).Error; err != nil {
return nil, err
}
// 构建返回结果,添加科室名称
var result []DoctorWithDepartment
for _, doctor := range doctors {
var dept model.Department
s.db.Where("id = ?", doctor.DepartmentID).First(&dept)
result = append(result, DoctorWithDepartment{
Doctor: doctor,
DepartmentName: dept.Name,
})
}
return &PaginatedResponse{
List: result,
Total: total,
Page: params.Page,
PageSize: params.PageSize,
}, nil
}
func (s *Service) GetDoctorByID(ctx context.Context, id string) (*DoctorWithDepartment, error) {
var doctor DoctorWithDepartment
err := s.db.Table("doctors").
Select("doctors.*, departments.name as department_name").
Joins("LEFT JOIN departments ON doctors.department_id = departments.id").
Where("doctors.id = ? AND doctors.deleted_at IS NULL", id).
Scan(&doctor).Error
return &doctor, err
}
func (s *Service) GetDoctorSchedule(ctx context.Context, doctorID, startDate, endDate string) ([]model.DoctorSchedule, error) {
var schedules []model.DoctorSchedule
err := s.db.Where("doctor_id = ? AND date >= ? AND date <= ?", doctorID, startDate, endDate).
Order("date ASC, start_time ASC").
Find(&schedules).Error
return schedules, err
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
package preconsult
import (
"github.com/gin-gonic/gin"
"internet-hospital/pkg/middleware"
"internet-hospital/pkg/response"
)
type Handler struct {
service *Service
}
func NewHandler() *Handler {
return &Handler{
service: NewService(),
}
}
func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
pc := r.Group("/pre-consult")
pc.Use(middleware.JWTAuth())
{
pc.GET("/:id", h.GetPreConsultDetail)
pc.GET("/list", h.GetPreConsultList)
pc.GET("/latest", h.GetLatestInProgress)
pc.GET("/by-consultation/:consultation_id", h.GetByConsultationID)
pc.GET("/by-patient/:patient_id", h.GetByPatientID)
}
}
// GetPreConsultDetail 获取预问诊详情
func (h *Handler) GetPreConsultDetail(c *gin.Context) {
id := c.Param("id")
result, err := h.service.GetPreConsultByID(c.Request.Context(), id)
if err != nil {
response.Error(c, 404, err.Error())
return
}
response.Success(c, result)
}
// GetPreConsultList 获取预问诊列表
func (h *Handler) GetPreConsultList(c *gin.Context) {
userID, _ := c.Get("user_id")
list, err := h.service.GetPreConsultList(c.Request.Context(), userID.(string))
if err != nil {
response.Error(c, 500, "获取预问诊列表失败")
return
}
response.Success(c, list)
}
// GetLatestInProgress 获取患者最新进行中的预问诊(用于页面恢复对话)
func (h *Handler) GetLatestInProgress(c *gin.Context) {
userID, _ := c.Get("user_id")
result, err := h.service.GetLatestInProgress(c.Request.Context(), userID.(string))
if err != nil {
// 没有进行中的预问诊不算错误,返回空
response.Success(c, nil)
return
}
response.Success(c, result)
}
// GetByConsultationID 根据问诊ID获取预问诊信息(医生端使用)
func (h *Handler) GetByConsultationID(c *gin.Context) {
consultationID := c.Param("consultation_id")
result, err := h.service.GetPreConsultByConsultationID(c.Request.Context(), consultationID)
if err != nil {
response.Error(c, 404, err.Error())
return
}
response.Success(c, result)
}
// GetByPatientID 根据患者ID获取最新预问诊信息(医生端使用)
func (h *Handler) GetByPatientID(c *gin.Context) {
patientID := c.Param("patient_id")
result, err := h.service.GetLatestByPatientID(c.Request.Context(), patientID)
if err != nil {
response.Error(c, 404, err.Error())
return
}
response.Success(c, result)
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
-- 为 users 表添加 gender 和 age 列
ALTER TABLE users ADD COLUMN IF NOT EXISTS gender VARCHAR(10);
ALTER TABLE users ADD COLUMN IF NOT EXISTS age INTEGER;
-- 添加列注释
COMMENT ON COLUMN users.gender IS '性别';
COMMENT ON COLUMN users.age IS '年龄';
This diff is collapsed.
This diff is collapsed.
package ai
import (
"internet-hospital/internal/model"
"internet-hospital/pkg/database"
)
// GetPromptByKey 从数据库中根据template_key获取Prompt内容(向后兼容)
func GetPromptByKey(key string) string {
db := database.GetDB()
if db == nil {
return ""
}
var template model.PromptTemplate
if err := db.Where("template_key = ? AND status = ?", key, "active").First(&template).Error; err != nil {
return ""
}
return template.Content
}
// GetActivePromptByScene 根据场景获取当前激活的Prompt内容
// 优先按 scene 字段查找 active 模板,找不到再按 template_key 查找
func GetActivePromptByScene(scene string) string {
db := database.GetDB()
if db == nil {
return ""
}
var template model.PromptTemplate
// 先按 scene 查找
if err := db.Where("scene = ? AND status = ?", scene, "active").
Order("updated_at DESC").First(&template).Error; err == nil {
return template.Content
}
// 兜底:按 template_key 查找(向后兼容)
if err := db.Where("template_key = ? AND status = ?", scene, "active").First(&template).Error; err == nil {
return template.Content
}
return ""
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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