feat: add profile api

This commit is contained in:
2026-06-08 17:36:03 +08:00
parent ba9fb33062
commit 2b86c91edd
9 changed files with 452 additions and 4 deletions
@@ -0,0 +1,23 @@
# Generated by Django 5.2.14 on 2026-06-08 08:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0004_institution_code_unique'),
]
operations = [
migrations.AddField(
model_name='institution',
name='banner_url',
field=models.CharField(blank=True, help_text='机构专属图片:可为静态相对路径(如 institutions/xxx.png)或完整 http(s) URL', max_length=500, verbose_name='机构Banner图'),
),
migrations.AddField(
model_name='user',
name='practice_years',
field=models.CharField(blank=True, max_length=20, verbose_name='执业年限'),
),
]
+5
View File
@@ -51,6 +51,7 @@ class User(AbstractBaseUser, PermissionsMixin, BaseModel):
null=True, blank=True, verbose_name='所属科室'
)
title_name = models.CharField('职称', max_length=50, blank=True)
practice_years = models.CharField('执业年限', max_length=20, blank=True)
major = models.CharField('专业', max_length=100, blank=True)
training_stage = models.CharField('培训阶段', max_length=50, blank=True)
learning_target = models.CharField('学习目标', max_length=255, blank=True)
@@ -138,6 +139,10 @@ class Institution(BaseModel):
level = models.CharField('等级', max_length=30, blank=True)
province = models.CharField('省份', max_length=50, blank=True)
city = models.CharField('城市', max_length=50, blank=True)
banner_url = models.CharField(
'机构Banner图', max_length=500, blank=True,
help_text='机构专属图片:可为静态相对路径(如 institutions/xxx.png)或完整 http(s) URL'
)
class Meta:
db_table = 'institution'
+20 -4
View File
@@ -11,8 +11,8 @@ class UserSerializer(serializers.ModelSerializer):
fields = [
'id', 'username', 'real_name', 'phone', 'avatar',
'gender', 'role_type', 'institution', 'institution_name',
'department', 'department_name', 'title_name', 'major',
'training_stage', 'learning_target', 'competency_profile',
'department', 'department_name', 'title_name', 'practice_years',
'major', 'training_stage', 'learning_target', 'competency_profile',
'weak_dimensions', 'strong_dimensions', 'ai_preference',
'total_training_count', 'total_case_count', 'current_level',
'status', 'last_login_time', 'created_at', 'updated_at'
@@ -44,11 +44,27 @@ class UserUpdateSerializer(serializers.ModelSerializer):
model = User
fields = [
'real_name', 'phone', 'avatar', 'gender', 'role_type',
'institution', 'department', 'title_name', 'major',
'training_stage', 'learning_target', 'status'
'institution', 'department', 'title_name', 'practice_years',
'major', 'training_stage', 'learning_target', 'status'
]
class StudentProfileConfigSerializer(serializers.Serializer):
"""医学生信息配置(首次进入系统)——科室、专业职称、执业年限"""
department = serializers.PrimaryKeyRelatedField(
queryset=Department.objects.all(), help_text='执业科室 ID'
)
title_name = serializers.CharField(max_length=50, help_text='专业职称,如:住院医师')
practice_years = serializers.CharField(max_length=20, help_text='执业年限,如:1-3年')
def validate_department(self, value):
"""科室必须属于当前用户所在机构"""
user = self.context['request'].user
if user.institution_id and value.institution_id != user.institution_id:
raise serializers.ValidationError('所选科室不属于您所在的机构')
return value
class UserPasswordSerializer(serializers.Serializer):
"""密码修改序列化器"""
old_password = serializers.CharField(required=True, write_only=True)
+4
View File
@@ -19,6 +19,10 @@ urlpatterns = [
path('', include(router.urls)),
# 移动端机构列表(不分页,登录前可调用)
path('institution_list/', views.institution_list, name='institution-list'),
# 移动端配置页(登录后):机构信息 / 所属机构科室列表 / 学生信息配置
path('institution_info/', views.institution_info, name='institution-info'),
path('my_departments/', views.my_departments, name='my-departments'),
path('profile/config/', views.student_profile_config, name='student-profile-config'),
# 认证相关
path('auth/send-code/', send_code, name='send-code'),
path('auth/register/', register, name='register'),
+100
View File
@@ -1,3 +1,4 @@
from django.conf import settings
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny
@@ -10,6 +11,7 @@ from .auth import TRIAL_INSTITUTION_NAME
from .models import User, Role, TeacherStudentRelation, Institution, Department
from .serializers import (
UserSerializer, UserCreateSerializer, UserUpdateSerializer,
StudentProfileConfigSerializer,
RoleSerializer,
TeacherStudentRelationSerializer, InstitutionSerializer, DepartmentSerializer
)
@@ -228,3 +230,101 @@ def institution_list(request):
for inst in institutions
]
return Response(data)
# ── 配置页相关接口(移动端,首次进入系统)─────────────────────────────────────
def _build_banner_url(request, banner_value):
"""把机构 banner 字段转成完整可访问 URL。
- 空值回退到 settings.DEFAULT_INSTITUTION_BANNER
- 已是 http(s) 完整 URL 时原样返回
- 否则视为相对 STATIC_URL 的静态路径,拼成绝对 URL
"""
value = banner_value or settings.DEFAULT_INSTITUTION_BANNER
if value.startswith(('http://', 'https://')):
return value
path = '/' + settings.STATIC_URL.strip('/') + '/' + value.lstrip('/')
return request.build_absolute_uri(path)
@extend_schema(
summary='机构信息获取接口',
description='返回当前登录学生所属机构的信息,含机构专属 Banner 图 URL(用于配置页/首页顶部)。',
responses={200: None},
tags=['机构'],
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def institution_info(request):
"""机构信息获取接口 — 当前用户所属机构 + Banner 图 URL"""
inst = request.user.institution
if inst is None:
raise AppError('USER_INSTITUTION_NOT_FOUND', '当前账号未关联机构', status_code=404)
return Response({
'id': inst.id,
'code': inst.code,
'name': inst.name,
'type': inst.type,
'level': inst.level,
'province': inst.province,
'city': inst.city,
'banner_url': _build_banner_url(request, inst.banner_url),
})
@extend_schema(
summary='所属机构科室列表接口(不分页)',
description='返回当前登录学生所属机构下的全部科室,不分页,供配置页选择执业科室。',
responses={200: None},
tags=['机构'],
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def my_departments(request):
"""所属机构科室列表接口 — 当前用户机构下全部科室、不分页"""
inst = request.user.institution
if inst is None:
raise AppError('USER_INSTITUTION_NOT_FOUND', '当前账号未关联机构', status_code=404)
departments = Department.objects.filter(institution=inst).order_by('name')
data = [
{
'id': dept.id,
'name': dept.name,
'category': dept.category,
}
for dept in departments
]
return Response(data)
@extend_schema(
summary='医学生信息配置接口',
description='首次进入系统时录入学生信息:执业科室、专业职称、执业年限。'
'机构在登录时已选定,此处不再修改。',
request=StudentProfileConfigSerializer,
responses={200: UserSerializer},
tags=['用户'],
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def student_profile_config(request):
"""医学生信息配置接口 — 录入科室、职称、执业年限"""
serializer = StudentProfileConfigSerializer(
data=request.data, context={'request': request}
)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
user = request.user
user.department = data['department']
user.title_name = data['title_name']
user.practice_years = data['practice_years']
user.save(update_fields=['department', 'title_name', 'practice_years', 'updated_at'])
return Response({
'message': '配置成功',
'user': UserSerializer(user).data,
})