from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status import json import ast import re from decimal import Decimal, InvalidOperation from User.models import User, Approval from User.utils import log_operation, normalize_approvers_param, build_missing_approvers_message from .models import PreFiling, ProjectRegistration, Bid, Case, Invoice, Caselog, SealApplication, Warehousing, \ RegisterPlatform, Announcement, LawyerFlie, Schedule, permission, role, CaseChangeRequest, CaseTag,Propaganda,System, ContractCounter from .contract_no import generate_next_contract_no, get_next_contract_no_preview from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from utility.utility import flies from datetime import datetime from django.db.models import Count, Q from django.db import transaction import os def get_user_team_name(username): if not username: return None try: user = User.objects.get(username=username, is_deleted=False) return user.team except User.DoesNotExist: return None def get_team_name_from_responsiblefor(responsiblefor): if not responsiblefor: return None try: data = json.loads(responsiblefor) if isinstance(responsiblefor, str) else responsiblefor except (json.JSONDecodeError, TypeError): return None if isinstance(data, dict): responsible_person = data.get('responsible_person') if responsible_person is None: return None # 兼容负责人人员ID或用户名 if str(responsible_person).strip().isdigit(): try: user = User.objects.get(id=int(responsible_person), is_deleted=False) return user.team except User.DoesNotExist: return None return get_user_team_name(responsible_person) return None def search_related_records(client_info, party_info, exclude_project_id=None): """ 根据委托人和相对方信息检索预立案、投标、立项三个表的冲突记录 只返回委托人和相对方都匹配的记录(冲突数据) Args: client_info: 委托人身份信息(字符串) party_info: 相对方身份信息(字符串) exclude_project_id: 要排除的立项ID(可选,用于排除当前正在创建的记录) Returns: dict: 包含三个表的冲突记录信息 { 'prefiling_conflicts': [...], # 预立案冲突记录列表 'project_conflicts': [...], # 立项冲突记录列表 'bid_conflicts': [...] # 投标冲突记录列表 } """ result = { 'prefiling_conflicts': [], 'project_conflicts': [], 'bid_conflicts': [] } if not client_info and not party_info: return result # 解析委托人信息和相对方信息 client_parsed = parse_person_info(client_info) if client_info else {'name': '', 'id_number': '', 'original': ''} party_parsed = parse_person_info(party_info) if party_info else {'name': '', 'id_number': '', 'original': ''} # 提取姓名/名称(用于匹配) client_name = client_parsed['name'] client_id_number = client_parsed['id_number'] client_original = client_parsed['original'] party_name = party_parsed['name'] party_id_number = party_parsed['id_number'] party_original = party_parsed['original'] # 检索预立案表 - 查找委托人和相对方都匹配的记录(冲突数据) # 注意:预立案和立项是完全独立的,没有关联关系,只根据委托人和相对方信息匹配 # 预立案表字段:client_username/party_username(JSON数组格式字符串,如:[{"index":1,"name":"张三","idNumber":"110101199001011234"}]) # 立项表字段:client_info/party_info(完整身份信息,如"张三,身份证号:110101199001011234") # # 冲突判断逻辑: # 1. 委托人和相对方都匹配(顺序相同或相反) # 2. 预立案中委托人和相对方是同一个人,且这个人在立项的委托人或相对方中 # 3. 立项中委托人和相对方是同一个人,且这个人在预立案的委托人或相对方中 if (client_info or client_name) and (party_info or party_name): matched_prefilings = [] # 检查立项中委托人和相对方是否是同一个人 is_same_person_in_project = False if client_name and party_name and client_name.strip() == party_name.strip(): is_same_person_in_project = True elif client_id_number and party_id_number and client_id_number == party_id_number: # 通过身份证号判断是否为同一人 is_same_person_in_project = True # 遍历预立案记录,解析JSON格式并匹配 prefiling_all = PreFiling.objects.filter(is_deleted=False) for pf in prefiling_all[:100]: # 限制查询数量以提高性能 if len(matched_prefilings) >= 10: break if not pf.client_username or not pf.party_username: continue # 解析预立案的client_username(JSON数组格式) pf_client_names = [] try: client_data = json.loads(pf.client_username) if isinstance(pf.client_username, str) else pf.client_username if isinstance(client_data, list): for item in client_data: if isinstance(item, dict) and 'name' in item: pf_client_names.append(item['name'].strip()) elif isinstance(client_data, dict) and 'name' in client_data: pf_client_names.append(client_data['name'].strip()) except (json.JSONDecodeError, TypeError, AttributeError): # 如果不是JSON格式,尝试直接使用字符串(兼容旧数据) pf_client_names = [pf.client_username.strip()] # 解析预立案的party_username(JSON数组格式) pf_party_names = [] try: party_data = json.loads(pf.party_username) if isinstance(pf.party_username, str) else pf.party_username if isinstance(party_data, list): for item in party_data: if isinstance(item, dict) and 'name' in item: pf_party_names.append(item['name'].strip()) elif isinstance(party_data, dict) and 'name' in party_data: pf_party_names.append(party_data['name'].strip()) except (json.JSONDecodeError, TypeError, AttributeError): # 如果不是JSON格式,尝试直接使用字符串(兼容旧数据) pf_party_names = [pf.party_username.strip()] if not pf_client_names or not pf_party_names: continue # 检查预立案中委托人和相对方是否是同一个人(所有委托人和所有相对方中有相同的名字) is_same_person_in_prefiling = bool(set(pf_client_names) & set(pf_party_names)) # 对每个委托人名字和相对方名字进行匹配 client_match = False party_match = False reverse_client_match = False reverse_party_match = False # 检查正向匹配:预立案的委托人是否在立项的委托人信息中,预立案的相对方是否在立项的相对方信息中 for pf_client in pf_client_names: if client_name and pf_client in client_name: client_match = True break elif client_original and pf_client in client_original: client_match = True break for pf_party in pf_party_names: if party_name and pf_party in party_name: party_match = True break elif party_original and pf_party in party_original: party_match = True break # 检查反向匹配:预立案的委托人是否在立项的相对方信息中,预立案的相对方是否在立项的委托人信息中 for pf_client in pf_client_names: if party_name and pf_client in party_name: reverse_client_match = True break elif party_original and pf_client in party_original: reverse_client_match = True break for pf_party in pf_party_names: if client_name and pf_party in client_name: reverse_party_match = True break elif client_original and pf_party in client_original: reverse_party_match = True break # 判断是否冲突: # 1. 正向匹配或反向匹配都成功 # 2. 预立案中委托人和相对方是同一个人,且这个人在立项的委托人或相对方信息中 # 3. 立项中委托人和相对方是同一个人,且这个人在预立案的委托人或相对方信息中 is_conflict = False if (client_match and party_match) or (reverse_client_match and reverse_party_match): is_conflict = True elif is_same_person_in_prefiling: # 预立案中委托人和相对方是同一个人,检查这个共同的名字是否在立项信息中 common_names = set(pf_client_names) & set(pf_party_names) for common_name in common_names: if (client_name and common_name in client_name) or (party_name and common_name in party_name) or \ (client_original and common_name in client_original) or (party_original and common_name in party_original): is_conflict = True break elif is_same_person_in_project: # 立项中委托人和相对方是同一个人,检查这个共同的名字是否在预立案信息中 for pf_client in pf_client_names: if (client_name and pf_client in client_name) or (party_name and pf_client in party_name) or \ (client_original and pf_client in client_original) or (party_original and pf_client in party_original): is_conflict = True break if not is_conflict: for pf_party in pf_party_names: if (client_name and pf_party in client_name) or (party_name and pf_party in party_name) or \ (client_original and pf_party in client_original) or (party_original and pf_party in party_original): is_conflict = True break if is_conflict: matched_prefilings.append({ 'id': pf.id, 'times': pf.times, 'client_username': pf.client_username, 'party_username': pf.party_username, 'description': pf.description }) result['prefiling_conflicts'] = matched_prefilings # 检索立项表(排除当前正在创建的记录)- 只查找委托人和相对方都匹配的记录(冲突数据) if client_info and party_info: project_records = ProjectRegistration.objects.filter(is_deleted=False) if exclude_project_id: project_records = project_records.exclude(id=exclude_project_id) # 委托人和相对方都匹配,或者相对方和委托人都匹配(可能是顺序相反) # 支持姓名和完整信息匹配 project_q = Q() if client_name and party_name: project_q |= (Q(client_info__icontains=client_name) & Q(party_info__icontains=party_name)) project_q |= (Q(client_info__icontains=party_name) & Q(party_info__icontains=client_name)) if client_original and party_original: project_q |= (Q(client_info__icontains=client_original[:50]) & Q(party_info__icontains=party_original[:50])) project_q |= (Q(client_info__icontains=party_original[:50]) & Q(party_info__icontains=client_original[:50])) if client_id_number and party_id_number: project_q |= (Q(client_info__icontains=client_id_number) & Q(party_info__icontains=party_id_number)) project_q |= (Q(client_info__icontains=party_id_number) & Q(party_info__icontains=client_id_number)) if project_q: project_records = project_records.filter(project_q) # 获取项目列表(最多10条) project_list = list(project_records[:10].values('id', 'ContractNo', 'times', 'type', 'client_info', 'party_info')) result['project_conflicts'] = project_list # 检索投标表 - 投标表只有BiddingUnit字段,需要检查是否包含委托人或相对方信息 # BiddingUnit可能同时包含委托人和相对方,或者只包含其中一方 bid_records = Bid.objects.filter(is_deleted=False) bid_q = Q() # 如果有提取出的名称,优先使用名称匹配 if client_name: bid_q |= Q(BiddingUnit__icontains=client_name) if party_name: bid_q |= Q(BiddingUnit__icontains=party_name) # 如果有完整信息,也尝试匹配 if client_original: bid_q |= Q(BiddingUnit__icontains=client_original[:50]) if party_original: bid_q |= Q(BiddingUnit__icontains=party_original[:50]) # 如果有身份证号,也尝试匹配 if client_id_number: bid_q |= Q(BiddingUnit__icontains=client_id_number) if party_id_number: bid_q |= Q(BiddingUnit__icontains=party_id_number) # 如果构建了查询条件,执行查询 if bid_q: bid_records = bid_records.filter(bid_q) bid_list = list(bid_records[:10].values('id', 'ProjectName', 'times', 'BiddingUnit')) result['bid_conflicts'] = bid_list return result def get_change_approvers(): """获取变更审批人(管委会或行政部)""" try: approvers = ( User.objects.filter(is_deleted=False) .filter( Q(role__RoleName__in=["管委会", "行政部"]) | Q(department__username="行政部") ) .distinct() .order_by("id") ) return [user.username for user in approvers if user.username] except Exception: return [] def build_change_approval_content(applicant_name, case, change_item, change_reason, agreement_urls): agreement_display = ",".join(agreement_urls) if agreement_urls else "无" return ( f"{applicant_name}提交案件变更申请," f"案件ID:{case.id}," f"变更事项:{change_item}," f"变更原因:{change_reason}," f"变更协议:{agreement_display}" ) def build_case_approval_content(project_registration, times, change_request=False): times_str = times or "未填写日期" content_parts = [f"{times_str}提交了一份案件信息"] if change_request: content_parts[0] = f"{times_str}提交了一份案件信息,更改了变更申请" if project_registration: if project_registration.ContractNo: content_parts.append(f"合同编号:{project_registration.ContractNo}") if project_registration.type: content_parts.append(f"项目类型:{project_registration.type}") if project_registration.client_info: # 解析客户名称:如果是JSON格式,只提取name字段 client_info_str = project_registration.client_info try: # 尝试解析为JSON数组 client_info_list = json.loads(client_info_str) if isinstance(client_info_list, list) and len(client_info_list) > 0: # 提取所有name字段,用逗号连接 client_names = [item.get('name', '') for item in client_info_list if isinstance(item, dict) and item.get('name')] client_info_str = '、'.join(client_names) if client_names else client_info_str except (json.JSONDecodeError, TypeError, AttributeError): # 如果不是JSON格式,保持原样 pass content_parts.append(f"客户名称:{client_info_str}") if project_registration.party_info: # 解析相对方名称:如果是JSON格式,只提取name字段 party_info_str = project_registration.party_info try: # 尝试解析为JSON数组 party_info_list = json.loads(party_info_str) if isinstance(party_info_list, list) and len(party_info_list) > 0: # 提取所有name字段,用逗号连接 party_names = [item.get('name', '') for item in party_info_list if isinstance(item, dict) and item.get('name')] party_info_str = '、'.join(party_names) if party_names else party_info_str except (json.JSONDecodeError, TypeError, AttributeError): # 如果不是JSON格式,保持原样 pass content_parts.append(f"相对方名称:{party_info_str}") if project_registration.description: content_parts.append(f"项目简述:{project_registration.description}") try: responsiblefor_dict = json.loads(project_registration.responsiblefor) if project_registration.responsiblefor else {} except (json.JSONDecodeError, TypeError): responsiblefor_dict = project_registration.responsiblefor if project_registration.responsiblefor else {} if isinstance(responsiblefor_dict, dict) and responsiblefor_dict.get("responsible_person"): content_parts.append(f"承办人:{responsiblefor_dict.get('responsible_person')}") if project_registration.charge: content_parts.append(f"收费情况:{project_registration.charge}") if change_request: content_parts.append("请审核变更申请") else: content_parts.append("请审核") return ",".join(content_parts) def normalize_amount_value(value): if value is None: return "0" if isinstance(value, (int, float, Decimal)): return str(value) value_str = str(value).strip() if value_str in ["", "未开票", "None", "null"]: return "0" numbers = re.findall(r"\d+\.?\d*", value_str) return numbers[0] if numbers else "0" class registration(APIView): def post(self, request, *args, **kwargs): """ 预立案登记 :param request: :param args: :param kwargs: :return: """ token = request.META.get('token') times = request.data.get('times') client_username = request.data.get('client_username') party_username = request.data.get('party_username') description = request.data.get('description') Undertaker = request.data.get('Undertaker') user = User.objects.get(token=token, is_deleted=False).username if not all([times, description, Undertaker]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) prefiling = PreFiling.objects.create( times=times, client_username=client_username, party_username=party_username, description=description, Undertaker=Undertaker, submit=user ) # 记录操作日志 new_data = { 'id': prefiling.id, 'client_username': prefiling.client_username, 'party_username': prefiling.party_username, 'undertaker': prefiling.Undertaker } log_operation( request=request, operation_type='CREATE', module='Business', action='新增预立案登记', target_type='PreFiling', target_id=prefiling.id, target_name=f'{prefiling.client_username} vs {prefiling.party_username}', new_data=new_data, remark=f'新增预立案登记:委托人 {prefiling.client_username},相对方 {prefiling.party_username}' ) return Response({'message': '登记成功', 'code': 0}, status=status.HTTP_200_OK) class registrationList(APIView): def post(self, request, *args, **kwargs): """ 预立案登记列表接口 注意:由于预立案和立项登记、投标登记已经拆分,现在返回所有未删除的预立案 :param request: :param args: :param kwargs: :return: """ # 由于预立案和立项登记、投标登记已经拆分,不再需要检查关联 # 直接返回所有未删除的预立案 prefiling_list = PreFiling.objects.filter(is_deleted=False) data = [] for prefiling in prefiling_list: data.append({ "id": prefiling.id, "client_username": prefiling.client_username, "party_username": prefiling.party_username, }) return Response({'message': '展示成功', 'data': data, 'code': 0}, status=status.HTTP_200_OK) class registrationDetail(APIView): def post(self, request, *args, **kwargs): """ 预立案登记展示 :param request: :param args: :param kwargs: :return: """ page = request.data.get('page') per_page = request.data.get('per_page') times = request.data.get('times') end_time = request.data.get('end_time') Undertaker = request.data.get('Undertaker') if not all([page, per_page]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) Q_obj = Q() if times and end_time: Q_obj &= Q(times__gte=times) & Q(times__lte=end_time) if Undertaker: Q_obj &= Q(Undertaker__icontains=Undertaker) pre = PreFiling.objects.filter(Q_obj, is_deleted=False).order_by('-id') total = len(pre) paginator = Paginator(pre, per_page) try: user_agents_page = paginator.page(page) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) data = [] for info in user_agents_page.object_list: data.append({ "id": info.id, 'times': info.times, "client_username": info.client_username, "party_username": info.party_username, "description": info.description, "Undertaker": info.Undertaker, }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class EditRegistration(APIView): def post(self, request, *args, **kwargs): """ 编辑预立案登记 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') times = request.data.get('times') client_username = request.data.get('client_username') party_username = request.data.get('party_username') description = request.data.get('description') Undertaker = request.data.get('Undertaker') if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: pre = PreFiling.objects.get(id=id, is_deleted=False) except PreFiling.DoesNotExist: return Response({'status': 'error', 'message': '预立案登记不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 更新字段(如果提供了值) if times: pre.times = times pre.save(update_fields=['times']) if client_username: pre.client_username = client_username pre.save(update_fields=['client_username']) if party_username: pre.party_username = party_username pre.save(update_fields=['party_username']) if description: pre.description = description pre.save(update_fields=['description']) if Undertaker: pre.Undertaker = Undertaker pre.save(update_fields=['Undertaker']) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class Project(APIView): def post(self, request, *args, **kwargs): """ 立项登记 - 独立创建,不再需要预立案。 必填:项目类型、立项日期、项目简述、收费情况、承办人信息、合同。 合同编号(ContractNo)可选:不传或传空时由后端按规则自动生成(校准(字)字【年份】第n号)。 相对方(party_info)为非必填项,委托人(client_info)可为空。 :param request: :param args: :param kwargs: :return: """ project_type = request.data.get('type') ContractNo = request.data.get('ContractNo') if ContractNo is not None and not isinstance(ContractNo, str): ContractNo = str(ContractNo) times = request.data.get('times') client_info = request.data.get('client_info') # 委托人身份信息(可选) party_info = request.data.get('party_info') # 相对方身份信息(非必填) description = request.data.get('description') # 项目简述 responsiblefor = request.data.get('responsiblefor') # 承办人信息(字典格式) charge = request.data.get('charge') contract = request.FILES.getlist('contract') approvers = request.data.get('approvers') # 审核人列表(可选,多人团队时需要,推荐:用户ID数组如[1,2,3],兼容:用户名数组) # 兼容旧接口:如果传了 personincharge,转换为 approvers personincharge = request.data.get('personincharge') approvers = normalize_approvers_param(approvers, personincharge) import datetime import json # 明确拒绝 user_id 参数(此接口不再需要预立案,不需要 user_id) if 'user_id' in request.data: return Response({'status': 'error', 'message': '此接口不需要 user_id 参数,立项登记已独立创建', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 验证必填字段(合同编号可选,不传则后端自动生成) if not all([project_type, times, description, charge]): return Response({'status': 'error', 'message': '缺少必填参数(项目类型、立项日期、项目简述、收费情况)', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 验证承办人信息(字典格式) if not responsiblefor: return Response({'status': 'error', 'message': '承办人信息不能为空', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 解析承办人信息(支持字符串JSON或字典) try: if isinstance(responsiblefor, str): responsiblefor_dict = json.loads(responsiblefor) else: responsiblefor_dict = responsiblefor # 验证承办人(responsible_person)必填 if not responsiblefor_dict.get('responsible_person'): return Response({'status': 'error', 'message': '承办人不能为空', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 将字典转换为JSON字符串存储 responsiblefor_str = json.dumps(responsiblefor_dict, ensure_ascii=False) except (json.JSONDecodeError, TypeError, AttributeError): return Response({'status': 'error', 'message': '承办人信息格式错误,应为字典格式', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) if contract: contract = flies(contract) else: contract = [] # 将文件URL列表转换为字符串存储(如果有多个URL,用逗号分隔) if contract: # 如果只有一个URL,直接存储;如果有多个,用逗号分隔 contract_str = contract[0] if len(contract) == 1 else ','.join(contract) else: contract_str = "" need_auto_contract_no = not (ContractNo and ContractNo.strip()) if not need_auto_contract_no: # 用户传入合同编号:检查是否已存在 existing_project = ProjectRegistration.objects.filter(ContractNo=ContractNo.strip(), is_deleted=False).first() if existing_project: return Response({'status': 'error', 'message': '该合同编号已存在,不能重复创建', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) contract_year = int(times[:4]) if times and len(times) >= 4 else datetime.now().year # 使用事务确保创建立项和更新计数器是原子操作 with transaction.atomic(): if need_auto_contract_no: ContractNo = generate_next_contract_no(project_type, contract_year) if not ContractNo: return Response({ 'status': 'error', 'message': f'项目类型「{project_type}」未配置合同编号规则,无法自动生成。请手动填写合同编号,或选择已配置的类型(如:法律顾问、专项服务、民事案件代理等)', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 创建立项登记(相对方可为空,空字符串统一为 None) pro = ProjectRegistration.objects.create( type=project_type, ContractNo=ContractNo.strip() if isinstance(ContractNo, str) else ContractNo, times=times, client_info=client_info or None, party_info=party_info if party_info not in (None, '') else None, description=description, responsiblefor=responsiblefor_str, charge=charge, contract=contract_str, state="审核中", ) # 仅当用户手动传入合同编号时,更新计数器(与自动生成逻辑一致,保证序号连续) if not need_auto_contract_no: try: counter, created = ContractCounter.objects.select_for_update().get_or_create( project_type=project_type, year=contract_year, defaults={'current_number': 1} ) if not created: counter.current_number += 1 counter.save(update_fields=['current_number', 'updated_at']) except Exception as e: import logging logging.warning(f"更新合同编号计数器失败: {e}") today = datetime.datetime.now() formatted_date = today.strftime("%Y-%m-%d") # 获取团队信息:优先承办人,其次当前登录用户 team_name = get_team_name_from_responsiblefor(responsiblefor_dict) request_user = None token = request.META.get('token') or request.META.get('HTTP_AUTHORIZATION', '').replace('Bearer ', '') if token: try: request_user = User.objects.get(token=token, is_deleted=False) if not team_name: team_name = request_user.team except User.DoesNotExist: pass # 使用统一的审核流程函数 from User.utils import create_approval_with_team_logic # 构建承办人信息描述 responsible_desc = responsiblefor_dict.get('responsible_person', '') if responsiblefor_dict.get('main_lawyer'): responsible_desc += f",主办律师:{responsiblefor_dict.get('main_lawyer')}" if responsiblefor_dict.get('assistant_lawyer'): responsible_desc += f",协办律师:{responsiblefor_dict.get('assistant_lawyer')}" if responsiblefor_dict.get('case_manager_lawyer'): responsible_desc += f",案管律师:{responsiblefor_dict.get('case_manager_lawyer')}" # 检索相关记录(预立案、投标、立项)- 使用 conflict_search 函数,支持更灵活的参数组合 related_records_info = conflict_search(client_info=client_info, party_info=party_info, exclude_project_id=pro.id) # 构建冲突信息文本(用于content字段,保持向后兼容) conflict_parts = [] if related_records_info['prefiling_conflicts']: conflict_parts.append(f"预立案冲突:{len(related_records_info['prefiling_conflicts'])}条(ID:{','.join([str(r['id']) for r in related_records_info['prefiling_conflicts'][:5]])}{'...' if len(related_records_info['prefiling_conflicts']) > 5 else ''})") if related_records_info['project_conflicts']: conflict_parts.append(f"立项冲突:{len(related_records_info['project_conflicts'])}条(合同编号:{','.join([r['ContractNo'] for r in related_records_info['project_conflicts'][:3]])}{'...' if len(related_records_info['project_conflicts']) > 3 else ''})") if related_records_info['bid_conflicts']: conflict_parts.append(f"投标冲突:{len(related_records_info['bid_conflicts'])}条(项目:{','.join([r['ProjectName'][:20] if r['ProjectName'] else '' for r in related_records_info['bid_conflicts'][:3]])}{'...' if len(related_records_info['bid_conflicts']) > 3 else ''})") conflict_text = ";冲突记录:" + ";".join(conflict_parts) if conflict_parts else "" applicant_name = request_user.username if request_user else "" content = f"{responsiblefor_dict.get('responsible_person')}在{times}办理立项登记,项目类型:{project_type},合同编号:{ContractNo},{responsible_desc},收费情况:{charge},申请人:{applicant_name}{conflict_text}" approval, approvers_order_json, needs_approval = create_approval_with_team_logic( team_name=team_name, approvers=approvers, title=responsiblefor_dict.get('responsible_person', '') + "立项登记", content=content, approval_type="立项登记", user_id=pro.id, business_record=pro, today=formatted_date, applicant=applicant_name ) # 如果返回None且需要审核,说明缺少审核人 if approval is None and needs_approval: return Response({ 'status': 'error', 'message': build_missing_approvers_message(team_name, approvers), 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 记录操作日志 new_data = { 'id': pro.id, 'contract_no': pro.ContractNo, 'type': pro.type, 'responsiblefor': responsiblefor_dict, 'times': pro.times } log_operation( request=request, operation_type='CREATE', module='Business', action='新增立项登记', target_type='ProjectRegistration', target_id=pro.id, target_name=pro.ContractNo, new_data=new_data, remark=f'新增立项登记:合同编号 {pro.ContractNo},承办人 {responsiblefor_dict.get("responsible_person", "")}' ) return Response({ 'message': '登记成功', 'code': 0, 'contract_no': pro.ContractNo, }, status=status.HTTP_200_OK) class Projectquerytype(APIView): def post(self, request, *args, **kwargs): """ 根据年份和项目类型,先查该年份该项目类型的编号(序号),再组装成合同编号返回。 入参: - type(必填):项目类型,如 法律顾问、专项服务 - year(可选):年份,如 2025;不传则用 times 或当前年 - times(可选):日期字符串,取前四位作为年份 逻辑:1) 按 年份 + 项目类型 查询当前已用到的编号(序号) 2) 下一个可用编号 = 当前编号 + 1,组装成「校准(字)字【年份】第n号」 返回 data: - contract_no:组装好的合同编号(下一个可用),如 校准(顾)字【2025】第1号 - number:下一个可用序号 - count:该年份该项目类型当前已用数量(已用到的最大序号) """ project_type = request.data.get('type') if not project_type: return Response({'status': 'error', 'message': '缺少参数type', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 解析年份:优先 year,其次 times 前四位,否则当前年 year_param = request.data.get('year') times = request.data.get('times') if year_param is not None and str(year_param).strip(): try: current_year = int(str(year_param).strip()[:4]) except ValueError: current_year = datetime.now().year elif times and len(str(times)) >= 4: try: current_year = int(str(times)[:4]) except ValueError: current_year = datetime.now().year else: current_year = datetime.now().year # 1) 根据年份 + 项目类型 查询该年份该项目类型的编号(当前已用到的序号) try: with transaction.atomic(): counter = ContractCounter.objects.filter( project_type=project_type, year=current_year ).first() count = counter.current_number if counter else 0 except Exception: start_of_year = datetime(current_year, 1, 1) end_of_year = datetime(current_year, 12, 31) start_of_year_str = start_of_year.strftime("%Y-%m-%d") end_of_year_str = end_of_year.strftime("%Y-%m-%d") count = ProjectRegistration.objects.filter( type=project_type, times__gte=start_of_year_str, times__lte=end_of_year_str, is_deleted=False ).count() # 2) 下一个可用编号 = 当前数量 + 1,组装成合同编号 next_number, contract_no = get_next_contract_no_preview(project_type, current_year) if not contract_no: return Response({ 'status': 'error', 'message': f'项目类型「{project_type}」未配置合同编号规则,无法组装编号', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) data = { "contract_no": contract_no, "number": next_number, "count": count, } return Response({'message': '查询成功', "data": data, 'code': 0}, status=status.HTTP_200_OK) def _get_project_registration_queryset_by_permission(request, base_q=None): """ 立项登记权限:管理员可见全部,承办人仅可见自己承办的立项。 base_q: 额外筛选条件(Q 对象),可为 None。 返回 (queryset, error_response)。error_response 非 None 时直接 return 给前端。 """ token = request.META.get('token') if not token: return None, Response({'status': 'error', 'message': '需要登录', 'code': 1}, status=status.HTTP_401_UNAUTHORIZED) try: current_user = User.objects.get(token=token, is_deleted=False) except User.DoesNotExist: return None, Response({'status': 'error', 'message': '用户不存在或未登录', 'code': 1}, status=status.HTTP_401_UNAUTHORIZED) is_admin = (current_user.username == 'admin' or current_user.account == 'admin') q = base_q if base_q is not None else Q() q &= Q(is_deleted=False) if not is_admin: # 承办人只能看到自己承办的立项(responsiblefor 中包含当前用户名,如 responsible_person 等) q &= Q(responsiblefor__icontains=current_user.username) return ProjectRegistration.objects.filter(q), None class ProjectDetail(APIView): def post(self, request, *args, **kwargs): """ 立项登记展示 权限:管理员可见全部立项;承办人仅可见自己承办的立项(负责人修改为承办人)。 :param request: :param args: :param kwargs: :return: """ page = request.data.get('page') per_page = request.data.get('per_page') times = request.data.get('times') end_time = request.data.get('end_time') client_info = request.data.get('client_info') # 委托人信息搜索 party_info = request.data.get('party_info') # 相对方信息搜索 if not all([page, per_page]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) Q_obj = Q() if times and end_time: Q_obj &= Q(times__gte=times) & Q(times__lte=end_time) if client_info: Q_obj &= Q(client_info__icontains=client_info) if party_info: Q_obj &= Q(party_info__icontains=party_info) pre_qs, err = _get_project_registration_queryset_by_permission(request, Q_obj) if err is not None: return err pre = pre_qs.order_by('-id') total = len(pre) paginator = Paginator(pre, per_page) try: user_agents_page = paginator.page(page) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) data = [] import json for info in user_agents_page.object_list: # 解析承办人信息(JSON字符串转字典,原负责人即承办人) try: responsiblefor_dict = json.loads(info.responsiblefor) if info.responsiblefor else {} except Exception: responsiblefor_dict = info.responsiblefor if info.responsiblefor else {} data.append({ "id": info.id, 'times': info.times, "type": info.type, "ContractNo": info.ContractNo, "client_info": info.client_info, "party_info": info.party_info, "description": info.description, "responsiblefor": responsiblefor_dict, # 承办人信息(字典格式) "charge": info.charge, "contract": info.contract, "state": info.state, }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class EditProject(APIView): def post(self, request, *args, **kwargs): """ 编辑立案登记 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') if not id: return Response({'status': 'error', 'message': '缺少参数id', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 检查不可修改的参数(前端不会传入,但作为安全措施进行检查) forbidden_params = [] if 'type' in request.data: forbidden_params.append('type') if 'ContractNo' in request.data: forbidden_params.append('ContractNo') if forbidden_params: return Response({ 'status': 'error', 'message': f'以下参数不允许修改: {", ".join(forbidden_params)}', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 获取可修改的参数(前端只传入这些参数) times = request.data.get('times') client_info = request.data.get('client_info') party_info = request.data.get('party_info') description = request.data.get('description') responsiblefor = request.data.get('responsiblefor') # 承办人信息(字典格式) charge = request.data.get('charge') contract = request.FILES.getlist('contract') approvers = request.data.get('approvers') # 审核人列表(可选,团队类型时需要) personincharge = request.data.get('personincharge') approvers = normalize_approvers_param(approvers, personincharge) import json try: pro = ProjectRegistration.objects.get(id=id, is_deleted=False) except ProjectRegistration.DoesNotExist: return Response({'status': 'error', 'message': '立案登记不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) import datetime # 保存原始值用于日志记录 original_type = pro.type original_ContractNo = pro.ContractNo try: original_responsiblefor = json.loads(pro.responsiblefor) if pro.responsiblefor else {} except: original_responsiblefor = {} original_times = pro.times original_charge = pro.charge update_fields_list = [] if contract: contract = flies(contract) # 将文件URL列表转换为字符串存储(如果有多个URL,用逗号分隔) if contract: contract_str = contract[0] if len(contract) == 1 else ','.join(contract) else: contract_str = "" pro.contract = contract_str update_fields_list.append('contract') if times: pro.times = times update_fields_list.append('times') if responsiblefor: # 解析承办人信息(支持字符串JSON或字典) try: if isinstance(responsiblefor, str): responsiblefor_dict = json.loads(responsiblefor) else: responsiblefor_dict = responsiblefor # 验证承办人(responsible_person)必填 if not responsiblefor_dict.get('responsible_person'): return Response({'status': 'error', 'message': '承办人不能为空', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 将字典转换为JSON字符串存储 responsiblefor_str = json.dumps(responsiblefor_dict, ensure_ascii=False) pro.responsiblefor = responsiblefor_str update_fields_list.append('responsiblefor') except (json.JSONDecodeError, TypeError, AttributeError): return Response({'status': 'error', 'message': '承办人信息格式错误,应为字典格式', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) if charge: pro.charge = charge update_fields_list.append('charge') if client_info: pro.client_info = client_info update_fields_list.append('client_info') if party_info: pro.party_info = party_info update_fields_list.append('party_info') if description: pro.description = description update_fields_list.append('description') pro.state = "审核中" update_fields_list.append('state') # 记录操作前的数据(original_responsiblefor已经是字典格式) old_data = { 'id': pro.id, 'contract_no': original_ContractNo, 'type': original_type, 'responsiblefor': original_responsiblefor if isinstance(original_responsiblefor, dict) else {}, 'times': original_times, 'charge': original_charge } if update_fields_list: pro.save(update_fields=update_fields_list) # 记录操作后的数据 try: current_responsiblefor = json.loads(pro.responsiblefor) if pro.responsiblefor else {} except: current_responsiblefor = pro.responsiblefor if pro.responsiblefor else {} new_data = { 'id': pro.id, 'contract_no': pro.ContractNo, 'type': pro.type, 'responsiblefor': current_responsiblefor, 'times': pro.times, 'charge': pro.charge } today = datetime.datetime.now() formatted_date = today.strftime("%Y-%m-%d") # 获取承办人名称用于审批记录 if responsiblefor: current_responsiblefor = responsiblefor_dict.get('responsible_person', '') # 构建承办人信息描述 responsible_desc = current_responsiblefor if responsiblefor_dict.get('main_lawyer'): responsible_desc += f",主办律师:{responsiblefor_dict.get('main_lawyer')}" if responsiblefor_dict.get('assistant_lawyer'): responsible_desc += f",助理律师:{responsiblefor_dict.get('assistant_lawyer')}" if responsiblefor_dict.get('case_manager_lawyer'): responsible_desc += f",案管律师:{responsiblefor_dict.get('case_manager_lawyer')}" else: current_responsiblefor = original_responsiblefor.get('responsible_person', '') if isinstance(original_responsiblefor, dict) else '' responsible_desc = current_responsiblefor edit_user = None token = request.META.get('token') or request.META.get('HTTP_AUTHORIZATION', '').replace('Bearer ', '') if token: try: edit_user = User.objects.get(token=token, is_deleted=False) except User.DoesNotExist: pass applicant_name = edit_user.username if edit_user else "" from User.utils import create_approval_with_team_logic team_name = get_team_name_from_responsiblefor(responsiblefor_dict if responsiblefor else original_responsiblefor) content = current_responsiblefor + "在" + ( times or original_times) + "办理立项登记,项目类型:" + original_type + ",合同编号:" + original_ContractNo + "描述:" + ",承办人:" + responsible_desc + ",收费情况:" + (charge or original_charge) + ",申请人:" + applicant_name approval, approvers_order_json, needs_approval = create_approval_with_team_logic( team_name=team_name, approvers=approvers, title=current_responsiblefor + "立项登记重新编辑", content=content, approval_type="立项登记", user_id=pro.id, business_record=pro, today=formatted_date, applicant=applicant_name ) if approval is None and needs_approval: return Response({ 'status': 'error', 'message': build_missing_approvers_message(team_name, approvers), 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 记录操作日志 log_operation( request=request, operation_type='UPDATE', module='Business', action='编辑立项登记', target_type='ProjectRegistration', target_id=pro.id, target_name=pro.ContractNo, old_data=old_data, new_data=new_data, remark=f'编辑立项登记:合同编号 {pro.ContractNo}' ) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class DeleteProject(APIView): def post(self, request, *args, **kwargs): """ 删除立项登记 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: pro = ProjectRegistration.objects.get(id=id, is_deleted=False) except ProjectRegistration.DoesNotExist: return Response({'status': 'error', 'message': '立项登记不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 检查是否已经被案件管理关联 case_exists = Case.objects.filter(project_id=pro.id, is_deleted=False).exists() if case_exists: return Response({'status': 'error', 'message': '该立项已被案件管理关联,无法删除', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 记录操作前的数据 old_data = { 'id': pro.id, 'contract_no': pro.ContractNo, 'type': pro.type, 'responsiblefor': pro.responsiblefor, 'client_info': pro.client_info } # 软删除:更新 is_deleted 字段 pro.is_deleted = True pro.save() # 记录操作日志 log_operation( request=request, operation_type='DELETE', module='Business', action='删除立项登记', target_type='ProjectRegistration', target_id=pro.id, target_name=pro.ContractNo, old_data=old_data, remark=f'删除立项登记:合同编号 {pro.ContractNo}' ) return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) class BidRegistration(APIView): def post(self, request, *args, **kwargs): """ 投标登记 - 独立创建,不再关联预立案 :param request: :param args: :param kwargs: :return: """ BiddingUnit = request.data.get('BiddingUnit') ProjectName = request.data.get('ProjectName') times = request.data.get('times') BiddingAnnouncement = request.FILES.getlist('BiddingAnnouncement') approvers = request.data.get('approvers') # 审核人列表(可选,多人团队时需要,推荐:用户ID数组如[1,2,3],兼容:用户名数组) # 兼容旧接口:如果传了 personincharge,转换为 approvers personincharge = request.data.get('personincharge') approvers = normalize_approvers_param(approvers, personincharge) if not all([BiddingUnit, ProjectName, times]): return Response({'status': 'error', 'message': '缺少必填参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) import datetime # 获取当前用户信息(用于团队信息及申请人) token = request.META.get('token') team_name = None user = None try: user = User.objects.get(token=token, is_deleted=False) team_name = user.team except User.DoesNotExist: pass bib = Bid.objects.create( BiddingUnit=BiddingUnit, ProjectName=ProjectName, times=times, BiddingAnnouncement=json.dumps(flies(BiddingAnnouncement)), state="审核中", ) today = datetime.datetime.now() formatted_date = today.strftime("%Y-%m-%d") # 利益冲突检索(与立项相同逻辑):比对预立案、立项、投标三张表 conflict_result = conflict_search(bidding_unit=BiddingUnit, exclude_bid_id=bib.id) conflict_parts = [] if conflict_result['prefiling_conflicts']: conflict_parts.append(f"预立案冲突:{len(conflict_result['prefiling_conflicts'])}条(ID:{','.join([str(r['id']) for r in conflict_result['prefiling_conflicts'][:5]])}{'...' if len(conflict_result['prefiling_conflicts']) > 5 else ''})") if conflict_result['project_conflicts']: conflict_parts.append(f"立项冲突:{len(conflict_result['project_conflicts'])}条(合同编号:{','.join([r['ContractNo'] for r in conflict_result['project_conflicts'][:3]])}{'...' if len(conflict_result['project_conflicts']) > 3 else ''})") if conflict_result['bid_conflicts']: conflict_parts.append(f"投标冲突:{len(conflict_result['bid_conflicts'])}条(项目:{','.join([(r.get('ProjectName') or '')[:20] for r in conflict_result['bid_conflicts'][:3]])}{'...' if len(conflict_result['bid_conflicts']) > 3 else ''})") conflict_text = ";冲突记录:" + ";".join(conflict_parts) if conflict_parts else "" # 使用统一的审核流程函数 from User.utils import create_approval_with_team_logic applicant_name = user.username if user else "" content = f"项目名称:{ProjectName},申请日期:{times},招标单位:{BiddingUnit},申请人:{applicant_name}{conflict_text}" approval, approvers_order_json, needs_approval = create_approval_with_team_logic( team_name=team_name, approvers=approvers, title=ProjectName + "投标登记", content=content, approval_type="投标登记", user_id=bib.id, business_record=bib, today=formatted_date, applicant=applicant_name ) # 如果返回None且需要审核,说明缺少审核人 if approval is None and needs_approval: return Response({ 'status': 'error', 'message': build_missing_approvers_message(team_name, approvers), 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) return Response({'message': '登记成功', 'code': 0}, status=status.HTTP_200_OK) class BidDetail(APIView): def post(self, request, *args, **kwargs): """ 投标登记展示(列表+搜索) 支持搜索:project_name 项目名称、client_username 招标单位名字、times/end_time 日期范围、keyword 关键词。 """ page = request.data.get('page') per_page = request.data.get('per_page') times = request.data.get('times') end_time = request.data.get('end_time') # 项目名称:兼容 project_name / projectName project_name = (request.data.get('project_name') or request.data.get('projectName') or '').strip() # 招标单位名字:变量名 client_username,对应库表字段 BiddingUnit client_username = (request.data.get('client_username') or '').strip() # 关键词:同时匹配项目名称和招标单位 keyword = (request.data.get('keyword') or request.data.get('search') or '').strip() if not all([page, per_page]): return Response({'status': 'error', 'message': '缺少参数 page 或 per_page', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) Q_obj = Q(is_deleted=False) if times and end_time: Q_obj &= Q(times__gte=times) & Q(times__lte=end_time) if project_name: Q_obj &= Q(ProjectName__icontains=project_name) if client_username: Q_obj &= Q(BiddingUnit__icontains=client_username) if keyword: Q_obj &= (Q(ProjectName__icontains=keyword) | Q(BiddingUnit__icontains=keyword)) qs = Bid.objects.filter(Q_obj).order_by('-id') total = qs.count() paginator = Paginator(qs, per_page) try: user_agents_page = paginator.page(int(page)) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) data = [] for info in user_agents_page.object_list: data.append({ "id": info.id, "times": info.times, "BiddingUnit": info.BiddingUnit, "ProjectName": info.ProjectName, "BiddingAnnouncement": info.BiddingAnnouncement, "state": info.state, }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class EditBid(APIView): def post(self, request, *args, **kwargs): """ 编辑投标登记 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') BiddingUnit = request.data.get('BiddingUnit') ProjectName = request.data.get('ProjectName') times = request.data.get('times') BiddingAnnouncement = request.FILES.getlist('BiddingAnnouncement') approvers = request.data.get('approvers') personincharge = request.data.get('personincharge') approvers = normalize_approvers_param(approvers, personincharge) if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: bid = Bid.objects.get(id=id, is_deleted=False) except Bid.DoesNotExist: return Response({'status': 'error', 'message': '投标登记不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) if BiddingUnit: bid.BiddingUnit = BiddingUnit bid.save(update_fields=['BiddingUnit']) if ProjectName: bid.ProjectName = ProjectName bid.save(update_fields=['ProjectName']) if times: bid.times = times bid.save(update_fields=['times']) if BiddingAnnouncement: bid.BiddingAnnouncement = json.dumps(flies(BiddingAnnouncement)) bid.save(update_fields=['BiddingAnnouncement']) if approvers: import datetime today = datetime.datetime.now() formatted_date = today.strftime("%Y-%m-%d") team_name = None edit_user = None token = request.META.get('token') if token: try: edit_user = User.objects.get(token=token, is_deleted=False) team_name = edit_user.team except User.DoesNotExist: team_name = None from User.utils import create_approval_with_team_logic applicant_name = edit_user.username if edit_user else "" content_edit = f"项目名称:{bid.ProjectName},申请日期:{times or bid.times},申请人:{applicant_name}" approval, approvers_order_json, needs_approval = create_approval_with_team_logic( team_name=team_name, approvers=approvers, title=bid.ProjectName + "投标登记重新编辑", content=content_edit, approval_type="投标登记", user_id=bid.id, business_record=bid, today=formatted_date, applicant=applicant_name ) if approval is None and needs_approval: return Response({ 'status': 'error', 'message': build_missing_approvers_message(team_name, approvers), 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class DeleteBid(APIView): def post(self, request, *args, **kwargs): """ 删除投标登记 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: bid = Bid.objects.get(id=id, is_deleted=False) except Bid.DoesNotExist: return Response({'status': 'error', 'message': '投标登记不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 记录操作前的数据 old_data = { 'id': bid.id, 'project_name': bid.ProjectName, 'times': bid.times } # 软删除:更新 is_deleted 字段 bid.is_deleted = True bid.save() # 记录操作日志 log_operation( request=request, operation_type='DELETE', module='Business', action='删除投标登记', target_type='Bid', target_id=bid.id, target_name=bid.ProjectName, old_data=old_data, remark=f'删除投标登记:{bid.ProjectName}' ) return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) class caseManagement(APIView): def post(self, request, *args, **kwargs): """ 补充案件资料(立项审批通过即成案件,本接口用于创建/补充案件材料) - 立项审批通过后,用本接口补充合同返还、结案申请等资料。 - 首次调用(该立项尚无案件):创建案件,立案时间 = 立项审批通过时间(无则用请求 times 或当前日期)。 - 再次调用(该立项已有案件):仅更新/补充材料,不修改立案时间。 :param request: :param args: :param kwargs: :return: """ project_id = request.data.get('project_id') # 立项登记ID times = request.data.get('times') # 立案时间(仅当无审批通过时间时使用) Contractreturn = request.FILES.getlist('Contractreturn') Closingapplication = request.FILES.getlist('Closingapplication') invoice_status = request.data.get('invoice_status') # 已开票 paymentcollection = request.data.get('paymentcollection') tag_ids = request.data.get('tag_ids') # 标签ID列表(数组或逗号分隔的字符串) approvers = request.data.get('approvers') personincharge = request.data.get('personincharge') approvers = normalize_approvers_param(approvers, personincharge) # 检查是否已立项 if not project_id: return Response({'status': 'error', 'message': '缺少参数project_id(立项登记ID)', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 使用事务和行级锁防止并发重复创建 try: with transaction.atomic(): # 使用 select_for_update 锁定立项登记记录,防止并发问题 project_registration = ProjectRegistration.objects.select_for_update().get(id=project_id, is_deleted=False) # 再次检查是否已经存在案件(在锁内检查,防止并发重复创建) case = Case.objects.filter(project_id=project_id, is_deleted=False).first() if not case: # 立案时间:优先取立项审批通过时间,否则用请求 times 或当前日期 project_approval = Approval.objects.filter( type='立项登记', user_id=str(project_id), state='已通过', is_deleted=False ).order_by('-id').first() if project_approval and getattr(project_approval, 'completeTiem', None): filing_time = project_approval.completeTiem.strftime('%Y-%m-%d') else: filing_time = times if times else datetime.now().strftime('%Y-%m-%d') # 处理材料:上传后存储URL列表(代理合同字段已从案件管理移除,创建时存空) contract_return_list = flies(Contractreturn) closing_application_list = flies(Closingapplication) project_contract_no = project_registration.ContractNo if project_registration else None project_type = project_registration.type if project_registration else None project_client_name = project_registration.client_info if project_registration else None project_party_name = project_registration.party_info if project_registration else None project_description = project_registration.description if project_registration else None project_responsiblefor = project_registration.responsiblefor if project_registration else None project_charge = project_registration.charge if project_registration else None # 创建新案件(立案时间 = 审批通过时间) case_id = Case.objects.create( project_id=project_id, contract_no=project_contract_no, project_type=project_type, client_name=project_client_name, party_name=project_party_name, project_description=project_description, responsiblefor=project_responsiblefor, charge=project_charge, times=filing_time, AgencyContract=json.dumps([], ensure_ascii=False), # 代理合同已从案件管理移除,库字段保留 Contractreturn=json.dumps(contract_return_list, ensure_ascii=False), Closingapplication=json.dumps(closing_application_list, ensure_ascii=False), ChangeRequest="", invoice_status=normalize_amount_value(invoice_status), paymentcollection=normalize_amount_value(paymentcollection), state="审核中" ) from User.utils import create_approval_with_team_logic team_name = get_team_name_from_responsiblefor(project_registration.responsiblefor) approval, approvers_order_json, needs_approval = create_approval_with_team_logic( team_name=team_name, approvers=approvers, title="案件管理信息提交", content=build_case_approval_content(project_registration, filing_time), approval_type="案件管理", user_id=case_id.id, business_record=case_id, today=filing_time ) if approval is None and needs_approval: return Response({ 'status': 'error', 'message': build_missing_approvers_message(team_name, approvers), 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 处理标签关联 if tag_ids: try: # 支持数组或逗号分隔的字符串 if isinstance(tag_ids, str): if ',' in tag_ids: tag_id_list = [int(x.strip()) for x in tag_ids.split(',') if x.strip()] else: tag_id_list = [int(tag_ids)] elif isinstance(tag_ids, list): tag_id_list = [int(x) for x in tag_ids if x] else: tag_id_list = [] # 验证标签是否存在 valid_tags = CaseTag.objects.filter(id__in=tag_id_list, is_deleted=False) case_id.tags.set(valid_tags) except (ValueError, TypeError) as e: # 标签ID格式错误,但不影响案件创建,只记录警告 pass # 创建成功,直接返回 return Response({'message': '创建成功', 'code': 0}, status=status.HTTP_200_OK) # 如果已存在,事务结束,在事务外处理更新逻辑 except ProjectRegistration.DoesNotExist: return Response({'status': 'error', 'message': '该立项登记不存在或已删除,请先完成立项登记', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 如果代码执行到这里,说明案件已存在,需要执行更新逻辑 # 注意:这里不需要再次锁定,因为更新操作不会造成重复创建问题 case = Case.objects.filter(project_id=project_id, is_deleted=False).first() if not case: # 这种情况理论上不应该发生,因为事务内已检查,但为了安全起见 return Response({'status': 'error', 'message': '案件不存在', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 执行更新逻辑(代理合同已从案件管理移除,不再接收或更新) update_fields_list = [] if times: case.times = times update_fields_list.append('times') if Contractreturn: case.Contractreturn = json.dumps(flies(Contractreturn), ensure_ascii=False) update_fields_list.append('Contractreturn') if Closingapplication: case.Closingapplication = json.dumps(flies(Closingapplication), ensure_ascii=False) update_fields_list.append('Closingapplication') if paymentcollection is not None: case.paymentcollection = normalize_amount_value(paymentcollection) update_fields_list.append('paymentcollection') if invoice_status is not None: case.invoice_status = normalize_amount_value(invoice_status) update_fields_list.append('invoice_status') if update_fields_list: case.save(update_fields=update_fields_list) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class CaseAttachmentUpload(APIView): def post(self, request, *args, **kwargs): """ 案件材料上传(合同返还/结案申请) 通过 type 指定上传类型,返回文件URL列表 """ upload_type = request.data.get('type') files = request.FILES.getlist('file') or request.FILES.getlist('files') allowed_types = ["Contractreturn", "Closingapplication"] if upload_type not in allowed_types: return Response({ 'status': 'error', 'message': f'type参数错误,支持类型:{", ".join(allowed_types)}', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) if not files: return Response({'status': 'error', 'message': '请上传文件', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) file_urls = flies(files) return Response({ 'message': '上传成功', 'code': 0, 'data': { 'type': upload_type, 'files': file_urls } }, status=status.HTTP_200_OK) class CaseAttachmentUpdate(APIView): def post(self, request, *args, **kwargs): """ 案件材料更新(合同返还/结案申请) 通过 type 指定更新哪一种材料 """ case_id = request.data.get('case_id') upload_type = request.data.get('type') files = request.FILES.getlist('file') or request.FILES.getlist('files') Contractreturn = request.data.get('Contractreturn') approvers = request.data.get('approvers') personincharge = request.data.get('personincharge') approvers = normalize_approvers_param(approvers, personincharge) if not case_id: return Response({'status': 'error', 'message': '缺少参数case_id', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) allowed_types = ["Contractreturn", "Closingapplication","AgencyContract"] if upload_type not in allowed_types: return Response({ 'status': 'error', 'message': f'type参数错误,支持类型:{", ".join(allowed_types)}', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # if not files: # return Response({'status': 'error', 'message': '请上传文件', 'code': 1}, # status=status.HTTP_400_BAD_REQUEST) try: case = Case.objects.get(id=case_id, is_deleted=False) except Case.DoesNotExist: return Response({'status': 'error', 'message': '案件不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) file_urls = flies(files) update_fields = [] if upload_type == "Contractreturn": case.Contractreturn = Contractreturn update_fields = ['Contractreturn'] elif upload_type == "Closingapplication": # 结案申请需要审批通过后才更新 Case.Closingapplication 字段 # 这里不直接更新,文件URL保存在 Schedule.remark 中,审批通过后再更新到 Case pass elif upload_type == "AgencyContract": case.AgencyContract = json.dumps(file_urls, ensure_ascii=False) update_fields = ['AgencyContract'] if update_fields: case.save(update_fields=update_fields) # 结案申请:加入审批流程(独立于“案件管理”审批,避免覆盖 Case.approvers_order / Case.state) if upload_type == "Closingapplication": # 审批通过后才更新 Case.Closingapplication 字段 try: token = request.META.get('token') submitter = User.objects.get(token=token, is_deleted=False) except Exception: submitter = None # 以 Schedule 作为承载对象,复用统一审批流 try: from datetime import date today = date.today().strftime("%Y-%m-%d") contract_no = case.contract_no or "" title = f"结案申请(合同号:{contract_no or ('案件#' + str(case.id))})" remark = json.dumps({ "case_id": case.id, "contract_no": contract_no, "closing_application_files": file_urls }, ensure_ascii=False) schedule = Schedule.objects.create( title=title, tiems=today, end_time=today, remark=remark, state="未完成", submit=submitter.username if submitter else None ) from User.utils import create_approval_with_team_logic team_name = submitter.team if submitter else None content = f"{submitter.username if submitter else '系统'}提交了结案申请,合同号:{contract_no or ('案件#' + str(case.id))}" approval, approvers_order_json, needs_approval = create_approval_with_team_logic( team_name=team_name, approvers=approvers, title="结案申请审批", content=content, approval_type="结案申请", user_id=schedule.id, business_record=schedule, today=today, applicant=submitter.username if submitter else None # 传入申请人,用于"待查看"流程 ) if approval is None and needs_approval: return Response({ 'status': 'error', 'message': build_missing_approvers_message(team_name, approvers), 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: # 不影响材料更新主流程,但记录错误信息 import logging logger = logging.getLogger(__name__) logger.error(f"结案申请审批创建失败(case_id={case.id}): {str(e)}") return Response({ 'message': '更新成功', 'code': 0, 'data': { 'case_id': case.id, 'type': upload_type, 'files': file_urls } }, status=status.HTTP_200_OK) class caseManagementDetail(APIView): def post(self, request, *args, **kwargs): """ 案件管理版块展示 搜索支持按标签:传 tags(标签名称,模糊匹配)或 tag_ids(标签ID列表/逗号分隔,匹配拥有任一标签的案件),可同时使用。 :param request: page, per_page 必填;可选 times, end_time, type, contract_no, party_name, client_name, tags, tag_ids :return: """ page = request.data.get('page') per_page = request.data.get('per_page') times = request.data.get('times') end_time = request.data.get('end_time') type = request.data.get('type') serialnumber = request.data.get('contract_no') party_name = request.data.get('party_name') client_name = request.data.get('client_name') tags = request.data.get('tags') # 标签:传字符串为名称模糊匹配,传数组 [2,3] 为标签ID列表 tag_ids = request.data.get('tag_ids') # 标签ID列表(与 tags 传 ID 时等效) if not all([page, per_page]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) Q_obj = Q() if times and end_time: Q_obj &= Q(times__gte=times) & Q(times__lte=end_time) if type: por_id = ProjectRegistration.objects.filter(type=type, is_deleted=False).values_list('id', flat=True) Q_obj &= Q(project_id__in=por_id) if serialnumber: Q_obj &= Q(contract_no__icontains=serialnumber) if party_name: Q_obj &= Q(party_name__icontains=party_name) if client_name: Q_obj &= Q(client_name__icontains=client_name) # 按标签搜索:tags 可为字符串(名称模糊)、数组(标签ID)、或 form 传来的字符串 "[2]";tag_ids 为标签ID列表;满足任一标签即匹配 tag_id_list = [] if tags is not None and tags != '': if isinstance(tags, list): try: tag_id_list.extend([int(x) for x in tags if x is not None and str(x).strip() != '']) except (ValueError, TypeError): pass elif isinstance(tags, str): tags_stripped = tags.strip() # 前端 form-data 可能把 [2] 或 [2,3] 当成字符串传,先尝试按 JSON 数组解析 if tags_stripped.startswith('[') and tags_stripped.endswith(']'): try: parsed = json.loads(tags_stripped) if isinstance(parsed, list): tag_id_list.extend([int(x) for x in parsed if x is not None and str(x).strip() != '']) except (ValueError, TypeError, json.JSONDecodeError): pass if not tag_id_list: tag_id_list.extend( CaseTag.objects.filter(name__icontains=tags, is_deleted=False).values_list('id', flat=True) ) if tag_ids: try: if isinstance(tag_ids, str): tag_id_list.extend([int(x.strip()) for x in tag_ids.split(',') if x.strip()]) elif isinstance(tag_ids, list): tag_id_list.extend([int(x) for x in tag_ids if x is not None and str(x).strip() != '']) except (ValueError, TypeError): pass tag_id_list = list(set(tag_id_list)) if tag_id_list: Q_obj &= Q(tags__in=tag_id_list) pre = Case.objects.filter(Q_obj, is_deleted=False).order_by('-id') if tag_id_list: pre = pre.distinct() total = len(pre) paginator = Paginator(pre, per_page) try: user_agents_page = paginator.page(page) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) # 导入财务模型,用于统计开票金额和收入确认金额 from finance.models import Invoice as FinanceInvoice, Income # 收集所有合同号,用于批量查询财务数据 case_list = list(user_agents_page.object_list) contract_nos = [] case_contract_map = {} # 案件ID -> 合同号的映射 for info in case_list: pro = None if info.project_id: try: pro = ProjectRegistration.objects.get(id=info.project_id, is_deleted=False) except ProjectRegistration.DoesNotExist: pro = None contract_no = info.contract_no or (pro.ContractNo if pro else None) if contract_no: contract_nos.append(contract_no) case_contract_map[info.id] = contract_no # 批量查询开票金额(按合同号统计) invoice_amounts = {} # 合同号 -> 总开票金额 if contract_nos: # 查询所有未删除的开票记录(一次性查询,避免N+1问题) invoices = FinanceInvoice.objects.filter( ContractNo__in=contract_nos, is_deleted=False ).values('ContractNo', 'amount') # 计算每个合同号的总开票金额(需要解析金额字符串) for invoice in invoices: contract_no = invoice['ContractNo'] amount_str = normalize_amount_value(invoice['amount']) try: amount = float(amount_str) except (ValueError, TypeError): amount = 0.0 if contract_no in invoice_amounts: invoice_amounts[contract_no] = str(float(invoice_amounts[contract_no]) + amount) else: invoice_amounts[contract_no] = str(amount) # 批量查询收入确认金额(按合同号统计,只统计审核通过的) income_amounts = {} # 合同号 -> 总收入确认金额 if contract_nos: # 查询所有未删除且审核通过的收入确认记录(一次性查询,避免N+1问题) # 只统计 state="已通过" 的记录 incomes = Income.objects.filter( ContractNo__in=contract_nos, is_deleted=False, state="已通过" # 只统计审核通过的收入确认 ).values('ContractNo', 'amount') # 计算每个合同号的总收入确认金额 for income in incomes: contract_no = income['ContractNo'] amount_str = normalize_amount_value(income['amount']) try: amount = float(amount_str) except (ValueError, TypeError): amount = 0.0 if contract_no in income_amounts: income_amounts[contract_no] = str(float(income_amounts[contract_no]) + amount) else: income_amounts[contract_no] = str(amount) data = [] for info in case_list: pro = None if info.project_id: try: pro = ProjectRegistration.objects.get(id=info.project_id, is_deleted=False) except ProjectRegistration.DoesNotExist: pro = None contract_no = info.contract_no or (pro.ContractNo if pro else None) project_type = info.project_type or (pro.type if pro else None) client_name = info.client_name or (pro.client_info if pro else None) party_name = info.party_name or (pro.party_info if pro else None) project_description = info.project_description or (pro.description if pro else None) responsiblefor_raw = info.responsiblefor or (pro.responsiblefor if pro else None) charge = info.charge or (pro.charge if pro else None) # 解析负责人信息(JSON字符串转字典) try: responsiblefor_dict = json.loads(responsiblefor_raw) if responsiblefor_raw else {} except (json.JSONDecodeError, TypeError): responsiblefor_dict = responsiblefor_raw if responsiblefor_raw else {} # 解析结案申请(JSON字符串转列表);代理合同已从案件管理移除,不再返回 try: closing_application_list = json.loads(info.Closingapplication) if info.Closingapplication else [] except (json.JSONDecodeError, TypeError): closing_application_list = [] if not closing_application_list: closing_application_list = "" contractreturn_str = info.Contractreturn or "" if contractreturn_str.strip() in ["[]", ""]: contractreturn_str = "" closing_application_str = info.Closingapplication or "" if closing_application_str.strip() in ["[]", ""]: closing_application_str = "" # 从财务数据动态获取开票金额和收入确认金额 invoice_status_value = "0" paymentcollection_value = "0" if contract_no: # 从财务数据获取开票金额 invoice_status_value = invoice_amounts.get(contract_no, "0") # 从财务数据获取收入确认金额 paymentcollection_value = income_amounts.get(contract_no, "0") else: # 如果没有合同号,使用Case表中的旧数据(向后兼容) invoice_status_value = normalize_amount_value(info.invoice_status) paymentcollection_value = normalize_amount_value(info.paymentcollection) # 获取案件标签 tags = info.tags.filter(is_deleted=False) tag_list = [] for tag in tags: tag_list.append({ 'id': tag.id, 'name': tag.name, 'color': tag.color }) # 如果没有标签,返回空字符串而不是空数组 tags_value = tag_list if tag_list else "" tag_ids_value = [tag['id'] for tag in tag_list] if tag_list else "" # 如果没有负责人信息,返回空字符串而不是空字典 # 检查 responsiblefor_dict 是否为有效的非空字典 if responsiblefor_dict and isinstance(responsiblefor_dict, dict) and len(responsiblefor_dict) > 0: responsiblefor_value = responsiblefor_dict else: responsiblefor_value = "" data.append({ "id": info.id, "ContractNo": contract_no or "", # 合同编号 "type": project_type or "", # 项目类型 "client_info": client_name or "", # 客户名称(委托人身份信息) "party_info": party_name or "", # 相对方名称 "description": project_description or "", # 项目简述(没有数据时返回空字符串) "responsiblefor": responsiblefor_value, # 负责人信息(字典格式,没有数据时返回空字符串) "charge": charge or "", # 收费情况(没有数据时返回空字符串) 'times': info.times or "", # 立案时间(没有数据时返回空字符串) "Contractreturn": contractreturn_str, # 合同返还(没有数据时返回空字符串) "Closingapplication": closing_application_str, # 结案申请(JSON字符串,没有文件时返回空字符串) "ClosingapplicationUrls": closing_application_list if closing_application_list else "", # 结案申请(URL列表,没有文件时返回空字符串) "ChangeRequest": info.ChangeRequest or "", # 变更申请(兼容旧字段,没有数据时返回空字符串) "ChangeItem": info.ChangeItem or "", # 变更事项(没有数据时返回空字符串) "ChangeReason": info.ChangeReason or "", # 变更原因(没有数据时返回空字符串) "ChangeAgreement": info.ChangeAgreement or "", # 变更协议(没有数据时返回空字符串) "invoice_status": invoice_status_value or "0", # 已开票(从财务数据动态计算,没有数据时返回"0") "paymentcollection": paymentcollection_value or "0", # 已收款(从财务数据动态计算,没有数据时返回"0") "state": info.state or "", # 状态(没有数据时返回空字符串) "project_id": info.project_id if info.project_id is not None else "", # 项目ID(没有数据时返回空字符串) "tags": tags_value, # 标签列表(没有数据时返回空字符串) "tag_ids": tag_ids_value, # 标签ID列表(没有数据时返回空字符串) "AgencyContract":info.AgencyContract }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class EditCase(APIView): def post(self, request, *args, **kwargs): """ 编辑案件管理 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') times = request.data.get('times') Contractreturn = request.FILES.getlist('Contractreturn') Closingapplication = request.FILES.getlist('Closingapplication') invoice_status = request.data.get('invoice_status') # 已开票 paymentcollection = request.data.get('paymentcollection') tag_ids = request.data.get('tag_ids') # 标签ID列表(数组或逗号分隔的字符串) approvers = request.data.get('approvers') personincharge = request.data.get('personincharge') approvers = normalize_approvers_param(approvers, personincharge) # 检查不可修改的参数(前端不会传入,但作为安全措施进行检查) forbidden_params = [] if 'project_id' in request.data: forbidden_params.append('project_id') if forbidden_params: return Response({ 'status': 'error', 'message': f'以下参数不允许修改: {", ".join(forbidden_params)}', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) if not id: return Response({'status': 'error', 'message': '缺少参数id', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: case = Case.objects.get(id=id, is_deleted=False) except Case.DoesNotExist: return Response({'status': 'error', 'message': '案件管理不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) update_fields_list = [] if times: case.times = times update_fields_list.append('times') if Contractreturn: case.Contractreturn = json.dumps(flies(Contractreturn), ensure_ascii=False) update_fields_list.append('Contractreturn') if Closingapplication: case.Closingapplication = json.dumps(flies(Closingapplication), ensure_ascii=False) update_fields_list.append('Closingapplication') if paymentcollection is not None: case.paymentcollection = normalize_amount_value(paymentcollection) update_fields_list.append('paymentcollection') if invoice_status is not None: case.invoice_status = normalize_amount_value(invoice_status) update_fields_list.append('invoice_status') if update_fields_list: case.save(update_fields=update_fields_list) # 处理标签更新 if tag_ids is not None: try: # 支持数组或逗号分隔的字符串 if isinstance(tag_ids, str): if tag_ids.strip() == '': tag_id_list = [] elif ',' in tag_ids: tag_id_list = [int(x.strip()) for x in tag_ids.split(',') if x.strip()] else: tag_id_list = [int(tag_ids)] elif isinstance(tag_ids, list): tag_id_list = [int(x) for x in tag_ids if x] else: tag_id_list = [] # 验证标签是否存在并更新关联 valid_tags = CaseTag.objects.filter(id__in=tag_id_list, is_deleted=False) case.tags.set(valid_tags) except (ValueError, TypeError) as e: # 标签ID格式错误,但不影响案件更新 pass return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class DeleteCase(APIView): def post(self, request, *args, **kwargs): """ 删除案件管理 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: case = Case.objects.get(id=id, is_deleted=False) except Case.DoesNotExist: return Response({'status': 'error', 'message': '案件管理不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 记录操作前的数据 old_data = { 'id': case.id, 'times': case.times, 'state': case.state } # 软删除:更新 is_deleted 字段 case.is_deleted = True case.save() # 记录操作日志 log_operation( request=request, operation_type='DELETE', module='Business', action='删除案件管理', target_type='Case', target_id=case.id, target_name=f'案件-{case.id}', old_data=old_data, remark=f'删除案件管理:ID {case.id}' ) return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) class Uploadinvoice(APIView): def post(self, request, *args, **kwargs): """案件管理发票 - 关联案件""" case_id = request.data.get('case_id') # 案件ID amount = request.data.get('amount') file = request.FILES.getlist('file') if not case_id: return Response({'status': 'error', 'message': '缺少参数case_id', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 检查案件是否存在 try: case = Case.objects.get(id=case_id, is_deleted=False) except Case.DoesNotExist: return Response({'status': 'error', 'message': '案件不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) Invoice.objects.create( case_id=case_id, amount=amount, file=json.dumps(flies(file)), ) return Response({'message': '上传成功', 'code': 0}, status=status.HTTP_200_OK) class InvoiceDetail(APIView): def post(self, request, *args, **kwargs): """ 发票展示 - 关联案件 :param request: :param args: :param kwargs: :return: """ case_id = request.data.get('case_id') # 案件ID if not case_id: return Response({'status': 'error', 'message': '缺少参数case_id', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) invoices = Invoice.objects.filter(case_id=case_id, is_deleted=False) data = [] for invoice in invoices: data.append({ "id": invoice.id, "amount": invoice.amount, "file": invoice.file, }) return Response({'message': '展示成功', 'data': data, 'code': 0}, status=status.HTTP_200_OK) class Log(APIView): def post(self, request, *args, **kwargs): """ 案件日志 - 关联案件 """ token = request.META.get('token') # 明确拒绝 user_id 参数(此接口使用案件ID,不再使用预立案ID) if 'user_id' in request.data: return Response({'status': 'error', 'message': '此接口不需要 user_id 参数,请使用 case_id(案件ID)', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) case_id = request.data.get('case_id') # 案件ID content = request.data.get('content') file = request.FILES.getlist('file') if not all([case_id, content]): return Response({'status': 'error', 'message': '缺少参数case_id或content', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 检查案件是否存在 try: case = Case.objects.get(id=case_id, is_deleted=False) except Case.DoesNotExist: return Response({'status': 'error', 'message': '案件不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) user = User.objects.get(token=token, is_deleted=False) now = datetime.now() date_str = now.strftime('%Y-%m-%d') Caselog.objects.create( case_id=case_id, content=content, file=json.dumps(flies(file)), times=date_str, username=user.username, ) return Response({'message': '添加成功', 'code': 0}, status=status.HTTP_200_OK) class LogDetail(APIView): def post(self, request, *args, **kwargs): """ 案件日志展示(按案件ID查询) 必填参数:case_id(案件ID)、page、per_page """ page = request.data.get('page') per_page = request.data.get('per_page') case_id = request.data.get('case_id') # 案件ID(必填) if not all([page, per_page]): return Response({'status': 'error', 'message': '缺少参数page或per_page', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) if not case_id: return Response({'status': 'error', 'message': '缺少参数case_id(案件ID)', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 根据案件ID过滤日志 pre = Caselog.objects.filter(is_deleted=False, case_id=case_id) total = pre.count() paginator = Paginator(pre, per_page) try: user_agents_page = paginator.page(page) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) data = [] for info in user_agents_page.object_list: data.append({ "id": info.id, "case_id": info.case_id if info.case else None, # 案件ID "content": info.content, # 日志内容 "times": info.times, # 时间 "username": info.username, # 提交人(保留兼容性) "recorder": info.username, # 记录人 "file": info.file, # 文件 }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class accumulate(APIView): def post(self, request, *args, **kwargs): """ 累加 - 关联案件 :param request: :param args: :param kwargs: :return: """ case_id = request.data.get('case_id') # 案件ID paymentcollection = request.data.get('paymentcollection') if not case_id: return Response({'status': 'error', 'message': '缺少参数case_id', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: case = Case.objects.get(id=case_id, is_deleted=False) except Case.DoesNotExist: return Response({'status': 'error', 'message': '案件不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) current_amount_str = normalize_amount_value(case.paymentcollection) add_amount_str = normalize_amount_value(paymentcollection) try: current_amount = Decimal(current_amount_str) except InvalidOperation: current_amount = Decimal("0") try: add_amount = Decimal(add_amount_str) except InvalidOperation: add_amount = Decimal("0") case.paymentcollection = str(current_amount + add_amount) case.save(update_fields=['paymentcollection']) return Response({'message': '成功', 'code': 0}, status=status.HTTP_200_OK) class preFilingLinkedCases(APIView): def post(self, request, *args, **kwargs): """ 立项登记列表接口(用于新增案件) 权限:管理员可见全部;承办人仅可见自己承办的立项。 返回可以用于新增案件的立项登记列表,排除已有案件的立项登记 :param request: :param args: :param kwargs: :return: """ pre_qs, err = _get_project_registration_queryset_by_permission(request) if err is not None: return err case_project_ids = Case.objects.filter(is_deleted=False).values_list('project_id', flat=True) projects = pre_qs.exclude(id__in=case_project_ids) data = [] for project in projects: data.append({ "id": project.id, "ContractNo": project.ContractNo, "type": project.type, "client_info": project.client_info, "party_info": project.party_info, "description": project.description, }) return Response({'message': '展示成功', 'data': data, 'code': 0}, status=status.HTTP_200_OK) class ProjectDropdownList(APIView): """获取成功立项的下拉列表接口""" def post(self, request, *args, **kwargs): """ 获取已通过审核的立项登记列表(用于下拉选择) 权限:管理员可见全部;承办人仅可见自己承办的立项。 返回成功立项的数据,格式简洁,适合下拉列表使用 :param request: :param args: :param kwargs: :return: """ # 权限过滤:管理员全部,承办人只看自己承办的 pre_qs, err = _get_project_registration_queryset_by_permission(request) if err is not None: return err # 已通过审核的立项 projects = pre_qs.filter(state="已通过").order_by('-id') # 可选参数:是否排除已有案件的立项 exclude_has_case = request.data.get('exclude_has_case', False) if exclude_has_case: case_project_ids = Case.objects.filter(is_deleted=False).values_list('project_id', flat=True) projects = projects.exclude(id__in=case_project_ids) data = [] import json for project in projects: # 解析承办人信息(原 responsible_person 即承办人) responsible_person = "" try: responsiblefor_dict = json.loads(project.responsiblefor) if project.responsiblefor else {} if isinstance(responsiblefor_dict, dict): responsible_person = responsiblefor_dict.get('responsible_person', '') except (json.JSONDecodeError, TypeError): pass display_text = f"{project.ContractNo}" if project.type: display_text += f" - {project.type}" if responsible_person: display_text += f" ({responsible_person})" data.append({ "id": project.id, "value": project.id, "label": display_text, "ContractNo": project.ContractNo, "type": project.type, "client_info": project.client_info, "party_info": project.party_info, "description": project.description, "responsible_person": responsible_person, # 承办人(保留字段名兼容) "times": project.times, }) return Response({ 'message': '查询成功', 'data': data, 'total': len(data), 'code': 0 }, status=status.HTTP_200_OK) class CaseDropdownList(APIView): """获取案件的下拉列表接口""" def post(self, request, *args, **kwargs): """ 获取案件列表(用于下拉选择) 返回案件数据,格式简洁,适合下拉列表使用。 可选按标签筛选:传 tags(字符串为名称模糊,数组如 [2,3] 为标签ID)或 tag_ids,匹配拥有任一标签的案件。 :param request: 可选 state, tags, tag_ids :return: """ state_filter = request.data.get('state', None) tags = request.data.get('tags') # 字符串=名称模糊,数组=标签ID如 [2,3] tag_ids = request.data.get('tag_ids') Q_obj = Q(is_deleted=False) if state_filter: Q_obj &= Q(state=state_filter) tag_id_list = [] if tags is not None and tags != '': if isinstance(tags, list): try: tag_id_list.extend([int(x) for x in tags if x is not None and str(x).strip() != '']) except (ValueError, TypeError): pass elif isinstance(tags, str): tags_stripped = tags.strip() if tags_stripped.startswith('[') and tags_stripped.endswith(']'): try: parsed = json.loads(tags_stripped) if isinstance(parsed, list): tag_id_list.extend([int(x) for x in parsed if x is not None and str(x).strip() != '']) except (ValueError, TypeError, json.JSONDecodeError): pass if not tag_id_list: tag_id_list.extend( CaseTag.objects.filter(name__icontains=tags, is_deleted=False).values_list('id', flat=True) ) if tag_ids: try: if isinstance(tag_ids, str): tag_id_list.extend([int(x.strip()) for x in tag_ids.split(',') if x.strip()]) elif isinstance(tag_ids, list): tag_id_list.extend([int(x) for x in tag_ids if x is not None and str(x).strip() != '']) except (ValueError, TypeError): pass tag_id_list = list(set(tag_id_list)) if tag_id_list: Q_obj &= Q(tags__in=tag_id_list) cases = Case.objects.filter(Q_obj).order_by('-id') if tag_id_list: cases = cases.distinct() data = [] for case in cases: # 解析负责人信息,获取负责人姓名 responsible_person = "" try: responsiblefor_dict = json.loads(case.responsiblefor) if case.responsiblefor else {} if isinstance(responsiblefor_dict, dict): responsible_person = responsiblefor_dict.get('responsible_person', '') except (json.JSONDecodeError, TypeError): pass # 构建显示文本(用于下拉列表显示) display_text = case.contract_no or f"案件#{case.id}" if case.project_type: display_text += f" - {case.project_type}" if case.client_name: display_text += f" ({case.client_name})" elif responsible_person: display_text += f" ({responsible_person})" data.append({ "id": case.id, "value": case.id, # 下拉列表的值 "label": display_text, # 下拉列表的显示文本 "contract_no": case.contract_no or "", # 合同编号 "project_type": case.project_type or "", # 项目类型 "client_name": case.client_name or "", # 客户名称 "party_name": case.party_name or "", # 相对方名称 "project_description": case.project_description or "", # 项目简述 "responsible_person": responsible_person, # 负责人 "times": case.times, # 立案时间 "state": case.state, # 状态 }) return Response({ 'message': '查询成功', 'data': data, 'total': len(data), 'code': 0 }, status=status.HTTP_200_OK) class Application(APIView): def post(self, request, *args, **kwargs): """ 申请用印 :param request: :param args: :param kwargs: :return: """ token = request.META.get('token') Printingpurpose = request.data.get('Printingpurpose') # 案件编号(前端可不传),后端自动同步为“合同号” # 兼容:仍支持直接传 CaseNumber(即合同号) CaseNumber = request.data.get('CaseNumber') case_id = request.data.get('case_id') project_id = request.data.get('project_id') Reason = request.data.get('Reason') seal_number = request.data.get('seal_number') seal_type = request.data.get('seal_type') file = request.FILES.getlist('file') approvers = request.data.get('approvers') personincharge = request.data.get('personincharge') approvers = normalize_approvers_param(approvers, personincharge) if not all([Printingpurpose, Reason, seal_number, seal_type, file]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) now = datetime.now() date_str = now.strftime('%Y-%m-%d') user = User.objects.get(token=token, is_deleted=False) # 自动查询合同号写入 CaseNumber(案件编号) # 优先级:case_id > project_id > CaseNumber(兼容旧传参) contract_no = None if case_id: try: case = Case.objects.select_related('project').get(id=case_id, is_deleted=False) contract_no = case.contract_no or (case.project.ContractNo if case.project else None) except Case.DoesNotExist: return Response({'status': 'error', 'message': '案件不存在或已被删除', 'code': 1}, status=status.HTTP_404_NOT_FOUND) elif project_id: try: project = ProjectRegistration.objects.get(id=project_id, is_deleted=False) contract_no = project.ContractNo except ProjectRegistration.DoesNotExist: return Response({'status': 'error', 'message': '立项登记不存在或已被删除', 'code': 1}, status=status.HTTP_404_NOT_FOUND) else: contract_no = CaseNumber if not contract_no: return Response({ 'status': 'error', 'message': '缺少案件信息:请传 case_id 或 project_id 以自动获取合同号,或直接传 CaseNumber(合同号)', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) sea = SealApplication.objects.create( Printingpurpose=Printingpurpose, CaseNumber=contract_no, Reason=Reason, seal_number=seal_number, seal_type=seal_type, file=json.dumps(flies(file)), times=date_str, state="审核中", username=user.username, ) team_name = user.team from User.utils import create_approval_with_team_logic approval, approvers_order_json, needs_approval = create_approval_with_team_logic( team_name=team_name, approvers=approvers, title=user.username + "申请用印", content=user.username + "在" + date_str + "申请用印,合同编号:" + str(contract_no) + ",用印事由:" + Reason + ",盖章份数:" + seal_number + "盖着类型:" + seal_type, approval_type="申请用印", user_id=sea.id, business_record=sea, today=date_str ) if approval is None and needs_approval: return Response({ 'status': 'error', 'message': build_missing_approvers_message(team_name, approvers), 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) return Response({'message': '添加成功', 'code': 0}, status=status.HTTP_200_OK) class ApplicationDetail(APIView): def post(self, request, *args, **kwargs): """ 申请用印展示 :param request: :param args: :param kwargs: :return: """ page = request.data.get('page') per_page = request.data.get('per_page') times = request.data.get('times') end_time = request.data.get('end_time') seal_type = request.data.get('seal_type') CaseNumber = request.data.get('CaseNumber') if not all([page, per_page]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) Q_obj = Q() if times and end_time: Q_obj &= Q(times__gte=times) & Q(times__lte=end_time) if seal_type: Q_obj &= Q(seal_type=seal_type) if CaseNumber: Q_obj &= Q(CaseNumber__icontains=CaseNumber) seas = SealApplication.objects.filter(Q_obj, is_deleted=False).order_by('-id') total = len(seas) paginator = Paginator(seas, per_page) try: user_agents_page = paginator.page(page) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) data = [] for info in user_agents_page.object_list: data.append({ "id": info.id, "Printingpurpose": info.Printingpurpose, "CaseNumber": info.CaseNumber, "Reason": info.Reason, "seal_number": info.seal_number, "seal_type": info.seal_type, "file": info.file, 'times': info.times, "state": info.state, "username": info.username, }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class EditApplication(APIView): def post(self, request, *args, **kwargs): """ 编辑申请用印 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') Printingpurpose = request.data.get('Printingpurpose') # 案件编号(合同号):支持前端直接传,也支持通过 case_id/project_id 自动同步 CaseNumber = request.data.get('CaseNumber') case_id = request.data.get('case_id') project_id = request.data.get('project_id') Reason = request.data.get('Reason') seal_number = request.data.get('seal_number') seal_type = request.data.get('seal_type') file = request.FILES.getlist('file') approvers = request.data.get('approvers') personincharge = request.data.get('personincharge') approvers = normalize_approvers_param(approvers, personincharge) if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: app = SealApplication.objects.get(id=id, is_deleted=False) except SealApplication.DoesNotExist: return Response({'status': 'error', 'message': '申请用印不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) if Printingpurpose: app.Printingpurpose = Printingpurpose app.save(update_fields=['Printingpurpose']) # 优先通过 case_id / project_id 自动同步合同号 contract_no = None if case_id: try: case = Case.objects.select_related('project').get(id=case_id, is_deleted=False) contract_no = case.contract_no or (case.project.ContractNo if case.project else None) except Case.DoesNotExist: return Response({'status': 'error', 'message': '案件不存在或已被删除', 'code': 1}, status=status.HTTP_404_NOT_FOUND) elif project_id: try: project = ProjectRegistration.objects.get(id=project_id, is_deleted=False) contract_no = project.ContractNo except ProjectRegistration.DoesNotExist: return Response({'status': 'error', 'message': '立项登记不存在或已被删除', 'code': 1}, status=status.HTTP_404_NOT_FOUND) elif CaseNumber: contract_no = CaseNumber if contract_no: app.CaseNumber = contract_no app.save(update_fields=['CaseNumber']) if Reason: app.Reason = Reason app.save(update_fields=['Reason']) if seal_number: app.seal_number = seal_number app.save(update_fields=['seal_number']) if seal_type: app.seal_type = seal_type app.save(update_fields=['seal_type']) if file: app.file = json.dumps(flies(file)) app.save(update_fields=['file']) if approvers: now = datetime.now() date_str = now.strftime('%Y-%m-%d') from User.utils import create_approval_with_team_logic team_name = get_user_team_name(app.username) approval, approvers_order_json, needs_approval = create_approval_with_team_logic( team_name=team_name, approvers=approvers, title=app.username + "申请用印重新编辑", content=app.username + "在" + date_str + "申请用印,合同编号:" + str(contract_no or app.CaseNumber) + ",用印事由:" + ( Reason or app.Reason) + ",盖章份数:" + (seal_number or app.seal_number) + "盖着类型:" + ( seal_type or app.seal_type), approval_type="申请用印", user_id=app.id, business_record=app, today=date_str ) if approval is None and needs_approval: return Response({ 'status': 'error', 'message': build_missing_approvers_message(team_name, approvers), 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class DeleteApplication(APIView): def post(self, request, *args, **kwargs): """ 删除申请用印 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: app = SealApplication.objects.get(id=id, is_deleted=False) except SealApplication.DoesNotExist: return Response({'status': 'error', 'message': '申请用印不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 软删除:更新 is_deleted 字段 app.is_deleted = True app.save() return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) class WarehousingRegistration(APIView): def post(self, request, *args, **kwargs): """ 入库登记 :param request: :param args: :param kwargs: :return: """ unit = request.data.get('unit') mark = request.data.get('mark') lawyer = request.data.get('lawyer') deadline = request.data.get('deadline') contract = request.FILES.getlist('contract') if not all([unit, mark, lawyer, deadline, contract]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) now = datetime.now() date_str = now.strftime('%Y-%m-%d') Warehousing.objects.create( unit=unit, mark=mark, lawyer=lawyer, deadline=deadline, contract=json.dumps(flies(contract)), times=date_str, ) return Response({'message': '添加成功', 'code': 0}, status=status.HTTP_200_OK) class WarehousingDetail(APIView): def post(self, request, *args, **kwargs): """ 入库登记展示 :param request: :param args: :param kwargs: :return: """ page = request.data.get('page') per_page = request.data.get('per_page') times = request.data.get('times') end_time = request.data.get('end_time') unit = request.data.get('unit') if not all([page, per_page]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) Q_obj = Q() if times and end_time: Q_obj &= Q(times__gte=times) & Q(times__lte=end_time) if unit: Q_obj &= Q(unit__icontains=unit) seas = Warehousing.objects.filter(Q_obj, is_deleted=False).order_by('-id') total = len(seas) paginator = Paginator(seas, per_page) try: user_agents_page = paginator.page(page) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) data = [] for info in user_agents_page.object_list: data.append({ "id": info.id, "unit": info.unit, "mark": info.mark, "lawyer": info.lawyer, "deadline": info.deadline, "contract": info.contract, "times": info.times, }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class EditWarehousing(APIView): def post(self, request, *args, **kwargs): """ 编辑入库登记 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') unit = request.data.get('unit') mark = request.data.get('mark') lawyer = request.data.get('lawyer') deadline = request.data.get('deadline') contract = request.FILES.getlist('contract') if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: ware = Warehousing.objects.get(id=id, is_deleted=False) except Warehousing.DoesNotExist: return Response({'status': 'error', 'message': '入库登记不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) if unit: ware.unit = unit ware.save(update_fields=['unit']) if mark: ware.mark = mark ware.save(update_fields=['mark']) if lawyer: ware.lawyer = lawyer ware.save(update_fields=['lawyer']) if deadline: ware.deadline = deadline ware.save(update_fields=['deadline']) if contract: ware.contract = json.dumps(flies(contract)) ware.save(update_fields=['contract']) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class DeleteWarehousing(APIView): def post(self, request, *args, **kwargs): """ 删除入库登记 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: ware = Warehousing.objects.get(id=id, is_deleted=False) except Warehousing.DoesNotExist: return Response({'status': 'error', 'message': '入库登记不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 软删除:更新 is_deleted 字段 ware.is_deleted = True ware.save() return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) class PlatformRegistration(APIView): def post(self, request, *args, **kwargs): """ 注册平台登记 :param request: :param args: :param kwargs: :return: """ platform = request.data.get('platform') number = request.data.get('number') password = request.data.get('password') username = request.data.get('username') if not all([platform, number, password, username]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) RegisterPlatform.objects.create( platform=platform, number=number, password=password, username=username, ) return Response({'message': '添加成功', 'code': 0}, status=status.HTTP_200_OK) class PlatformDetail(APIView): def post(self, request, *args, **kwargs): """ 注册平台登记展示 :param request: :param args: :param kwargs: :return: """ page = request.data.get('page') per_page = request.data.get('per_page') seas = RegisterPlatform.objects.filter(is_deleted=False).order_by('-id') total = len(seas) paginator = Paginator(seas, per_page) try: user_agents_page = paginator.page(page) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) data = [] for info in user_agents_page.object_list: data.append({ "id": info.id, "platform": info.platform, "number": info.number, "password": info.password, "username": info.username, }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class EditPlatformDetail(APIView): def post(self, request, *args, **kwargs): """ 注册平台登记编辑 :param request: :param args: :param kwargs: :return: """ ID = request.data.get('id') platform = request.data.get('platform') number = request.data.get('number') password = request.data.get('password') username = request.data.get('username') if not all([platform, number, password, username, ID]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) reg = RegisterPlatform.objects.get(id=ID, is_deleted=False) reg.platform = platform reg.number = number reg.password = password reg.username = username reg.save(update_fields=['platform', 'number', 'password', 'username']) return Response({'message': '修改成功', 'code': 0}, status=status.HTTP_200_OK) class DeletePlatformDetail(APIView): def post(self, request, *args, **kwargs): """ 删除注册平台登记 :param request: :param args: :param kwargs: :return: """ ID = request.data.get('id') try: platform = RegisterPlatform.objects.get(id=ID, is_deleted=False) # 软删除:更新 is_deleted 字段 platform.is_deleted = True platform.save() return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) except RegisterPlatform.DoesNotExist: return Response({'status': 'error', 'message': '注册平台登记不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) class bulletin(APIView): def post(self, request, *args, **kwargs): """ 公告 :param request: :param args: :param kwargs: :return: """ title = request.data.get('title') content = request.data.get('content') file = request.FILES.getlist('file') state = request.data.get('state') token = request.META.get('token') if not all([title, content]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) now = datetime.now() date_str = now.strftime('%Y-%m-%d') bol = 0 if state == "置顶": bol = 1 user = User.objects.get(token=token, is_deleted=False).username Announcement.objects.create( title=title, content=content, file=json.dumps(flies(file)), username=user, times=date_str, state=bol ) return Response({'message': '添加成功', 'code': 0}, status=status.HTTP_200_OK) class BulletinDetail(APIView): def post(self, request, *args, **kwargs): """ 公告展示 :param request: :param args: :param kwargs: :return: """ page = request.data.get('page') per_page = request.data.get('per_page') seas = Announcement.objects.filter(is_deleted=False).order_by('-state', '-times') total = len(seas) paginator = Paginator(seas, per_page) try: user_agents_page = paginator.page(page) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) data = [] for info in user_agents_page.object_list: # 解析 file 字段:有文件返回数组,无文件返回空字符串 try: file_list = json.loads(info.file) if info.file else [] except (json.JSONDecodeError, TypeError): file_list = [] # 空列表返回空字符串 file_result = file_list if file_list else "" if file_list !="": file_result = json.dumps(file_result) data.append({ "id": info.id, "title": info.title, "content": info.content, "times": info.times, "file": file_result,#file_result, "username": info.username, "state": info.state, }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class EditBulletin(APIView): def post(self, request, *args, **kwargs): """ 编辑公告 :param request: :param args: :param kwargs: :return: """ ID = request.data.get('id') title = request.data.get('title') content = request.data.get('content') file = request.FILES.getlist('file') state = request.data.get('state') token = request.META.get('token') # 从中间件设置的 token 获取 if not all([title, content, ID]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: ann = Announcement.objects.get(id=ID, is_deleted=False) except Announcement.DoesNotExist: return Response({'status': 'error', 'message': '公告不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) now = datetime.now() date_str = now.strftime('%Y-%m-%d') if state: bol = 0 if state == "置顶": bol = 1 ann.state = bol ann.save(update_fields=['state']) # 获取用户信息(如果 token 存在) if token: try: user = User.objects.get(token=token, is_deleted=False).username except User.DoesNotExist: # 如果用户不存在,使用原有用户名 user = ann.username else: # 如果 token 不存在,使用原有用户名 user = ann.username if title: ann.title = title ann.save(update_fields=['title']) if content: ann.content = content ann.save(update_fields=['content']) if file: ann.file = json.dumps(flies(file)) # 使用 flies 函数处理文件 ann.save(update_fields=['file']) ann.username = user ann.times = date_str ann.save(update_fields=['username', 'times']) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class deleteBulletin(APIView): def post(self, request, *args, **kwargs): ID = request.data.get('id') try: announcement = Announcement.objects.get(id=ID, is_deleted=False) # 软删除:更新 is_deleted 字段 announcement.is_deleted = True announcement.save() return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) except Announcement.DoesNotExist: return Response({'status': 'error', 'message': '公告不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) class Lawyersdocuments(APIView): def post(self, request, *args, **kwargs): token = request.META.get('token') file = request.FILES.getlist('file') filename = file[0].name name_without_extension, extension = os.path.splitext(filename) user = User.objects.get(token=token, is_deleted=False).username title = name_without_extension + extension if not all([file]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) now = datetime.now() date_str = now.strftime('%Y-%m-%d') LawyerFlie.objects.create( title=title, remark=user, file=json.dumps(flies(file)), times=date_str, ) return Response({'message': '文件上传成功', 'code': 0}, status=status.HTTP_200_OK) class LawyersdocumentsDetail(APIView): def post(self, request, *args, **kwargs): title = request.data.get('title') obj =Q(is_deleted=False) if title: obj &= Q(title__contains=title) law = LawyerFlie.objects.filter(obj).order_by('-id') total = law.count() data = [] for info in law: data.append({ "id": info.id, "title": info.title, "remark": info.remark, "file": info.file, "times": info.times, }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class LwaDetail(APIView): def post(self, request, *args, **kwargs): id = request.data.get('id') try: lawyer_file = LawyerFlie.objects.get(id=id, is_deleted=False) # 软删除:更新 is_deleted 字段 lawyer_file.is_deleted = True lawyer_file.save() return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) except LawyerFlie.DoesNotExist: return Response({'status': 'error', 'message': '律师文件不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) class EditLawyerFlie(APIView): def post(self, request, *args, **kwargs): """ 编辑律师文件 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') title = request.data.get('title') file = request.FILES.getlist('file') remark = request.data.get('remark') if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: law = LawyerFlie.objects.get(id=id, is_deleted=False) except LawyerFlie.DoesNotExist: return Response({'status': 'error', 'message': '律师文件不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) if title: law.title = title law.save(update_fields=['title']) if remark: law.remark = remark law.save(update_fields=['remark']) if file: law.file = json.dumps(flies(file)) law.save(update_fields=['file']) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class CreateSchedule(APIView): def post(self, request, *args, **kwargs): """ 待办创建 :param request: :param args: :param kwargs: :return: """ title = request.data.get('title') tiems = request.data.get('tiems') end_time = request.data.get('end_time') remark = request.data.get('remark') approvers = request.data.get('approvers') # 审核人列表(团队类型时需要,推荐:用户ID数组如[1,2,3],兼容:用户名数组) personincharge = request.data.get('personincharge') approvers = normalize_approvers_param(approvers, personincharge) if not all([title, tiems, end_time]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 获取当前用户 token = request.META.get('token') try: user = User.objects.get(token=token, is_deleted=False) except User.DoesNotExist: return Response({'status': 'error', 'message': '用户不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) Schedule.objects.create( title=title, tiems=tiems, end_time=end_time, remark=remark, state="未完成", submit=user.username # 记录提交人 ) return Response({'message': '添加成功', 'code': 0}, status=status.HTTP_200_OK) class DeleteSchedule(APIView): def post(self, request, *args, **kwargs): """ 删除日程 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') try: schedule = Schedule.objects.get(id=id, is_deleted=False) # 软删除:更新 is_deleted 字段 schedule.is_deleted = True schedule.save() return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) except Schedule.DoesNotExist: return Response({'status': 'error', 'message': '日程不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) class ScheduleDetail(APIView): def post(self, request, *args, **kwargs): """ 日程展示(仅展示用户主动创建的日程/待办,不包含结案申请等审批承载的 Schedule) :param request: :param args: :param kwargs: :return: """ title = request.data.get('title') obj = Q(is_deleted=False) if title: obj &= Q(title__contains=title) # 排除作为「结案申请」审批承载的 Schedule,结案申请应出现在「审核中」而非「日程中」 closing_application_schedule_ids = Approval.objects.filter( type="结案申请", is_deleted=False ).values_list('user_id', flat=True) closing_application_schedule_ids = [int(sid) for sid in closing_application_schedule_ids if sid and str(sid).isdigit()] if closing_application_schedule_ids: obj &= ~Q(id__in=closing_application_schedule_ids) sches = Schedule.objects.filter(obj).order_by('-id') page = request.data.get('page') per_page = request.data.get('per_page') total = len(sches) paginator = Paginator(sches, per_page) try: user_agents_page = paginator.page(page) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) data = [] for info in user_agents_page.object_list: data.append({ "id": info.id, "title": info.title, "tiems": info.tiems, "end_time": info.end_time, "remark": info.remark, 'state': info.state, }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class EditSchedule(APIView): def post(self, request, *args, **kwargs): """ 编辑日程 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') title = request.data.get('title') tiems = request.data.get('tiems') end_time = request.data.get('end_time') remark = request.data.get('remark') if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: schedule = Schedule.objects.get(id=id, is_deleted=False) except Schedule.DoesNotExist: return Response({'status': 'error', 'message': '日程不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) if title: schedule.title = title schedule.save(update_fields=['title']) if tiems: schedule.tiems = tiems schedule.save(update_fields=['tiems']) if end_time: schedule.end_time = end_time schedule.save(update_fields=['end_time']) if remark: schedule.remark = remark schedule.save(update_fields=['remark']) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class handleSchedule(APIView): def post(self, request, *args, **kwargs): id = request.data.get('id') state = request.data.get('state') if not all([id, state]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: sc = Schedule.objects.get(id=id, is_deleted=False) sc.state = "已完成" sc.save(update_fields=['state']) return Response({'message': '修改成功', 'code': 0}, status=status.HTTP_200_OK) except Schedule.DoesNotExist: return Response({'status': 'error', 'message': '日程不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) class AddRermission(APIView): """ 添加权限 """ def post(self, request, *args, **kwargs): permission_name = request.data.get('permission_name') permission_logo = request.data.get('permission_logo') parent = request.data.get('parent') if not all([permission_name, permission_logo, parent]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) permission.objects.create( permission_name=permission_name, permission_logo=permission_logo, parent=parent, ) return Response({'message': '添加成功', 'code': 0}, status=status.HTTP_200_OK) class DisplayRermission(APIView): def post(self, request, *args, **kwargs): """ 权限列表展示 """ permissions = permission.objects.filter(is_deleted=False) # 先将数据转换为字典格式 data = [] permission_dict = {} for per in permissions: item = { "id": str(per.id), "permission_name": per.permission_name, "permission_logo": per.permission_logo, "parent": str(per.parent), "children": [] } data.append(item) permission_dict[str(per.id)] = item # 构建树形结构 tree = [] for item in data: parent_id = item["parent"] # 如果parent是0或者不存在,说明是根节点 if parent_id == "0" or parent_id not in permission_dict: tree.append(item) else: # 找到父节点并添加到children中 parent_node = permission_dict.get(parent_id) if parent_node: parent_node["children"].append(item) return Response({ 'message': '展示成功', "data": tree, 'code': 0 }, status=status.HTTP_200_OK) class DeleteRermission(APIView): def post(self, request, *args, **kwargs): """ 删除权限 """ ID = request.data.get('id') try: perm = permission.objects.get(id=ID, is_deleted=False) # 记录操作前的数据 old_data = { 'id': perm.id, 'permission_name': perm.permission_name, 'permission_logo': perm.permission_logo } # 软删除:更新 is_deleted 字段 perm.is_deleted = True perm.save() # 记录操作日志 log_operation( request=request, operation_type='DELETE', module='Business', action='删除权限', target_type='Permission', target_id=perm.id, target_name=perm.permission_name, old_data=old_data, remark=f'删除权限:{perm.permission_name}' ) return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) except permission.DoesNotExist: return Response({'status': 'error', 'message': '权限不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) class EditRermission(APIView): def post(self, request, *args, **kwargs): """ 编辑权限 """ id = request.data.get('id') permission_name = request.data.get('permission_name') permission_logo = request.data.get('permission_logo') parent = request.data.get('parent') per = permission.objects.get(id=id) if permission_name: per.permission_name = permission_name per.save(update_fields=['permission_name']) if permission_logo: per.permission_logo = permission_logo per.save(update_fields=['permission_logo']) if parent: per.parent = parent per.save(update_fields=['parent']) return Response({'message': '修改成功', 'code': 0}, status=status.HTTP_200_OK) class addRole(APIView): def post(self, request, *args, **kwargs): RoleName = request.data.get('RoleName') remark = request.data.get('remark') if not all([RoleName]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 检查角色名是否已存在 if role.objects.filter(RoleName=RoleName).exists(): return Response({'status': 'error', 'message': '角色名已存在,不能重复', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) role.objects.create(RoleName=RoleName, remark=remark) return Response({'message': '添加成功', 'code': 0}, status=status.HTTP_200_OK) class DeleteRole(APIView): def post(self, request, *args, **kwargs): id = request.data.get('id') try: r = role.objects.get(id=id, is_deleted=False) # 记录操作前的数据 old_data = { 'id': r.id, 'role_name': r.RoleName, 'permission_id': r.permissionId } # 软删除:更新 is_deleted 字段 r.is_deleted = True r.save() # 记录操作日志 log_operation( request=request, operation_type='DELETE', module='Business', action='删除角色', target_type='Role', target_id=r.id, target_name=r.RoleName, old_data=old_data, remark=f'删除角色:{r.RoleName}' ) return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) except role.DoesNotExist: return Response({'status': 'error', 'message': '角色不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) class EditRole(APIView): def post(self, request, *args, **kwargs): id = request.data.get('id') RoleName = request.data.get('RoleName') remark = request.data.get('remark') if not all([id]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) ro = role.objects.get(id=id) if RoleName: # 检查角色名是否已被其他角色使用(排除当前角色) if role.objects.filter(RoleName=RoleName).exclude(id=id).exists(): return Response({'status': 'error', 'message': '角色名已存在,不能重复', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) ro.RoleName = RoleName ro.save(update_fields=['RoleName']) if remark: ro.remark = remark ro.save(update_fields=['remark']) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class displayRole(APIView): """ 角色列表展示 """ def post(self, request, *args, **kwargs): RoleName = request.data.get('RoleName') obj = Q() if RoleName: obj = Q(RoleName__icontains=RoleName) roles = role.objects.filter(obj) data = [] for ro in roles: data.append({ "id": str(ro.id), "RoleName": ro.RoleName, "remark": ro.remark, }) return Response({'message': '展示成功', 'data': data, 'code': 0}, status=status.HTTP_200_OK) class modifypermissions(APIView): def post(self, request, *args, **kwargs): """ 修改角色权限 """ id = request.data.get('id') permissionId = request.data.get('permissionId') if not all([id, permissionId]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) permissionId = ast.literal_eval(permissionId) ro = role.objects.get(id=id) # Convert list to compact comma-separated string format to save space if isinstance(permissionId, list): ro.permissionId = ','.join(map(str, permissionId)) else: ro.permissionId = str(permissionId) ro.save(update_fields=['permissionId']) return Response({'message': '权限修改成功', 'code': 0}, status=status.HTTP_200_OK) class getRolePermissions(APIView): def post(self, request, *args, **kwargs): """ 查询角色权限 :param request: :param args: :param kwargs: :return: """ role_id = request.data.get('roleId') if not role_id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: ro = role.objects.get(id=role_id) except role.DoesNotExist: return Response({'status': 'error', 'message': '角色不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 解析权限ID列表 try: if ro.permissionId: # Try to parse as comma-separated format first (new format) if ',' in ro.permissionId and not ro.permissionId.strip().startswith('['): permission_ids = [int(x.strip()) for x in ro.permissionId.split(',') if x.strip()] else: # Fall back to ast.literal_eval for backward compatibility permission_ids = ast.literal_eval(ro.permissionId) if not isinstance(permission_ids, list): permission_ids = [] else: permission_ids = [] except (ValueError, SyntaxError): # 如果解析失败,返回空列表 permission_ids = [] return Response({'message': '查询成功', 'data': permission_ids, 'code': 0}, status=status.HTTP_200_OK) class DeleteRegistration(APIView): def post(self, request, *args, **kwargs): """ 删除预立案登记 :param request: :param args: :param kwargs: :return: """ id = request.data.get('id') if not id: return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: pre = PreFiling.objects.get(id=id, is_deleted=False) # 注意:由于预立案和立项登记、投标登记已经拆分,不再需要检查关联 # 预立案现在可以独立删除(如果不再需要的话) # 如果将来需要保留预立案用于其他用途,可以保留此检查逻辑 # 软删除:更新 is_deleted 字段 pre.is_deleted = True pre.save() return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) except PreFiling.DoesNotExist: return Response({'status': 'error', 'message': '预立案登记不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) class TransferCase(APIView): def post(self, request, *args, **kwargs): """ 案件转移接口 将案件的承办人员从离职用户转移到新用户 :param request: :param args: :param kwargs: :return: """ old_undertaker = request.data.get('old_undertaker') # 原承办人员(离职用户) new_undertaker = request.data.get('new_undertaker') # 新承办人员 case_ids = request.data.get('case_ids') # 要转移的案件ID列表(可选,如果不传则转移所有案件) project_ids = request.data.get('project_ids') # 要转移的立项ID列表(可选) if not all([old_undertaker, new_undertaker]): return Response({ 'status': 'error', 'message': '缺少参数:原承办人员和新承办人员不能为空', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 检查原承办人员是否存在(排除软删除用户) try: old_user = User.objects.get(username=old_undertaker, is_deleted=False) except User.DoesNotExist: return Response({ 'status': 'error', 'message': '原承办人员不存在或已被删除', 'code': 1 }, status=status.HTTP_404_NOT_FOUND) # 检查新承办人员是否存在(排除软删除用户) try: new_user = User.objects.get(username=new_undertaker, is_deleted=False) except User.DoesNotExist: return Response({ 'status': 'error', 'message': '新承办人员不存在或已被删除', 'code': 1 }, status=status.HTTP_404_NOT_FOUND) # 查询要转移的预立案 if case_ids and isinstance(case_ids, list): prefiling_cases = PreFiling.objects.filter(Undertaker=old_undertaker, id__in=case_ids) else: prefiling_cases = PreFiling.objects.filter(Undertaker=old_undertaker) prefiling_count = prefiling_cases.count() def _replace_responsible(responsiblefor_value, old_name, new_name): try: data = json.loads(responsiblefor_value) if responsiblefor_value else {} except (json.JSONDecodeError, TypeError): data = responsiblefor_value if responsiblefor_value else {} if not isinstance(data, dict): return None, False changed = False for key, value in list(data.items()): if value == old_name: data[key] = new_name changed = True return data, changed # 查询要转移的正式案件 formal_cases_qs = Case.objects.filter(is_deleted=False, responsiblefor__icontains=old_undertaker) if case_ids and isinstance(case_ids, list): formal_cases_qs = formal_cases_qs.filter(id__in=case_ids) # 查询要转移的立项登记 formal_projects_qs = ProjectRegistration.objects.filter(is_deleted=False, responsiblefor__icontains=old_undertaker) if project_ids and isinstance(project_ids, list): formal_projects_qs = formal_projects_qs.filter(id__in=project_ids) formal_cases = list(formal_cases_qs) formal_projects = list(formal_projects_qs) total_count = prefiling_count + len(formal_cases) + len(formal_projects) if total_count == 0: return Response({ 'status': 'error', 'message': '没有找到需要转移的案件', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 执行转移 transferred_prefilings = [] for case in prefiling_cases: case.Undertaker = new_undertaker case.save(update_fields=['Undertaker']) transferred_prefilings.append({ 'id': case.id, 'times': case.times, 'client_username': case.client_username, 'party_username': case.party_username, 'description': case.description }) transferred_formal_cases = [] for case in formal_cases: updated_data, changed = _replace_responsible(case.responsiblefor, old_undertaker, new_undertaker) if changed: case.responsiblefor = json.dumps(updated_data, ensure_ascii=False) case.save(update_fields=['responsiblefor']) transferred_formal_cases.append({ 'id': case.id, 'project_id': case.project_id, 'contract_no': case.contract_no, 'project_type': case.project_type, 'times': case.times }) transferred_projects = [] for project in formal_projects: updated_data, changed = _replace_responsible(project.responsiblefor, old_undertaker, new_undertaker) if changed: project.responsiblefor = json.dumps(updated_data, ensure_ascii=False) project.save(update_fields=['responsiblefor']) transferred_projects.append({ 'id': project.id, 'contract_no': project.ContractNo, 'type': project.type, 'times': project.times }) return Response({ 'message': f'案件转移成功,共转移{total_count}个案件', 'code': 0, 'data': { 'old_undertaker': old_undertaker, 'new_undertaker': new_undertaker, 'transferred_count': total_count, 'prefiling_count': prefiling_count, 'formal_case_count': len(transferred_formal_cases), 'formal_project_count': len(transferred_projects), 'prefiling_cases': transferred_prefilings, 'formal_cases': transferred_formal_cases, 'formal_projects': transferred_projects } }, status=status.HTTP_200_OK) class CaseChangeRequestCreate(APIView): """创建案件变更申请(需传入审批人)""" def post(self, request, *args, **kwargs): case_id = request.data.get('case_id') # 案件ID(可选) contract_no = request.data.get('contract_no') # 合同编号(必填) change_item = request.data.get('change_item') # 变更事项 change_reason = request.data.get('change_reason') # 变更原因 change_agreement = request.FILES.getlist('change_agreement') or request.FILES.getlist('ChangeAgreement') # 变更协议文件 approvers = request.data.get('approvers') # 审批人(必填):用户ID数组如[1,2,3],或用户名数组["张三","李四"],或逗号分隔字符串 personincharge = request.data.get('personincharge') # 兼容旧接口,未传approvers时使用 approvers = normalize_approvers_param(approvers, personincharge) # 验证必填参数 if not contract_no: return Response({ 'status': 'error', 'message': '缺少参数contract_no(合同编号)', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) if not all([change_item, change_reason, change_agreement]): return Response({ 'status': 'error', 'message': '变更申请需要填写变更事项、变更原因并上传变更协议', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) if not approvers: return Response({ 'status': 'error', 'message': '缺少参数approvers(审批人),请传入审批人列表,如用户ID数组[1,2,3]或用户名数组["张三","李四"]', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 如果提供了case_id,验证案件是否存在(可选) case = None if case_id: try: case = Case.objects.get(id=case_id, is_deleted=False) except Case.DoesNotExist: return Response({ 'status': 'error', 'message': '案件不存在或已删除', 'code': 1 }, status=status.HTTP_404_NOT_FOUND) # 获取申请人 applicant_name = "未知用户" token = request.META.get('token') if token: try: applicant = User.objects.get(token=token, is_deleted=False) applicant_name = applicant.username except User.DoesNotExist: pass # 上传变更协议文件 agreement_urls = flies(change_agreement) if not agreement_urls: return Response({ 'status': 'error', 'message': '变更协议上传失败', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 创建变更申请(审批人由入参 approvers 传入,create_approval_with_team_logic 会解析并回写 approvers_order) today = datetime.now().strftime("%Y-%m-%d") change_request = CaseChangeRequest.objects.create( case=case, # 案件关联(可选) contract_no=contract_no, # 合同编号(必填) change_item=change_item, change_reason=change_reason, change_agreement=json.dumps(agreement_urls, ensure_ascii=False), times=today, applicant=applicant_name, state='审核中', approvers_order=None, # 由 create_approval_with_team_logic 根据 approvers 入参回写 need_approval=True ) # 如果提供了case_id,更新案件的change_request字段 if case: case.change_request = change_request case.save(update_fields=['change_request']) # 构建审批内容(不含审批流程,由 create_approval_with_team_logic 追加) if case: content = build_change_approval_content( applicant_name, case, change_item, change_reason, agreement_urls ) else: content = f"{applicant_name}提交案件变更申请,合同编号:{contract_no},变更事项:{change_item},变更原因:{change_reason},变更协议:{', '.join(agreement_urls)}" from User.utils import create_approval_with_team_logic approval, approvers_order_json, needs_approval = create_approval_with_team_logic( team_name=None, approvers=approvers, title="案件变更申请", content=content, approval_type="案件变更", user_id=str(change_request.id), business_record=change_request, today=today, applicant=applicant_name ) if approval is None and needs_approval: return Response({ 'status': 'error', 'message': '审批人无效或不存在,请检查approvers参数(需为系统中的用户ID或用户名)', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) return Response({ 'message': '变更申请提交成功', 'code': 0, 'data': { 'change_request_id': change_request.id } }, status=status.HTTP_200_OK) class CaseChangeRequestList(APIView): """ 案件变更申请列表查询。 默认仅展示审批通过的数据(审批通过后才进变更申请表格)。 我的申请/全部:传 show_all=true 或 state=审核中,待查看,已通过,未通过 可查全部或按状态筛选。 """ def post(self, request, *args, **kwargs): page = request.data.get('page') per_page = request.data.get('per_page') case_id = request.data.get('case_id') # 可选:按案件ID筛选 state = request.data.get('state') # 可选:按状态筛选;不传且不传 show_all 时默认只返回已通过 approved_only = request.data.get('approved_only') # 可选:为 true 时仅返回已通过 show_all = request.data.get('show_all') # 可选:为 true 时返回全部状态(我的申请用),不传则默认只返回已通过 # 参数验证和默认值处理 try: page = int(page) if page and str(page).lower() != 'undefined' else 1 per_page = int(per_page) if per_page and str(per_page).lower() != 'undefined' else 10 except (ValueError, TypeError): return Response({ 'status': 'error', 'message': '参数page和per_page必须是有效的数字', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) if page < 1: page = 1 if per_page < 1: per_page = 10 Q_obj = Q(is_deleted=False) if case_id: Q_obj &= Q(case_id=case_id) # 变更申请表格:默认只展示审批通过的数据;传 show_all=true 可查全部(我的申请) if show_all in (True, 'true', '1', 1): pass # 不按状态筛选,返回全部 elif approved_only in (True, 'true', '1', 1) or state == '已通过': Q_obj &= Q(state='已通过') elif state: # 支持单状态或逗号分隔多状态 if isinstance(state, str) and ',' in state: states = [s.strip() for s in state.split(',') if s.strip()] if states: Q_obj &= Q(state__in=states) else: Q_obj &= Q(state=state) else: # 默认:仅展示已通过,数据审批通过后才进变更申请表格 Q_obj &= Q(state='已通过') queryset = CaseChangeRequest.objects.filter(Q_obj).order_by('-id') total = queryset.count() paginator = Paginator(queryset, per_page) try: page_obj = paginator.page(page) except PageNotAnInteger: page_obj = paginator.page(1) except EmptyPage: page_obj = paginator.page(paginator.num_pages) data = [] for change_request in page_obj.object_list: try: agreement_list = json.loads(change_request.change_agreement) if change_request.change_agreement else [] except (json.JSONDecodeError, TypeError): agreement_list = [] try: approvers_list = json.loads(change_request.approvers_order) if change_request.approvers_order else [] except (json.JSONDecodeError, TypeError): approvers_list = [] data.append({ 'id': change_request.id, 'contract_no': change_request.contract_no, # 合同编号 'case_id': change_request.case.id if change_request.case else None, 'case_contract_no': change_request.case.contract_no if change_request.case else None, 'case_project_type': change_request.case.project_type if change_request.case else None, 'change_item': change_request.change_item, # 变更事项 'change_reason': change_request.change_reason, # 变更原因 'change_agreement': change_request.change_agreement, # 变更协议(JSON字符串) 'change_agreement_urls': agreement_list, # 解析后的URL列表 'times': change_request.times, 'applicant': change_request.applicant, 'state': change_request.state, 'approvers_order': approvers_list }) return Response({ 'message': '查询成功', 'total': total, 'data': data, 'code': 0 }, status=status.HTTP_200_OK) class CaseChangeRequestDetail(APIView): """案件变更申请详情查询""" def post(self, request, *args, **kwargs): change_request_id = request.data.get('change_request_id') or request.data.get('id') if not change_request_id: return Response({ 'status': 'error', 'message': '缺少参数change_request_id或id', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) try: change_request = CaseChangeRequest.objects.get(id=change_request_id, is_deleted=False) except CaseChangeRequest.DoesNotExist: return Response({ 'status': 'error', 'message': '变更申请不存在或已删除', 'code': 1 }, status=status.HTTP_404_NOT_FOUND) try: agreement_list = json.loads(change_request.change_agreement) if change_request.change_agreement else [] except (json.JSONDecodeError, TypeError): agreement_list = [] try: approvers_list = json.loads(change_request.approvers_order) if change_request.approvers_order else [] except (json.JSONDecodeError, TypeError): approvers_list = [] # 获取案件信息(如果有关联) case_data = None if change_request.case: case = change_request.case case_data = { 'id': case.id, 'contract_no': case.contract_no, 'project_type': case.project_type, 'client_name': case.client_name, 'party_name': case.party_name, 'project_description': case.project_description } data = { 'id': change_request.id, 'contract_no': change_request.contract_no, # 合同编号 'case': case_data, # 案件信息(如果有关联) 'change_item': change_request.change_item, # 变更事项 'change_reason': change_request.change_reason, # 变更原因 'change_agreement': change_request.change_agreement, # 变更协议(JSON字符串) 'change_agreement_urls': agreement_list, # 解析后的URL列表 'times': change_request.times, 'applicant': change_request.applicant, 'state': change_request.state, 'approvers_order': approvers_list } return Response({ 'message': '查询成功', 'data': data, 'code': 0 }, status=status.HTTP_200_OK) def parse_person_info(person_str): """ 解析人员信息字符串,支持多种格式: - "张三(3263452342342342)" - "张三,身份证号:3263452342342342" - "张三,3263452342342342" - "张三" Returns: dict: { 'name': '张三', 'id_number': '3263452342342342', # 可能为空 'original': '张三(3263452342342342)' # 原始字符串 } """ if not person_str: return {'name': '', 'id_number': '', 'original': ''} person_str = str(person_str).strip() result = { 'name': '', 'id_number': '', 'original': person_str } # 匹配格式:张三(3263452342342342)或张三(3263452342342342) match = re.match(r'^([^((]+?)[((]([^))]+)[))]', person_str) if match: result['name'] = match.group(1).strip() result['id_number'] = match.group(2).strip() return result # 匹配格式:张三,身份证号:3263452342342342 或 张三,身份证号:3263452342342342 match = re.match(r'^([^,,]+?)[,,]\s*[身身]份证[号号]?[::]?\s*([^\s,,]+)', person_str) if match: result['name'] = match.group(1).strip() result['id_number'] = match.group(2).strip() return result # 匹配格式:张三,3263452342342342(逗号分隔) match = re.match(r'^([^,,]+?)[,,]\s*(\d{15,18})', person_str) if match: result['name'] = match.group(1).strip() result['id_number'] = match.group(2).strip() return result # 如果没有匹配到特定格式,尝试提取姓名(去除可能的身份证号) # 先尝试提取姓名部分(去除末尾的数字) name_match = re.match(r'^([^,,\d((]+)', person_str) if name_match: result['name'] = name_match.group(1).strip() else: result['name'] = person_str # 尝试提取身份证号(15-18位数字) id_match = re.search(r'(\d{15,18})', person_str) if id_match: result['id_number'] = id_match.group(1) return result def conflict_search(client_info=None, party_info=None, undertaker=None, bidding_unit=None, exclude_prefiling_id=None, exclude_project_id=None, exclude_bid_id=None): """ 利益冲突检索函数 - 比对预立案登记、投标登记、立项登记三张表 Args: client_info: 委托人身份信息(可选) party_info: 相对方身份信息(可选) undertaker: 承办人员(可选) bidding_unit: 招标单位(可选) exclude_prefiling_id: 要排除的预立案ID(可选) exclude_project_id: 要排除的立项ID(可选) exclude_bid_id: 要排除的投标ID(可选) Returns: dict: 包含三个表的冲突记录信息 { 'prefiling_conflicts': [...], # 预立案冲突记录列表 'project_conflicts': [...], # 立项冲突记录列表 'bid_conflicts': [...] # 投标冲突记录列表 } """ result = { 'prefiling_conflicts': [], 'project_conflicts': [], 'bid_conflicts': [] } # 如果没有提供任何查询条件,返回空结果 if not any([client_info, party_info, undertaker, bidding_unit]): return result # 解析委托人信息和相对方信息 client_parsed = parse_person_info(client_info) if client_info else {'name': '', 'id_number': '', 'original': ''} party_parsed = parse_person_info(party_info) if party_info else {'name': '', 'id_number': '', 'original': ''} # 提取姓名/名称(用于匹配) client_name = client_parsed['name'] client_id_number = client_parsed['id_number'] client_original = client_parsed['original'] party_name = party_parsed['name'] party_id_number = party_parsed['id_number'] party_original = party_parsed['original'] # 1. 检索预立案表 prefiling_q = Q(is_deleted=False) if exclude_prefiling_id: prefiling_q &= ~Q(id=exclude_prefiling_id) matched_prefilings = [] prefiling_all = PreFiling.objects.filter(prefiling_q) for pf in prefiling_all: if len(matched_prefilings) >= 50: # 限制返回数量 break is_match = False match_reason = [] # 检查委托人/相对方匹配 if client_info or client_name: pf_client_names = [] try: client_data = json.loads(pf.client_username) if isinstance(pf.client_username, str) else pf.client_username if isinstance(client_data, list): for item in client_data: if isinstance(item, dict) and 'name' in item: pf_client_names.append(item['name'].strip()) elif isinstance(client_data, dict) and 'name' in client_data: pf_client_names.append(client_data['name'].strip()) except (json.JSONDecodeError, TypeError, AttributeError): pf_client_names = [pf.client_username.strip()] if pf.client_username else [] pf_party_names = [] try: party_data = json.loads(pf.party_username) if isinstance(pf.party_username, str) else pf.party_username if isinstance(party_data, list): for item in party_data: if isinstance(item, dict) and 'name' in item: pf_party_names.append(item['name'].strip()) elif isinstance(party_data, dict) and 'name' in party_data: pf_party_names.append(party_data['name'].strip()) except (json.JSONDecodeError, TypeError, AttributeError): pf_party_names = [pf.party_username.strip()] if pf.party_username else [] # 检查委托人匹配(支持姓名和身份证号匹配) if client_info or client_name: for pf_client in pf_client_names: # 匹配姓名 if client_name and pf_client in client_name: is_match = True match_reason.append(f"委托人姓名匹配:{pf_client}") break # 匹配完整信息 if client_original and pf_client in client_original: is_match = True match_reason.append(f"委托人信息匹配:{pf_client}") break # 检查相对方是否匹配委托人 for pf_party in pf_party_names: if client_name and pf_party in client_name: is_match = True match_reason.append(f"相对方与委托人姓名匹配:{pf_party}") break if client_original and pf_party in client_original: is_match = True match_reason.append(f"相对方与委托人信息匹配:{pf_party}") break # 检查相对方匹配(支持姓名和身份证号匹配) if party_info or party_name: for pf_party in pf_party_names: # 匹配姓名 if party_name and pf_party in party_name: is_match = True match_reason.append(f"相对方姓名匹配:{pf_party}") break # 匹配完整信息 if party_original and pf_party in party_original: is_match = True match_reason.append(f"相对方信息匹配:{pf_party}") break # 检查委托人是否匹配相对方 for pf_client in pf_client_names: if party_name and pf_client in party_name: is_match = True match_reason.append(f"委托人与相对方姓名匹配:{pf_client}") break if party_original and pf_client in party_original: is_match = True match_reason.append(f"委托人与相对方信息匹配:{pf_client}") break # 检查身份证号匹配(如果提供了身份证号) if client_id_number: # 检查预立案中的身份证号 try: client_data = json.loads(pf.client_username) if isinstance(pf.client_username, str) else pf.client_username if isinstance(client_data, list): for item in client_data: if isinstance(item, dict): pf_id = item.get('idNumber', '') or item.get('id_number', '') if pf_id and client_id_number in str(pf_id): is_match = True match_reason.append(f"委托人身份证号匹配") break elif isinstance(client_data, dict): pf_id = client_data.get('idNumber', '') or client_data.get('id_number', '') if pf_id and client_id_number in str(pf_id): is_match = True match_reason.append(f"委托人身份证号匹配") except (json.JSONDecodeError, TypeError, AttributeError): pass if party_id_number: # 检查预立案中的身份证号 try: party_data = json.loads(pf.party_username) if isinstance(pf.party_username, str) else pf.party_username if isinstance(party_data, list): for item in party_data: if isinstance(item, dict): pf_id = item.get('idNumber', '') or item.get('id_number', '') if pf_id and party_id_number in str(pf_id): is_match = True match_reason.append(f"相对方身份证号匹配") break elif isinstance(party_data, dict): pf_id = party_data.get('idNumber', '') or party_data.get('id_number', '') if pf_id and party_id_number in str(pf_id): is_match = True match_reason.append(f"相对方身份证号匹配") except (json.JSONDecodeError, TypeError, AttributeError): pass # 检查承办人员匹配 if undertaker and pf.Undertaker: if undertaker in pf.Undertaker or pf.Undertaker in undertaker: is_match = True match_reason.append(f"承办人员匹配:{pf.Undertaker}") if is_match: matched_prefilings.append({ 'id': pf.id, 'times': pf.times, 'client_username': pf.client_username, 'party_username': pf.party_username, 'description': pf.description, 'Undertaker': pf.Undertaker, 'match_reason': ';'.join(match_reason) if match_reason else '匹配' }) result['prefiling_conflicts'] = matched_prefilings # 2. 检索立项表 project_q = Q(is_deleted=False) if exclude_project_id: project_q &= ~Q(id=exclude_project_id) project_records = ProjectRegistration.objects.filter(project_q) project_list = [] for pro in project_records[:50]: # 限制查询数量 is_match = False match_reason = [] # 检查委托人/相对方匹配(支持姓名和身份证号匹配) if client_info or client_name: if pro.client_info: # 匹配姓名 if client_name and client_name in pro.client_info: is_match = True match_reason.append("委托人姓名匹配") # 匹配完整信息 elif client_original and client_original in pro.client_info: is_match = True match_reason.append("委托人信息匹配") # 匹配身份证号 elif client_id_number and client_id_number in pro.client_info: is_match = True match_reason.append("委托人身份证号匹配") if pro.party_info: # 匹配姓名 if client_name and client_name in pro.party_info: is_match = True match_reason.append("相对方与委托人姓名匹配") # 匹配完整信息 elif client_original and client_original in pro.party_info: is_match = True match_reason.append("相对方与委托人信息匹配") # 匹配身份证号 elif client_id_number and client_id_number in pro.party_info: is_match = True match_reason.append("相对方与委托人身份证号匹配") if party_info or party_name: if pro.party_info: # 匹配姓名 if party_name and party_name in pro.party_info: is_match = True match_reason.append("相对方姓名匹配") # 匹配完整信息 elif party_original and party_original in pro.party_info: is_match = True match_reason.append("相对方信息匹配") # 匹配身份证号 elif party_id_number and party_id_number in pro.party_info: is_match = True match_reason.append("相对方身份证号匹配") if pro.client_info: # 匹配姓名 if party_name and party_name in pro.client_info: is_match = True match_reason.append("委托人与相对方姓名匹配") # 匹配完整信息 elif party_original and party_original in pro.client_info: is_match = True match_reason.append("委托人与相对方信息匹配") # 匹配身份证号 elif party_id_number and party_id_number in pro.client_info: is_match = True match_reason.append("委托人与相对方身份证号匹配") # 检查负责人匹配 if undertaker and pro.responsiblefor: try: responsiblefor_data = json.loads(pro.responsiblefor) if isinstance(pro.responsiblefor, str) else pro.responsiblefor if isinstance(responsiblefor_data, dict): responsible_person = responsiblefor_data.get('responsible_person', '') main_lawyer = responsiblefor_data.get('main_lawyer', '') assistant_lawyer = responsiblefor_data.get('assistant_lawyer', '') case_manager_lawyer = responsiblefor_data.get('case_manager_lawyer', '') if undertaker in str(responsible_person) or undertaker in str(main_lawyer) or \ undertaker in str(assistant_lawyer) or undertaker in str(case_manager_lawyer): is_match = True match_reason.append("负责人匹配") except (json.JSONDecodeError, TypeError): if undertaker in pro.responsiblefor: is_match = True match_reason.append("负责人匹配") if is_match: project_list.append({ 'id': pro.id, 'ContractNo': pro.ContractNo, 'times': pro.times, 'type': pro.type, 'client_info': pro.client_info, 'party_info': pro.party_info, 'description': pro.description, 'responsiblefor': pro.responsiblefor, 'match_reason': ';'.join(match_reason) if match_reason else '匹配' }) result['project_conflicts'] = project_list # 3. 检索投标表 bid_q = Q(is_deleted=False) if exclude_bid_id: bid_q &= ~Q(id=exclude_bid_id) bid_records = Bid.objects.filter(bid_q) bid_list = [] for bid in bid_records[:50]: # 限制查询数量 is_match = False match_reason = [] # 检查招标单位匹配 if bidding_unit and bid.BiddingUnit: if bidding_unit in bid.BiddingUnit or bid.BiddingUnit in bidding_unit: is_match = True match_reason.append("招标单位匹配") # 检查委托人/相对方是否在招标单位中(支持姓名和身份证号匹配) if (client_info or client_name) and bid.BiddingUnit: if client_name and client_name in bid.BiddingUnit: is_match = True match_reason.append("委托人姓名在招标单位中") elif client_original and client_original in bid.BiddingUnit: is_match = True match_reason.append("委托人信息在招标单位中") elif client_id_number and client_id_number in bid.BiddingUnit: is_match = True match_reason.append("委托人身份证号在招标单位中") if (party_info or party_name) and bid.BiddingUnit: if party_name and party_name in bid.BiddingUnit: is_match = True match_reason.append("相对方姓名在招标单位中") elif party_original and party_original in bid.BiddingUnit: is_match = True match_reason.append("相对方信息在招标单位中") elif party_id_number and party_id_number in bid.BiddingUnit: is_match = True match_reason.append("相对方身份证号在招标单位中") if is_match: bid_list.append({ 'id': bid.id, 'ProjectName': bid.ProjectName, 'times': bid.times, 'BiddingUnit': bid.BiddingUnit, 'state': bid.state, 'match_reason': ';'.join(match_reason) if match_reason else '匹配' }) result['bid_conflicts'] = bid_list return result class ConflictSearch(APIView): """ 利益冲突检索查询接口 比对预立案登记、投标登记、立项登记三张表,返回冲突数据 """ def post(self, request, *args, **kwargs): """ 利益冲突检索 请求参数(至少提供一个): - client_info: 委托人身份信息(可选),支持格式: * "张三(3263452342342342)" * "张三,身份证号:3263452342342342" * "张三,3263452342342342" * "张三" - party_info: 相对方身份信息(可选),支持格式同上 - undertaker: 承办人员(可选) - bidding_unit: 招标单位(可选) - exclude_prefiling_id: 要排除的预立案ID(可选) - exclude_project_id: 要排除的立项ID(可选) - exclude_bid_id: 要排除的投标ID(可选) """ client_info = request.data.get('client_info', '').strip() if request.data.get('client_info') else None party_info = request.data.get('party_info', '').strip() if request.data.get('party_info') else None undertaker = request.data.get('undertaker', '').strip() if request.data.get('undertaker') else None bidding_unit = request.data.get('bidding_unit', '').strip() if request.data.get('bidding_unit') else None exclude_prefiling_id = request.data.get('exclude_prefiling_id') exclude_project_id = request.data.get('exclude_project_id') exclude_bid_id = request.data.get('exclude_bid_id') # 至少需要提供一个查询条件 if not any([client_info, party_info, undertaker, bidding_unit]): return Response({ 'status': 'error', 'message': '至少需要提供一个查询条件(client_info、party_info、undertaker、bidding_unit之一)', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 执行冲突检索 conflict_result = conflict_search( client_info=client_info, party_info=party_info, undertaker=undertaker, bidding_unit=bidding_unit, exclude_prefiling_id=exclude_prefiling_id, exclude_project_id=exclude_project_id, exclude_bid_id=exclude_bid_id ) # 归一化冲突记录为前端期望格式 [{ index, name, idNumber }, ...](预立案用 client_username/party_username,立项用 client_info/party_info,投标用 BiddingUnit) def _normalize_person_list(val): if not val: return [] try: lst = json.loads(val) if isinstance(val, str) else val if not isinstance(lst, list): return None out = [] for i, item in enumerate(lst): if not isinstance(item, dict): continue name = item.get('name') or item.get('name_original') or '' id_num = item.get('idNumber') or item.get('id_number') or '' if not isinstance(name, str): name = str(name) if name else '' if not isinstance(id_num, str): id_num = str(id_num) if id_num else '' out.append({ 'index': item.get('index') if isinstance(item.get('index'), (int, float)) else (i + 1), 'name': name, 'idNumber': id_num }) return out except (json.JSONDecodeError, TypeError, ValueError): return None def _normalize_conflict_records(records): processed = [] for record in records: new_record = dict(record) for key in ('client_info', 'party_info', 'BiddingUnit', 'client_username', 'party_username'): if key not in new_record or not new_record[key]: continue normalized = _normalize_person_list(new_record[key]) if normalized is not None: new_record[key] = normalized processed.append(new_record) return processed prefiling = _normalize_conflict_records(conflict_result['prefiling_conflicts']) project = _normalize_conflict_records(conflict_result['project_conflicts']) bid = _normalize_conflict_records(conflict_result['bid_conflicts']) # 统计冲突数量 total_conflicts = len(prefiling) + len(project) + len(bid) return Response({ 'message': '检索成功', 'code': 0, 'data': { 'total_conflicts': total_conflicts, 'prefiling_conflicts_count': len(prefiling), 'project_conflicts_count': len(project), 'bid_conflicts_count': len(bid), 'prefiling_conflicts': prefiling, 'project_conflicts': project, 'bid_conflicts': bid } }, status=status.HTTP_200_OK) # ==================== 案件标签管理接口 ==================== class CreateCaseTag(APIView): """创建案件标签""" def post(self, request, *args, **kwargs): name = request.data.get('name') color = request.data.get('color', '#1890ff') description = request.data.get('description', '') if not name: return Response({'status': 'error', 'message': '缺少参数:标签名称', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 检查标签名是否已存在 if CaseTag.objects.filter(name=name, is_deleted=False).exists(): return Response({'status': 'error', 'message': '标签名称已存在', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) import datetime now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') tag = CaseTag.objects.create( name=name, color=color, description=description, created_at=now, updated_at=now ) # 记录操作日志 log_operation( request=request, operation_type='CREATE', module='Business', action='创建案件标签', target_type='CaseTag', target_id=tag.id, target_name=tag.name, new_data={'name': name, 'color': color, 'description': description}, remark=f'创建案件标签:{name}' ) return Response({ 'message': '创建成功', 'code': 0, 'data': { 'id': tag.id, 'name': tag.name, 'color': tag.color, 'description': tag.description } }, status=status.HTTP_200_OK) class CaseTagList(APIView): """案件标签列表""" def post(self, request, *args, **kwargs): page = request.data.get('page', 1) per_page = request.data.get('per_page', 10) name = request.data.get('name', '') # 搜索标签名称 Q_obj = Q(is_deleted=False) if name: Q_obj &= Q(name__icontains=name) tags = CaseTag.objects.filter(Q_obj).order_by('-id') total = len(tags) paginator = Paginator(tags, per_page) try: tags_page = paginator.page(page) except PageNotAnInteger: tags_page = paginator.page(1) except EmptyPage: tags_page = paginator.page(paginator.num_pages) data = [] for tag in tags_page.object_list: # 统计使用该标签的案件数量 case_count = tag.cases.filter(is_deleted=False).count() data.append({ 'id': tag.id, 'name': tag.name, 'color': tag.color, 'description': tag.description, 'case_count': case_count, # 使用该标签的案件数量 'created_at': tag.created_at, 'updated_at': tag.updated_at }) return Response({ 'message': '查询成功', 'total': total, 'data': data, 'code': 0 }, status=status.HTTP_200_OK) class CaseTagDetail(APIView): """案件标签详情""" def post(self, request, *args, **kwargs): id = request.data.get('id') if not id: return Response({'status': 'error', 'message': '缺少参数:id', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: tag = CaseTag.objects.get(id=id, is_deleted=False) except CaseTag.DoesNotExist: return Response({'status': 'error', 'message': '标签不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 获取使用该标签的案件列表 cases = tag.cases.filter(is_deleted=False) case_list = [] for case in cases: case_list.append({ 'id': case.id, 'contract_no': case.contract_no or '', 'project_type': case.project_type or '', 'client_name': case.client_name or '', 'times': case.times }) return Response({ 'message': '查询成功', 'code': 0, 'data': { 'id': tag.id, 'name': tag.name, 'color': tag.color, 'description': tag.description, 'case_count': len(case_list), 'cases': case_list, 'created_at': tag.created_at, 'updated_at': tag.updated_at } }, status=status.HTTP_200_OK) class EditCaseTag(APIView): """编辑案件标签""" def post(self, request, *args, **kwargs): id = request.data.get('id') name = request.data.get('name') color = request.data.get('color') description = request.data.get('description') if not id: return Response({'status': 'error', 'message': '缺少参数:id', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: tag = CaseTag.objects.get(id=id, is_deleted=False) except CaseTag.DoesNotExist: return Response({'status': 'error', 'message': '标签不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 记录操作前的数据 old_data = { 'id': tag.id, 'name': tag.name, 'color': tag.color, 'description': tag.description } update_fields = [] if name: # 检查新名称是否已被其他标签使用 if CaseTag.objects.filter(name=name, is_deleted=False).exclude(id=id).exists(): return Response({'status': 'error', 'message': '标签名称已存在', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) tag.name = name update_fields.append('name') if color: tag.color = color update_fields.append('color') if description is not None: tag.description = description update_fields.append('description') if update_fields: import datetime tag.updated_at = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') update_fields.append('updated_at') tag.save(update_fields=update_fields) # 记录操作日志 log_operation( request=request, operation_type='UPDATE', module='Business', action='编辑案件标签', target_type='CaseTag', target_id=tag.id, target_name=tag.name, old_data=old_data, new_data={'name': tag.name, 'color': tag.color, 'description': tag.description}, remark=f'编辑案件标签:{tag.name}' ) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class DeleteCaseTag(APIView): """删除案件标签""" def post(self, request, *args, **kwargs): id = request.data.get('id') if not id: return Response({'status': 'error', 'message': '缺少参数:id', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: tag = CaseTag.objects.get(id=id, is_deleted=False) except CaseTag.DoesNotExist: return Response({'status': 'error', 'message': '标签不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 记录操作前的数据 old_data = { 'id': tag.id, 'name': tag.name, 'color': tag.color, 'description': tag.description } # 软删除:更新 is_deleted 字段 tag.is_deleted = True tag.save() # 记录操作日志 log_operation( request=request, operation_type='DELETE', module='Business', action='删除案件标签', target_type='CaseTag', target_id=tag.id, target_name=tag.name, old_data=old_data, remark=f'删除案件标签:{tag.name}' ) return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) class CaseTagDropdownList(APIView): """获取标签下拉列表(用于案件管理中选择标签)""" def post(self, request, *args, **kwargs): tags = CaseTag.objects.filter(is_deleted=False).order_by('name') data = [] for tag in tags: data.append({ 'id': tag.id, 'name': tag.name, 'color': tag.color }) return Response({ 'message': '查询成功', 'data': data, 'code': 0 }, status=status.HTTP_200_OK) class SetCaseTags(APIView): """给案件设置标签""" def post(self, request, *args, **kwargs): """ 给案件设置标签 支持设置多个标签,也可以清空所有标签 """ case_id = request.data.get('case_id') tag_ids = request.data.get('tag_ids') # 标签ID列表(数组或逗号分隔的字符串) if not case_id: return Response({ 'status': 'error', 'message': '缺少参数:case_id(案件ID)', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 验证案件是否存在 try: case = Case.objects.get(id=case_id, is_deleted=False) except Case.DoesNotExist: return Response({ 'status': 'error', 'message': '案件不存在或已被删除', 'code': 1 }, status=status.HTTP_404_NOT_FOUND) # 记录操作前的标签 old_tags = list(case.tags.filter(is_deleted=False).values('id', 'name')) old_tag_ids = [tag['id'] for tag in old_tags] # 处理标签ID列表 tag_id_list = [] if tag_ids is not None: try: # 支持数组或逗号分隔的字符串 if isinstance(tag_ids, str): if tag_ids.strip() == '': tag_id_list = [] # 空字符串表示清空所有标签 elif ',' in tag_ids: tag_id_list = [int(x.strip()) for x in tag_ids.split(',') if x.strip()] else: tag_id_list = [int(tag_ids)] elif isinstance(tag_ids, list): tag_id_list = [int(x) for x in tag_ids if x] else: tag_id_list = [] except (ValueError, TypeError) as e: return Response({ 'status': 'error', 'message': f'标签ID格式错误:{str(e)}', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 验证标签是否存在 if tag_id_list: valid_tags = CaseTag.objects.filter(id__in=tag_id_list, is_deleted=False) valid_tag_ids = list(valid_tags.values_list('id', flat=True)) # 检查是否有无效的标签ID invalid_tag_ids = set(tag_id_list) - set(valid_tag_ids) if invalid_tag_ids: return Response({ 'status': 'error', 'message': f'以下标签ID不存在或已被删除:{", ".join(map(str, invalid_tag_ids))}', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) # 设置标签关联 case.tags.set(valid_tags) else: # 清空所有标签 case.tags.clear() # 获取更新后的标签信息 new_tags = list(case.tags.filter(is_deleted=False).values('id', 'name', 'color')) new_tag_ids = [tag['id'] for tag in new_tags] # 记录操作日志 log_operation( request=request, operation_type='UPDATE', module='Business', action='设置案件标签', target_type='Case', target_id=case.id, target_name=case.contract_no or f'案件ID-{case.id}', old_data={'tag_ids': old_tag_ids, 'tags': old_tags}, new_data={'tag_ids': new_tag_ids, 'tags': new_tags}, remark=f'设置案件标签:案件ID {case_id},标签ID {new_tag_ids}' ) return Response({ 'message': '设置成功', 'code': 0, 'data': { 'case_id': case.id, 'tags': new_tags, 'tag_ids': new_tag_ids } }, status=status.HTTP_200_OK) class CaseListByTag(APIView): """按标签筛选案件列表""" def post(self, request, *args, **kwargs): """ 根据标签ID筛选案件列表 """ page = request.data.get('page') per_page = request.data.get('per_page') tag_ids = request.data.get('tag_ids') # 标签ID列表(数组或逗号分隔的字符串) times = request.data.get('times') end_time = request.data.get('end_time') type = request.data.get('type') if not all([page, per_page]): return Response({'status': 'error', 'message': '缺少参数:page和per_page', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) if not tag_ids: return Response({'status': 'error', 'message': '缺少参数:tag_ids', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 解析标签ID try: if isinstance(tag_ids, str): if ',' in tag_ids: tag_id_list = [int(x.strip()) for x in tag_ids.split(',') if x.strip()] else: tag_id_list = [int(tag_ids)] elif isinstance(tag_ids, list): tag_id_list = [int(x) for x in tag_ids if x] else: tag_id_list = [] except (ValueError, TypeError): return Response({'status': 'error', 'message': '标签ID格式错误', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) if not tag_id_list: return Response({'status': 'error', 'message': '标签ID列表为空', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 验证标签是否存在 valid_tags = CaseTag.objects.filter(id__in=tag_id_list, is_deleted=False) if not valid_tags.exists(): return Response({'status': 'error', 'message': '标签不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 构建查询条件 Q_obj = Q(is_deleted=False) # 按标签筛选(多标签取交集,即案件必须同时拥有所有指定标签) for tag_id in tag_id_list: Q_obj &= Q(tags__id=tag_id) # 时间范围筛选 if times and end_time: Q_obj &= Q(times__gte=times) & Q(times__lte=end_time) # 项目类型筛选 if type: por_id = ProjectRegistration.objects.filter(type=type, is_deleted=False).values_list('id', flat=True) Q_obj &= Q(project_id__in=por_id) # 查询案件(使用distinct去重,因为多对多关系可能产生重复) cases = Case.objects.filter(Q_obj).distinct().order_by('-id') total = len(cases) paginator = Paginator(cases, per_page) try: cases_page = paginator.page(page) except PageNotAnInteger: cases_page = paginator.page(1) except EmptyPage: cases_page = paginator.page(paginator.num_pages) data = [] for info in cases_page.object_list: pro = None if info.project_id: try: pro = ProjectRegistration.objects.get(id=info.project_id, is_deleted=False) except ProjectRegistration.DoesNotExist: pro = None contract_no = info.contract_no or (pro.ContractNo if pro else None) project_type = info.project_type or (pro.type if pro else None) client_name = info.client_name or (pro.client_info if pro else None) party_name = info.party_name or (pro.party_info if pro else None) project_description = info.project_description or (pro.description if pro else None) responsiblefor_raw = info.responsiblefor or (pro.responsiblefor if pro else None) charge = info.charge or (pro.charge if pro else None) # 解析负责人信息 try: responsiblefor_dict = json.loads(responsiblefor_raw) if responsiblefor_raw else {} except (json.JSONDecodeError, TypeError): responsiblefor_dict = responsiblefor_raw if responsiblefor_raw else {} # 解析结案申请;代理合同已从案件管理移除,不再返回 try: closing_application_list = json.loads(info.Closingapplication) if info.Closingapplication else [] except (json.JSONDecodeError, TypeError): closing_application_list = [] if not closing_application_list: closing_application_list = "" contractreturn_str = info.Contractreturn or "" if contractreturn_str.strip() in ["[]", ""]: contractreturn_str = "" closing_application_str = info.Closingapplication or "" if closing_application_str.strip() in ["[]", ""]: closing_application_str = "" # 处理已开票/已收款字段 invoice_status_value = normalize_amount_value(info.invoice_status) paymentcollection_value = normalize_amount_value(info.paymentcollection) # 获取案件标签 tags = info.tags.filter(is_deleted=False) tag_list = [] for tag in tags: tag_list.append({ 'id': tag.id, 'name': tag.name, 'color': tag.color }) data.append({ "id": info.id, "ContractNo": contract_no or "", "type": project_type or "", "client_info": client_name or "", "party_info": party_name or "", "description": project_description, "responsiblefor": responsiblefor_dict, "charge": charge, 'times': info.times, "Contractreturn": contractreturn_str, # 合同返还(没有数据时返回空字符串) "Closingapplication": closing_application_str, # 结案申请(JSON字符串,没有文件时返回空字符串) "ClosingapplicationUrls": closing_application_list if closing_application_list else "", # 结案申请(URL列表,没有文件时返回空字符串) "ChangeRequest": info.ChangeRequest, "ChangeItem": info.ChangeItem, "ChangeReason": info.ChangeReason, "ChangeAgreement": info.ChangeAgreement, "invoice_status": invoice_status_value, "paymentcollection": paymentcollection_value, "state": info.state, "project_id": info.project_id, "tags": tag_list, "tag_ids": [tag['id'] for tag in tag_list], }) return Response({ 'message': '查询成功', "total": total, 'data': data, 'code': 0 }, status=status.HTTP_200_OK) class PropagandaEit(APIView): def post(self, request): content = request.data.get('content') pro = Propaganda.objects.get(id="1") pro.content = content pro.save() return Response({ 'message': '更新成功', 'code': 0 }, status=status.HTTP_200_OK) class addSystem(APIView): def post(self, request): title = request.data.get('title') content = request.data.get('content') file = request.FILES.getlist('file') state = request.data.get('state') token = request.META.get('token') if not all([title, content]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) now = datetime.now() date_str = now.strftime('%Y-%m-%d') bol = 0 if state == "置顶": bol = 1 user = User.objects.get(token=token, is_deleted=False).username System.objects.create( title=title, content=content, file=json.dumps(flies(file)), username=user, times=date_str, state=bol ) return Response({'message': '添加成功', 'code': 0}, status=status.HTTP_200_OK) class SystemList(APIView): def post(self, request): page = request.data.get('page') per_page = request.data.get('per_page') seas = System.objects.filter(is_deleted=False).order_by('-state', '-times') total = len(seas) paginator = Paginator(seas, per_page) try: user_agents_page = paginator.page(page) except PageNotAnInteger: user_agents_page = paginator.page(1) except EmptyPage: user_agents_page = paginator.page(paginator.num_pages) data = [] for info in user_agents_page.object_list: # 解析 file 字段:有文件返回数组,无文件返回空字符串 try: file_list = json.loads(info.file) if info.file else [] except (json.JSONDecodeError, TypeError): file_list = [] # 空列表返回空字符串 file_result = file_list if file_list else "" if file_result !="": file_result = json.dumps(file_result) data.append({ "id": info.id, "title": info.title, "content": info.content, "times": info.times, "file": file_result, # file 字段解析, "username": info.username, "state": info.state, }) return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK) class eitSystem(APIView): def post(self, request): ID = request.data.get('id') title = request.data.get('title') content = request.data.get('content') file = request.FILES.getlist('file') state = request.data.get('state') token = request.META.get('token') # 从中间件设置的 token 获取 if not all([title, content, ID]): return Response({'status': 'error', 'message': '缺少参数', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) try: system_record = System.objects.get(id=ID, is_deleted=False) except System.DoesNotExist: return Response({'status': 'error', 'message': '系统公告不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) now = datetime.now() date_str = now.strftime('%Y-%m-%d') if state: bol = 0 if state == "置顶": bol = 1 system_record.state = bol system_record.save(update_fields=['state']) # 获取用户信息(如果 token 存在) if token: try: user = User.objects.get(token=token, is_deleted=False).username except User.DoesNotExist: # 如果用户不存在,使用原有用户名 user = system_record.username else: # 如果 token 不存在,使用原有用户名 user = system_record.username if title: system_record.title = title system_record.save(update_fields=['title']) if content: system_record.content = content system_record.save(update_fields=['content']) if file: system_record.file = json.dumps(flies(file)) # 使用 flies 函数处理文件 system_record.save(update_fields=['file']) system_record.username = user system_record.times = date_str system_record.save(update_fields=['username', 'times']) return Response({'message': '编辑成功', 'code': 0}, status=status.HTTP_200_OK) class deleteSystem(APIView): def post(self, request, *args, **kwargs): ID = request.data.get('id') try: system_record = System.objects.get(id=ID, is_deleted=False) # 软删除:更新 is_deleted 字段 system_record.is_deleted = True system_record.save() return Response({'message': '删除成功', 'code': 0}, status=status.HTTP_200_OK) except System.DoesNotExist: return Response({'status': 'error', 'message': '系统公告不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) class ExportCaseLogExcel(APIView): def post(self, request, *args, **kwargs): """ 导出指定案件的日志到Excel,上传到OSS并返回下载链接 必填参数:case_id(案件ID) 可选参数:start_time(开始时间)、end_time(结束时间) 返回:下载链接 """ import pandas as pd import oss2 import uuid from io import BytesIO case_id = request.data.get('case_id') start_time = request.data.get('start_time') end_time = request.data.get('end_time') if not case_id: return Response({'status': 'error', 'message': '缺少参数case_id', 'code': 1}, status=status.HTTP_400_BAD_REQUEST) # 获取案件信息 try: case = Case.objects.get(id=case_id, is_deleted=False) except Case.DoesNotExist: return Response({'status': 'error', 'message': '案件不存在', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 构建查询条件 Q_obj = Q(is_deleted=False, case_id=case_id) if start_time: Q_obj &= Q(times__gte=start_time) if end_time: Q_obj &= Q(times__lte=end_time) # 查询日志 logs = Caselog.objects.filter(Q_obj).order_by('-times', '-id') if not logs.exists(): return Response({'status': 'error', 'message': '该案件没有日志记录', 'code': 1}, status=status.HTTP_404_NOT_FOUND) # 构建Excel数据(只保留:记录人、记录时间、工作内容) excel_data = [] for log in logs: excel_data.append({ '记录人': log.username or '', '记录时间': log.times or '', '工作内容': log.content or '', }) # 生成Excel文件 df = pd.DataFrame(excel_data) output = BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, index=False, sheet_name='案件日志') ws = writer.sheets['案件日志'] for idx, col in enumerate(df.columns): width = min(max(df[col].astype(str).map(len).max(), len(col)) + 2, 50) ws.column_dimensions[chr(65 + idx) if idx < 26 else chr(64 + idx // 26) + chr(65 + idx % 26)].width = width output.seek(0) # 上传到阿里云OSS try: endpoint = 'https://oss-cn-beijing.aliyuncs.com' access_key_id = "LTAI5tRMxrM95Pi8JEEmqRcg" access_key_secret = "8vueGCsRVeFyQMcAA7sysO7LSnuJDG" bucket_name = 'oss-bucket-yj' auth = oss2.Auth(access_key_id, access_key_secret) bucket = oss2.Bucket(auth, endpoint, bucket_name) # 生成唯一文件名 contract_no_safe = (case.contract_no or f'case{case_id}').replace('/', '_').replace('\\', '_') filename = f'{uuid.uuid4().hex[:12]}_{contract_no_safe}_日志_{datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx' # 上传文件 result = bucket.put_object(filename, output.getvalue()) if result.status == 200: # 生成下载链接 download_url = f'http://{bucket_name}.{endpoint.replace("https://", "")}/{filename}' return Response({ 'message': '导出成功', 'code': 0, 'data': { 'url': download_url, 'filename': f'{case.contract_no or f"案件{case_id}"}_日志.xlsx' } }, status=status.HTTP_200_OK) else: return Response({'status': 'error', 'message': '文件上传失败', 'code': 1}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except Exception as e: return Response({'status': 'error', 'message': f'文件上传失败: {str(e)}', 'code': 1}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)