diff --git a/apps/organization/serializers.py b/apps/organization/serializers.py index 923c6be..328d291 100644 --- a/apps/organization/serializers.py +++ b/apps/organization/serializers.py @@ -37,11 +37,15 @@ class CmsInstitutionSerializer(serializers.ModelSerializer): value = (value or '').strip() if not value: raise AppError('CMS_VALIDATION_ERROR', '机构编码不能为空', status_code=400) - qs = Institution.objects.filter(code=value) + # 唯一性按 all_objects(含已停用)判定:编码唯一约束对软删行仍生效, + # 否则同编码重建会在写库时撞唯一约束抛 500。 + qs = Institution.all_objects.filter(code=value) if self.instance is not None: qs = qs.exclude(pk=self.instance.pk) if qs.exists(): - raise AppError('CMS_INSTITUTION_CODE_EXISTS', '机构编码已存在', status_code=400) + raise AppError('CMS_INSTITUTION_CODE_EXISTS', + '机构编码已存在(含已停用机构),如需复用请先恢复或更换编码', + status_code=400) return value @@ -58,9 +62,12 @@ class CmsDepartmentSerializer(serializers.ModelSerializer): value = (value or '').strip() if not value: raise AppError('CMS_VALIDATION_ERROR', '科室名称不能为空', status_code=400) - qs = Department.objects.filter(name=value) + # 唯一性按 all_objects(含已停用)判定,避免同名重建在写库时撞唯一约束抛 500。 + qs = Department.all_objects.filter(name=value) if self.instance is not None: qs = qs.exclude(pk=self.instance.pk) if qs.exists(): - raise AppError('CMS_DEPARTMENT_NAME_EXISTS', '科室名称已存在', status_code=400) + raise AppError('CMS_DEPARTMENT_NAME_EXISTS', + '科室名称已存在(含已停用科室),如需复用请先恢复或更换名称', + status_code=400) return value diff --git a/apps/organization/views.py b/apps/organization/views.py index 26ba2a2..4bd0d6a 100644 --- a/apps/organization/views.py +++ b/apps/organization/views.py @@ -172,14 +172,15 @@ class CmsDepartmentViewSet(viewsets.ModelViewSet): except Exception: raise AppError('CMS_IMPORT_BAD_FILE', '文件解析失败,请使用导入模板', status_code=400) - existing = set(Department.objects.values_list('name', flat=True)) + # 含已停用科室:避免导入与软删科室同名而产生重复行 + existing = set(Department.all_objects.values_list('name', flat=True)) success, errors = 0, [] for idx, row in enumerate(rows, start=2): name = (row.get('科室名称') or '').strip() if not name: errors.append({'row': idx, 'reason': '科室名称为空'}); continue if name in existing: - errors.append({'row': idx, 'reason': f'科室已存在:{name}'}); continue + errors.append({'row': idx, 'reason': f'科室已存在(含已停用):{name}'}); continue Department.objects.create(name=name, category=(row.get('分类') or '').strip()) existing.add(name) success += 1 diff --git a/apps/user/cms.py b/apps/user/cms.py index e6cd778..60ceede 100644 --- a/apps/user/cms.py +++ b/apps/user/cms.py @@ -81,11 +81,14 @@ class CmsUserWriteSerializer(serializers.ModelSerializer): value = (value or '').strip() if not re.match(r'^1[3-9]\d{9}$', value): raise AppError('CMS_VALIDATION_ERROR', '手机号格式不合法', status_code=400) + # 含已停用账号:手机号唯一约束对软删行仍生效,提示需复用应先恢复/换号 qs = User.all_objects.filter(phone=value) if self.instance is not None: qs = qs.exclude(pk=self.instance.pk) if qs.exists(): - raise AppError('CMS_USER_PHONE_EXISTS', '手机号已存在', status_code=400) + raise AppError('CMS_USER_PHONE_EXISTS', + '手机号已存在(含已停用账号),如需复用请先恢复或更换手机号', + status_code=400) return value def validate(self, attrs): @@ -131,7 +134,9 @@ class CmsUserWriteSerializer(serializers.ModelSerializer): username=phone, password=f'Pass{phone}', **validated_data ) except IntegrityError: - raise AppError('CMS_USER_PHONE_EXISTS', '手机号已存在', status_code=400) + raise AppError('CMS_USER_PHONE_EXISTS', + '手机号已存在(含已停用账号),如需复用请先恢复或更换手机号', + status_code=400) def update(self, instance, validated_data): for key, val in validated_data.items(): diff --git a/test/test_cms_department.py b/test/test_cms_department.py index fade44b..8edf0a7 100644 --- a/test/test_cms_department.py +++ b/test/test_cms_department.py @@ -68,6 +68,19 @@ class CmsDepartmentTest(CacheTestCase): self.assertFalse(Department.objects.filter(id=d.id).exists()) self.assertTrue(Department.all_objects.get(id=d.id).is_deleted) + def test_recreate_soft_deleted_name_returns_400(self): + """软删后用相同名称重建:返回 400 CMS_DEPARTMENT_NAME_EXISTS(不产生重复行)。 + + 按 all_objects 校验,避免与已停用科室同名而静默新建重复记录。 + """ + d = Department.objects.create(name='康复科', category='临床') + self.client.delete(d_detail(d.id)) + self.assertFalse(Department.objects.filter(name='康复科').exists()) + resp = self.client.post(CMS_DEPT_URL, {'name': '康复科', 'category': '临床'}) + self.assertEqual(resp.status_code, 400, resp.content) + self.assertEqual(resp.json()['code'], 'CMS_DEPARTMENT_NAME_EXISTS') + self.assertEqual(Department.all_objects.filter(name='康复科').count(), 1) + def test_import_and_export(self): f = make_xlsx(['科室名称', '分类'], [['心内科', '临床'], ['', 'x'], ['心内科', '临床']]) resp = self.client.post('/api/cms/departments/import/', {'file': f}, format='multipart') diff --git a/test/test_cms_institution.py b/test/test_cms_institution.py index 8220cea..7cb55a8 100644 --- a/test/test_cms_institution.py +++ b/test/test_cms_institution.py @@ -160,6 +160,20 @@ class CmsInstitutionCrudTest(CacheTestCase): resp = self.client.put(inst_detail_url(inst.id), {'code': 'CMS-PUT', 'name': 'Y'}) self.assertEqual(resp.status_code, 405, resp.content) + def test_recreate_soft_deleted_code_returns_400(self): + """软删后用相同编码重建:返回 400 CMS_INSTITUTION_CODE_EXISTS(而非 500)。 + + 编码唯一约束对软删行仍生效,须按 all_objects 校验,避免写库时撞约束抛 500。 + """ + inst = ensure_institution(name='待停用', code='CMS-SOFT-DUP') + self.client.delete(inst_detail_url(inst.id)) + self.assertFalse(Institution.objects.filter(code='CMS-SOFT-DUP').exists()) + resp = self.client.post(CMS_INST_URL, {'code': 'CMS-SOFT-DUP', 'name': '重建'}) + self.assertEqual(resp.status_code, 400, resp.content) + self.assertEqual(resp.json()['code'], 'CMS_INSTITUTION_CODE_EXISTS') + # 不应产生重复行(仍只有那条已软删的) + self.assertEqual(Institution.all_objects.filter(code='CMS-SOFT-DUP').count(), 1) + # ── Banner 上传(写临时静态目录,避免污染仓库)───────────────────────────────── diff --git a/test/test_cms_user.py b/test/test_cms_user.py index 85478a9..ff3ec84 100644 --- a/test/test_cms_user.py +++ b/test/test_cms_user.py @@ -127,6 +127,18 @@ class CmsUserCrudTest(CacheTestCase): obj = User.all_objects.get(id=u.id) self.assertTrue(obj.is_deleted) # 实际未物删 + def test_recreate_soft_deleted_phone_returns_400(self): + """软删后用相同手机号重建:返回 400 CMS_USER_PHONE_EXISTS(不产生重复行)。""" + u = create_test_user(phone='13922200061', role_type='student', institution=self.inst) + self.client.delete(u_detail(u.id)) + self.assertFalse(User.objects.filter(phone='13922200061').exists()) + resp = self.client.post(CMS_USER_URL, { + 'phone': '13922200061', 'real_name': '重建', 'role_type': 'student', + 'institution': self.inst.id}) + self.assertEqual(resp.status_code, 400, resp.content) + self.assertEqual(resp.json()['code'], 'CMS_USER_PHONE_EXISTS') + self.assertEqual(User.all_objects.filter(phone='13922200061').count(), 1) + def test_reset_password(self): u = create_test_user(phone='13922200070', password='OldPass1', role_type='student') resp = self.client.post(f'/api/cms/users/{u.id}/reset-password/', {})