init medical training project
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
from django.contrib import admin
|
||||
from .models import TrainingRecord, TrainingScoreDetail
|
||||
|
||||
|
||||
@admin.register(TrainingRecord)
|
||||
class TrainingRecordAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'id', 'user', 'case', 'training_mode',
|
||||
'total_score', 'evaluation_level', 'status',
|
||||
'start_time', 'duration_seconds'
|
||||
]
|
||||
list_filter = ['training_mode', 'evaluation_level', 'status']
|
||||
search_fields = ['user__real_name', 'case__title', 'feedback']
|
||||
ordering = ['-start_time']
|
||||
|
||||
|
||||
@admin.register(TrainingScoreDetail)
|
||||
class TrainingScoreDetailAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'record', 'dimension', 'score', 'ai_confidence']
|
||||
list_filter = ['dimension']
|
||||
search_fields = ['record__user__real_name', 'dimension']
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TrainingConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.training'
|
||||
verbose_name = '训练管理'
|
||||
@@ -0,0 +1,71 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-26 07:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('case', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TrainingScoreDetail',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('dimension', models.CharField(max_length=50, verbose_name='评分维度')),
|
||||
('score', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='分数')),
|
||||
('deducted_reason', models.TextField(blank=True, verbose_name='扣分原因')),
|
||||
('evidence_message_ids', models.JSONField(blank=True, default=list, verbose_name='对应对话证据')),
|
||||
('ai_confidence', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='AI评分置信度')),
|
||||
('comment', models.TextField(blank=True, verbose_name='评语')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '评分明细',
|
||||
'verbose_name_plural': '评分明细',
|
||||
'db_table': 'training_score_detail',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TrainingRecord',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('training_mode', models.CharField(choices=[('novice', '新手'), ('practice', '练习'), ('exam', '考试')], max_length=50, verbose_name='训练模式')),
|
||||
('case_type', models.CharField(blank=True, max_length=30, verbose_name='病例类型')),
|
||||
('start_time', models.DateTimeField(auto_now_add=True, verbose_name='开始时间')),
|
||||
('end_time', models.DateTimeField(blank=True, null=True, verbose_name='结束时间')),
|
||||
('duration_seconds', models.IntegerField(blank=True, null=True, verbose_name='训练时长')),
|
||||
('total_score', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='总分')),
|
||||
('ai_score', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='AI评分')),
|
||||
('teacher_score', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='教师评分')),
|
||||
('evaluation_level', models.CharField(blank=True, choices=[('excellent', '优秀'), ('good', '良好'), ('average', '一般'), ('poor', '较差')], max_length=20, verbose_name='评价等级')),
|
||||
('status', models.CharField(choices=[('in_progress', '进行中'), ('completed', '已完成'), ('aborted', '已中断')], default='in_progress', max_length=30, verbose_name='状态')),
|
||||
('feedback', models.TextField(blank=True, verbose_name='总评')),
|
||||
('thinking_chain', models.TextField(blank=True, verbose_name='临床推理链')),
|
||||
('diagnosis_path', models.TextField(blank=True, verbose_name='诊断路径')),
|
||||
('wrong_points', models.JSONField(blank=True, default=list, verbose_name='错误知识点')),
|
||||
('missed_questions', models.JSONField(blank=True, default=list, verbose_name='漏问项')),
|
||||
('recommendation_result', models.JSONField(blank=True, default=dict, verbose_name='AI推荐')),
|
||||
('ai_feedback_structured', models.JSONField(blank=True, default=dict, verbose_name='AI结构化反馈')),
|
||||
('osce_station_score', models.JSONField(blank=True, default=dict, verbose_name='OSCE各站点成绩')),
|
||||
('interruption_count', models.IntegerField(default=0, verbose_name='中断次数')),
|
||||
('emotion_analysis', models.JSONField(blank=True, default=dict, verbose_name='情绪分析')),
|
||||
('prompt_version', models.CharField(blank=True, max_length=50, verbose_name='Prompt版本')),
|
||||
('rag_context_version', models.CharField(blank=True, max_length=50, verbose_name='知识上下文版本')),
|
||||
('case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_records', to='case.casebase', verbose_name='病例')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '训练记录',
|
||||
'verbose_name_plural': '训练记录',
|
||||
'db_table': 'training_record',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-26 07:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('case', '0002_initial'),
|
||||
('training', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trainingrecord',
|
||||
name='teacher',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='supervised_records', to=settings.AUTH_USER_MODEL, verbose_name='带教老师'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trainingrecord',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_records', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trainingscoredetail',
|
||||
name='record',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='score_details', to='training.trainingrecord', verbose_name='训练记录'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trainingscoredetail',
|
||||
name='rule',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='case.scoringrule', verbose_name='评分规则'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from django.db import models
|
||||
from apps.common.models import BaseModel
|
||||
from apps.user.models import User
|
||||
from apps.case.models import CaseBase
|
||||
|
||||
|
||||
class TrainingRecord(BaseModel):
|
||||
"""训练记录表"""
|
||||
TRAINING_MODE_CHOICES = [
|
||||
('novice', '新手'),
|
||||
('practice', '练习'),
|
||||
('exam', '考试'),
|
||||
]
|
||||
EVALUATION_LEVEL_CHOICES = [
|
||||
('excellent', '优秀'),
|
||||
('good', '良好'),
|
||||
('average', '一般'),
|
||||
('poor', '较差'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('in_progress', '进行中'),
|
||||
('completed', '已完成'),
|
||||
('aborted', '已中断'),
|
||||
]
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE,
|
||||
related_name='training_records', verbose_name='用户'
|
||||
)
|
||||
case = models.ForeignKey(
|
||||
CaseBase, on_delete=models.CASCADE,
|
||||
related_name='training_records', verbose_name='病例'
|
||||
)
|
||||
training_mode = models.CharField('训练模式', max_length=50, choices=TRAINING_MODE_CHOICES)
|
||||
case_type = models.CharField('病例类型', max_length=30, blank=True)
|
||||
teacher = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL,
|
||||
null=True, blank=True, related_name='supervised_records',
|
||||
verbose_name='带教老师'
|
||||
)
|
||||
start_time = models.DateTimeField('开始时间', auto_now_add=True)
|
||||
end_time = models.DateTimeField('结束时间', null=True, blank=True)
|
||||
duration_seconds = models.IntegerField('训练时长', null=True, blank=True)
|
||||
total_score = models.DecimalField('总分', max_digits=5, decimal_places=2, null=True, blank=True)
|
||||
ai_score = models.DecimalField('AI评分', max_digits=5, decimal_places=2, null=True, blank=True)
|
||||
teacher_score = models.DecimalField('教师评分', max_digits=5, decimal_places=2, null=True, blank=True)
|
||||
evaluation_level = models.CharField('评价等级', max_length=20, choices=EVALUATION_LEVEL_CHOICES, blank=True)
|
||||
status = models.CharField('状态', max_length=30, choices=STATUS_CHOICES, default='in_progress')
|
||||
feedback = models.TextField('总评', blank=True)
|
||||
thinking_chain = models.TextField('临床推理链', blank=True)
|
||||
diagnosis_path = models.TextField('诊断路径', blank=True)
|
||||
wrong_points = models.JSONField('错误知识点', default=list, blank=True)
|
||||
missed_questions = models.JSONField('漏问项', default=list, blank=True)
|
||||
recommendation_result = models.JSONField('AI推荐', default=dict, blank=True)
|
||||
ai_feedback_structured = models.JSONField('AI结构化反馈', default=dict, blank=True)
|
||||
osce_station_score = models.JSONField('OSCE各站点成绩', default=dict, blank=True)
|
||||
interruption_count = models.IntegerField('中断次数', default=0)
|
||||
emotion_analysis = models.JSONField('情绪分析', default=dict, blank=True)
|
||||
prompt_version = models.CharField('Prompt版本', max_length=50, blank=True)
|
||||
rag_context_version = models.CharField('知识上下文版本', max_length=50, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'training_record'
|
||||
verbose_name = '训练记录'
|
||||
verbose_name_plural = '训练记录'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} - {self.case.title}"
|
||||
|
||||
|
||||
class TrainingScoreDetail(BaseModel):
|
||||
"""评分明细表"""
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
record = models.ForeignKey(
|
||||
TrainingRecord, on_delete=models.CASCADE,
|
||||
related_name='score_details', verbose_name='训练记录'
|
||||
)
|
||||
rule = models.ForeignKey(
|
||||
'case.ScoringRule', on_delete=models.CASCADE,
|
||||
null=True, blank=True, verbose_name='评分规则'
|
||||
)
|
||||
dimension = models.CharField('评分维度', max_length=50)
|
||||
score = models.DecimalField('分数', max_digits=5, decimal_places=2)
|
||||
deducted_reason = models.TextField('扣分原因', blank=True)
|
||||
evidence_message_ids = models.JSONField('对应对话证据', default=list, blank=True)
|
||||
ai_confidence = models.DecimalField('AI评分置信度', max_digits=5, decimal_places=2, null=True, blank=True)
|
||||
comment = models.TextField('评语', blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'training_score_detail'
|
||||
verbose_name = '评分明细'
|
||||
verbose_name_plural = '评分明细'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.record} - {self.dimension}: {self.score}"
|
||||
@@ -0,0 +1,49 @@
|
||||
from rest_framework import serializers
|
||||
from .models import TrainingRecord, TrainingScoreDetail
|
||||
|
||||
|
||||
class TrainingRecordListSerializer(serializers.ModelSerializer):
|
||||
user_name = serializers.CharField(source='user.real_name', read_only=True)
|
||||
case_title = serializers.CharField(source='case.title', read_only=True)
|
||||
teacher_name = serializers.CharField(source='teacher.real_name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TrainingRecord
|
||||
fields = [
|
||||
'id', 'user', 'user_name', 'case', 'case_title',
|
||||
'training_mode', 'case_type', 'teacher', 'teacher_name',
|
||||
'start_time', 'end_time', 'duration_seconds', 'total_score',
|
||||
'evaluation_level', 'status', 'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
|
||||
class TrainingRecordDetailSerializer(serializers.ModelSerializer):
|
||||
user_name = serializers.CharField(source='user.real_name', read_only=True)
|
||||
case_title = serializers.CharField(source='case.title', read_only=True)
|
||||
teacher_name = serializers.CharField(source='teacher.real_name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TrainingRecord
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class TrainingRecordCreateSerializer(serializers.ModelSerializer):
|
||||
"""训练记录创建序列化器"""
|
||||
class Meta:
|
||||
model = TrainingRecord
|
||||
fields = [
|
||||
'case', 'training_mode', 'case_type', 'teacher'
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['user'] = self.context['request'].user
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class TrainingScoreDetailSerializer(serializers.ModelSerializer):
|
||||
record_info = serializers.CharField(source='record', read_only=True)
|
||||
rule_dimension = serializers.CharField(source='rule.dimension', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TrainingScoreDetail
|
||||
fields = '__all__'
|
||||
@@ -0,0 +1,11 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from . import views
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'training-records', views.TrainingRecordViewSet, basename='training-record')
|
||||
router.register(r'training-score-details', views.TrainingScoreDetailViewSet, basename='training-score-detail')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
@@ -0,0 +1,104 @@
|
||||
from rest_framework import viewsets, filters, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from .models import TrainingRecord, TrainingScoreDetail
|
||||
from .serializers import (
|
||||
TrainingRecordListSerializer, TrainingRecordDetailSerializer,
|
||||
TrainingRecordCreateSerializer, TrainingScoreDetailSerializer
|
||||
)
|
||||
|
||||
|
||||
class TrainingRecordViewSet(viewsets.ModelViewSet):
|
||||
"""训练记录管理
|
||||
|
||||
list: 获取训练记录列表(支持过滤、搜索、排序)
|
||||
create: 开始训练(创建记录)
|
||||
retrieve: 获取训练详情
|
||||
update: 更新训练记录
|
||||
destroy: 删除训练记录
|
||||
"""
|
||||
queryset = TrainingRecord.objects.all()
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = [
|
||||
'user', 'case', 'training_mode', 'case_type',
|
||||
'teacher', 'evaluation_level', 'status'
|
||||
]
|
||||
search_fields = ['feedback']
|
||||
ordering_fields = ['start_time', 'end_time', 'total_score', 'created_at']
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'list':
|
||||
return TrainingRecordListSerializer
|
||||
elif self.action == 'create':
|
||||
return TrainingRecordCreateSerializer
|
||||
return TrainingRecordDetailSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""普通用户只能看到自己的记录,老师可以看到学生的"""
|
||||
queryset = super().get_queryset()
|
||||
user = self.request.user
|
||||
|
||||
# 超级管理员可以看所有
|
||||
if user.is_superuser:
|
||||
return queryset
|
||||
|
||||
# 老师可以看到自己学生的记录
|
||||
return queryset.filter(user=user) | queryset.filter(teacher=user)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def score_details(self, request, pk=None):
|
||||
"""获取训练评分明细"""
|
||||
record = self.get_object()
|
||||
details = record.score_details.all()
|
||||
serializer = TrainingScoreDetailSerializer(details, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def complete(self, request, pk=None):
|
||||
"""完成训练"""
|
||||
record = self.get_object()
|
||||
record.status = 'completed'
|
||||
record.end_time = timezone.now()
|
||||
|
||||
# 计算训练时长
|
||||
if record.start_time:
|
||||
duration = (record.end_time - record.start_time).total_seconds()
|
||||
record.duration_seconds = int(duration)
|
||||
|
||||
record.save()
|
||||
|
||||
# 更新用户统计
|
||||
user = record.user
|
||||
user.total_training_count += 1
|
||||
user.total_case_count += 1
|
||||
user.save()
|
||||
|
||||
return Response({'message': '训练已完成', 'duration_seconds': record.duration_seconds})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def abort(self, request, pk=None):
|
||||
"""中断训练"""
|
||||
record = self.get_object()
|
||||
record.status = 'aborted'
|
||||
record.end_time = timezone.now()
|
||||
record.interruption_count += 1
|
||||
record.save()
|
||||
return Response({'message': '训练已中断'})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def add_score(self, request, pk=None):
|
||||
"""添加评分"""
|
||||
record = self.get_object()
|
||||
serializer = TrainingScoreDetailSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(record=record)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class TrainingScoreDetailViewSet(viewsets.ModelViewSet):
|
||||
"""评分明细管理"""
|
||||
queryset = TrainingScoreDetail.objects.all()
|
||||
serializer_class = TrainingScoreDetailSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['record', 'rule', 'dimension']
|
||||
Reference in New Issue
Block a user