"""小程序协议 — 查询与签署""" import logging import time import uuid from django.db import transaction from rest_framework import permissions from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.response import Response from rest_framework.views import APIView from peizhi.models import XcxXieyi, XcxXieyiImage, XcxXieyiQianshu from peizhi.xieyi_service import get_unsigned_xieyi_list, serialize_unsigned_brief from utils.oss_utils import upload_to_oss, validate_image logger = logging.getLogger(__name__) VALID_SHENFEN = frozenset({'dashou', 'guanshi', 'zuzhang', 'shangjia', 'normal'}) def _client_ip(request): xff = request.META.get('HTTP_X_FORWARDED_FOR') if xff: ip = xff.split(',')[0].strip() if ip: return ip return request.META.get('REMOTE_ADDR', '') or '' def _serialize_xieyi(item, yonghuid=None, include_images=False): signed = False if yonghuid: signed = XcxXieyiQianshu.objects.filter( yonghuid=yonghuid, xieyi_id=item.id, xieyi_version=item.version, ).exists() data = { 'id': item.id, 'title': item.title, 'shenfen': item.shenfen, 'shenfen_label': item.get_shenfen_display(), 'mode': item.mode, 'content_title': item.content_title or '', 'content_text': item.content_text or '', 'version': item.version, 'is_active': item.is_active, 'must_sign': item.must_sign, 'sort_order': item.sort_order, 'signed': signed, } if include_images: data['images'] = [ {'id': img.id, 'image_path': img.image_path, 'sort_order': img.sort_order} for img in item.images.all().order_by('sort_order', 'id') ] return data class XieyiListView(APIView): """POST /peizhi/xieyihq — 按身份获取启用协议列表""" permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] def post(self, request): shenfen = (request.data.get('shenfen') or '').strip() if shenfen not in VALID_SHENFEN: return Response({'code': 400, 'msg': '无效的身份参数', 'data': {'list': []}}) yonghuid = request.user.yonghuid qs = XcxXieyi.objects.filter(shenfen=shenfen, is_active=True).order_by('sort_order', 'id') items = [_serialize_xieyi(x, yonghuid) for x in qs] return Response({'code': 0, 'msg': 'success', 'data': {'list': items}}) class XieyiDetailView(APIView): """POST /peizhi/xieyixq — 协议详情""" permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] def post(self, request): xieyi_id = request.data.get('id') or request.data.get('xieyi_id') if not xieyi_id: return Response({'code': 400, 'msg': '缺少协议 id', 'data': None}) try: item = XcxXieyi.objects.prefetch_related('images').get(id=xieyi_id, is_active=True) except XcxXieyi.DoesNotExist: return Response({'code': 404, 'msg': '协议不存在或已下线', 'data': None}) yonghuid = request.user.yonghuid return Response({ 'code': 0, 'msg': 'success', 'data': _serialize_xieyi(item, yonghuid, include_images=True), }) class XieyiCheckView(APIView): """POST /peizhi/xieyijc — 检查必签协议是否已全部签署(后端为准)""" permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] def post(self, request): shenfen = (request.data.get('shenfen') or '').strip() if shenfen not in VALID_SHENFEN: return Response({'code': 400, 'msg': '无效的身份参数', 'data': None}) yonghuid = request.user.yonghuid unsigned = get_unsigned_xieyi_list(yonghuid, shenfen) return Response({ 'code': 0, 'msg': 'success', 'data': { 'all_signed': len(unsigned) == 0, 'unsigned': serialize_unsigned_brief(unsigned), }, }) class XieyiSignStatusView(APIView): """POST /peizhi/xieyizt — 是否已签署当前版本""" permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] def post(self, request): xieyi_id = request.data.get('id') or request.data.get('xieyi_id') if not xieyi_id: return Response({'code': 400, 'msg': '缺少协议 id', 'data': None}) try: item = XcxXieyi.objects.get(id=xieyi_id, is_active=True) except XcxXieyi.DoesNotExist: return Response({'code': 404, 'msg': '协议不存在', 'data': None}) yonghuid = request.user.yonghuid record = XcxXieyiQianshu.objects.filter( yonghuid=yonghuid, xieyi_id=item.id, xieyi_version=item.version, ).order_by('-signed_at').first() return Response({ 'code': 0, 'msg': 'success', 'data': { 'signed': record is not None, 'version': item.version, 'signed_at': record.signed_at.strftime('%Y-%m-%d %H:%M:%S') if record else '', 'signature_path': record.signature_path if record else '', }, }) class XieyiSignView(APIView): """POST /peizhi/xieyiqm — 上传手写签名并完成签署""" permission_classes = [permissions.IsAuthenticated] parser_classes = (MultiPartParser, FormParser) def post(self, request): xieyi_id = request.data.get('xieyi_id') or request.data.get('id') shenfen = (request.data.get('shenfen') or '').strip() image_file = request.FILES.get('image') or request.FILES.get('file') if not xieyi_id: return Response({'code': 1, 'msg': '缺少 xieyi_id', 'data': None}) if not image_file: return Response({'code': 2, 'msg': '请上传签名图片', 'data': None}) if shenfen and shenfen not in VALID_SHENFEN: return Response({'code': 3, 'msg': '无效身份', 'data': None}) is_valid, error_msg = validate_image(image_file) if not is_valid: return Response({'code': 4, 'msg': error_msg, 'data': None}) try: item = XcxXieyi.objects.get(id=xieyi_id, is_active=True) except XcxXieyi.DoesNotExist: return Response({'code': 404, 'msg': '协议不存在或已下线', 'data': None}) yonghuid = request.user.yonghuid if XcxXieyiQianshu.objects.filter( yonghuid=yonghuid, xieyi_id=item.id, xieyi_version=item.version, ).exists(): return Response({'code': 5, 'msg': '您已签署过当前版本协议', 'data': None}) oss_path = f'qianming/xieyi/{yonghuid}/{item.id}_{int(time.time())}_{uuid.uuid4().hex[:8]}.png' image_file.seek(0) full_url = upload_to_oss(image_file, oss_path) if not full_url: return Response({'code': 6, 'msg': '签名上传失败', 'data': None}) sign_shenfen = shenfen or item.shenfen with transaction.atomic(): record = XcxXieyiQianshu.objects.create( yonghuid=yonghuid, xieyi=item, xieyi_version=item.version, shenfen=sign_shenfen, signature_path=oss_path, client_ip=_client_ip(request), ) return Response({ 'code': 0, 'msg': '签署成功', 'data': { 'id': record.id, 'signature_path': oss_path, 'signed_at': record.signed_at.strftime('%Y-%m-%d %H:%M:%S'), 'all_signed': len(get_unsigned_xieyi_list(yonghuid, item.shenfen)) == 0, }, })