From f6152204d366aaf4b8c4dbb94a5ebe9cb410dda7 Mon Sep 17 00:00:00 2001 From: XingQue Date: Sat, 27 Jun 2026 02:27:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8D=8F=E8=AE=AE=E7=AD=BE=E7=BD=B2?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E3=80=81=E6=B5=B7=E6=8A=A5=E8=83=8C=E6=99=AF?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E9=85=8D=E7=BD=AE=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- houtai/urls.py | 9 + houtai/xieyi_views.py | 258 ++++++++++++++++++++++++++++ peizhi/migrations/0017_xcx_xieyi.py | 93 ++++++++++ peizhi/models.py | 74 +++++++- peizhi/urls.py | 5 + peizhi/xieyi_views.py | 180 +++++++++++++++++++ utils/page_ziyuan_service.py | 6 + 7 files changed, 624 insertions(+), 1 deletion(-) create mode 100644 houtai/xieyi_views.py create mode 100644 peizhi/migrations/0017_xcx_xieyi.py create mode 100644 peizhi/xieyi_views.py diff --git a/houtai/urls.py b/houtai/urls.py index 0d3f02e..36c0f95 100644 --- a/houtai/urls.py +++ b/houtai/urls.py @@ -18,6 +18,10 @@ from .view import GetClubConfigView, GetCrossOrderListView, PartnerGetOrderDataV CaiwuView, CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView, \ KhgglView, ShgxgsjView, ZxkfghdsView, KptxwztjbView from .huashu_views import HuashuListAPIView, HuashuModifyAPIView +from .xieyi_views import ( + XieyiListAPIView, XieyiModifyAPIView, XieyiImageUploadAPIView, + XieyiImageDeleteAPIView, XieyiSignRecordAPIView, +) urlpatterns = [ @@ -89,6 +93,11 @@ urlpatterns = [ path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'), path('hthqhs', HuashuListAPIView.as_view(), name='后台获取话术配置'), path('htxghs', HuashuModifyAPIView.as_view(), name='后台更新话术配置'), + path('hthqxy', XieyiListAPIView.as_view(), name='后台获取协议列表'), + path('htxgxy', XieyiModifyAPIView.as_view(), name='后台协议增删改'), + path('htxgytp', XieyiImageUploadAPIView.as_view(), name='后台上传协议图片'), + path('htscxytp', XieyiImageDeleteAPIView.as_view(), name='后台删除协议图片'), + path('hthqxyqs', XieyiSignRecordAPIView.as_view(), name='后台查询协议签署记录'), path('kffkdssq', FineApplyView.as_view(), name='跨平台处罚罚款申请接口'), path('kffkdssq_notify', PartnerFineNotifyView.as_view(), name='跨屏要对方平台处罚通知接口'), diff --git a/houtai/xieyi_views.py b/houtai/xieyi_views.py new file mode 100644 index 0000000..9b6d587 --- /dev/null +++ b/houtai/xieyi_views.py @@ -0,0 +1,258 @@ +"""后台 — 小程序协议管理""" +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 utils.oss_utils import upload_to_oss, validate_image +from .utils import verify_kefu_permission + +logger = logging.getLogger(__name__) + +REQUIRED_PERMISSION = '8080a' + + +def _check_perm(request): + username = request.data.get('username') + kefu, perms_or_resp = verify_kefu_permission(request, username) + if kefu is None: + return None, perms_or_resp + if REQUIRED_PERMISSION not in perms_or_resp: + return None, Response({'code': 403, 'msg': '您没有权限访问协议管理(需 8080a)'}) + return kefu, None + + +def _serialize_admin(item): + return { + '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, + 'sort_order': item.sort_order, + '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') + ], + 'created_at': item.created_at.strftime('%Y-%m-%d %H:%M:%S') if item.created_at else '', + 'updated_at': item.updated_at.strftime('%Y-%m-%d %H:%M:%S') if item.updated_at else '', + } + + +class XieyiListAPIView(APIView): + """POST /houtai/hthqxy — 协议列表""" + + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + kefu, err = _check_perm(request) + if kefu is None: + return err + shenfen = request.data.get('shenfen') + qs = XcxXieyi.objects.prefetch_related('images').all().order_by('sort_order', 'id') + if shenfen: + qs = qs.filter(shenfen=shenfen) + items = [_serialize_admin(x) for x in qs] + scenes = [{'key': c[0], 'label': c[1]} for c in XcxXieyi.SHENFEN_CHOICES] + modes = [{'key': c[0], 'label': c[1]} for c in XcxXieyi.MODE_CHOICES] + return Response({'code': 0, 'data': {'items': items, 'shenfen_choices': scenes, 'mode_choices': modes}}) + + +class XieyiModifyAPIView(APIView): + """POST /houtai/htxgxy — 协议增删改""" + + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + kefu, err = _check_perm(request) + if kefu is None: + return err + action = request.data.get('action') + if action == 'create': + return self._create(request) + if action == 'update': + return self._update(request) + if action == 'delete': + return self._delete(request) + return Response({'code': 400, 'msg': '未知 action'}) + + def _create(self, request): + title = (request.data.get('title') or '').strip() + shenfen = (request.data.get('shenfen') or '').strip() + mode = int(request.data.get('mode') or XcxXieyi.MODE_TEXT) + if not title or not shenfen: + return Response({'code': 400, 'msg': 'title、shenfen 必填'}) + valid_shenfen = {c[0] for c in XcxXieyi.SHENFEN_CHOICES} + if shenfen not in valid_shenfen: + return Response({'code': 400, 'msg': '无效身份'}) + item = XcxXieyi.objects.create( + title=title, + shenfen=shenfen, + mode=mode, + content_title=request.data.get('content_title', ''), + content_text=request.data.get('content_text', ''), + is_active=bool(request.data.get('is_active', True)), + sort_order=int(request.data.get('sort_order', 0)), + ) + return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_admin(item)}) + + def _update(self, request): + item_id = request.data.get('id') + if not item_id: + return Response({'code': 400, 'msg': '缺少 id'}) + try: + item = XcxXieyi.objects.prefetch_related('images').get(id=item_id) + except XcxXieyi.DoesNotExist: + return Response({'code': 404, 'msg': '记录不存在'}) + + content_changed = False + if 'title' in request.data: + item.title = request.data.get('title', '') + if 'shenfen' in request.data: + shenfen = request.data.get('shenfen') + valid_shenfen = {c[0] for c in XcxXieyi.SHENFEN_CHOICES} + if shenfen not in valid_shenfen: + return Response({'code': 400, 'msg': '无效身份'}) + item.shenfen = shenfen + if 'mode' in request.data: + item.mode = int(request.data.get('mode')) + content_changed = True + if 'content_title' in request.data: + item.content_title = request.data.get('content_title', '') + content_changed = True + if 'content_text' in request.data: + item.content_text = request.data.get('content_text', '') + content_changed = True + if 'is_active' in request.data: + item.is_active = bool(request.data.get('is_active')) + if 'sort_order' in request.data: + item.sort_order = int(request.data.get('sort_order', 0)) + if 'bump_version' in request.data and request.data.get('bump_version'): + content_changed = True + + if content_changed: + item.version = (item.version or 1) + 1 + item.save() + return Response({'code': 0, 'msg': '更新成功', 'data': _serialize_admin(item)}) + + def _delete(self, request): + item_id = request.data.get('id') + if not item_id: + return Response({'code': 400, 'msg': '缺少 id'}) + deleted, _ = XcxXieyi.objects.filter(id=item_id).delete() + if not deleted: + return Response({'code': 404, 'msg': '记录不存在'}) + return Response({'code': 0, 'msg': '删除成功'}) + + +class XieyiImageUploadAPIView(APIView): + """POST /houtai/htxgytp — 上传协议图片""" + + permission_classes = [permissions.IsAuthenticated] + parser_classes = (MultiPartParser, FormParser) + + def post(self, request): + kefu, err = _check_perm(request) + if kefu is None: + return err + xieyi_id = request.data.get('xieyi_id') or request.data.get('id') + image_file = request.FILES.get('image') + if not xieyi_id or not image_file: + return Response({'code': 400, 'msg': 'xieyi_id 与 image 必填'}) + is_valid, error_msg = validate_image(image_file) + if not is_valid: + return Response({'code': 400, 'msg': error_msg}) + try: + item = XcxXieyi.objects.get(id=xieyi_id) + except XcxXieyi.DoesNotExist: + return Response({'code': 404, 'msg': '协议不存在'}) + + oss_path = f'a_long/xieyi/{item.id}/{int(time.time())}_{uuid.uuid4().hex[:8]}.jpg' + image_file.seek(0) + full_url = upload_to_oss(image_file, oss_path) + if not full_url: + return Response({'code': 500, 'msg': '上传失败'}) + + with transaction.atomic(): + img = XcxXieyiImage.objects.create( + xieyi=item, + image_path=oss_path, + sort_order=int(request.data.get('sort_order', 0)), + ) + item.version = (item.version or 1) + 1 + item.save(update_fields=['version', 'updated_at']) + + return Response({ + 'code': 0, + 'msg': '上传成功', + 'data': {'id': img.id, 'image_path': oss_path, 'version': item.version}, + }) + + +class XieyiImageDeleteAPIView(APIView): + """POST /houtai/htscxytp — 删除协议图片""" + + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + kefu, err = _check_perm(request) + if kefu is None: + return err + img_id = request.data.get('image_id') + if not img_id: + return Response({'code': 400, 'msg': '缺少 image_id'}) + try: + img = XcxXieyiImage.objects.select_related('xieyi').get(id=img_id) + except XcxXieyiImage.DoesNotExist: + return Response({'code': 404, 'msg': '图片不存在'}) + xieyi = img.xieyi + img.delete() + xieyi.version = (xieyi.version or 1) + 1 + xieyi.save(update_fields=['version', 'updated_at']) + return Response({'code': 0, 'msg': '删除成功'}) + + +class XieyiSignRecordAPIView(APIView): + """POST /houtai/hthqxyqs — 查询签署记录""" + + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + kefu, err = _check_perm(request) + if kefu is None: + return err + yonghuid = (request.data.get('yonghuid') or '').strip() + xieyi_id = request.data.get('xieyi_id') + qs = XcxXieyiQianshu.objects.select_related('xieyi').order_by('-signed_at') + if yonghuid: + qs = qs.filter(yonghuid=yonghuid) + if xieyi_id: + qs = qs.filter(xieyi_id=xieyi_id) + items = [] + for r in qs[:200]: + items.append({ + 'id': r.id, + 'yonghuid': r.yonghuid, + 'xieyi_id': r.xieyi_id, + 'xieyi_title': r.xieyi.title, + 'xieyi_version': r.xieyi_version, + 'shenfen': r.shenfen, + 'signature_path': r.signature_path, + 'signed_at': r.signed_at.strftime('%Y-%m-%d %H:%M:%S'), + 'client_ip': r.client_ip, + }) + return Response({'code': 0, 'data': {'items': items}}) diff --git a/peizhi/migrations/0017_xcx_xieyi.py b/peizhi/migrations/0017_xcx_xieyi.py new file mode 100644 index 0000000..031e078 --- /dev/null +++ b/peizhi/migrations/0017_xcx_xieyi.py @@ -0,0 +1,93 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0016_xcx_sys_peizhi_page_ziyuan'), + ] + + operations = [ + migrations.CreateModel( + name='XcxXieyi', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200, verbose_name='后台标题')), + ('shenfen', models.CharField( + choices=[ + ('dashou', '打手'), + ('guanshi', '管事'), + ('zuzhang', '组长'), + ('shangjia', '商家'), + ('normal', '点单老板'), + ], + db_index=True, + max_length=20, + verbose_name='适用身份', + )), + ('mode', models.PositiveSmallIntegerField( + choices=[(1, '纯图片'), (2, '文字')], + default=2, + verbose_name='展示模式', + )), + ('content_title', models.CharField(blank=True, default='', max_length=200, verbose_name='前端展示标题')), + ('content_text', models.TextField(blank=True, default='', verbose_name='文字内容')), + ('version', models.PositiveIntegerField(default=1, verbose_name='版本号')), + ('is_active', models.BooleanField(default=True, verbose_name='是否启用')), + ('sort_order', models.IntegerField(default=0, verbose_name='排序')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '小程序协议', + 'verbose_name_plural': '小程序协议', + 'db_table': 'xcx_xieyi', + 'ordering': ['sort_order', 'id'], + }, + ), + migrations.CreateModel( + name='XcxXieyiImage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image_path', models.CharField(max_length=500, verbose_name='OSS相对路径')), + ('sort_order', models.IntegerField(default=0, verbose_name='排序')), + ('xieyi', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='images', + to='peizhi.xcxxieyi', + verbose_name='协议', + )), + ], + options={ + 'verbose_name': '协议图片', + 'verbose_name_plural': '协议图片', + 'db_table': 'xcx_xieyi_image', + 'ordering': ['sort_order', 'id'], + }, + ), + migrations.CreateModel( + name='XcxXieyiQianshu', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('xieyi_version', models.PositiveIntegerField(verbose_name='签署时协议版本')), + ('shenfen', models.CharField(max_length=20, verbose_name='签署时身份')), + ('signature_path', models.CharField(max_length=500, verbose_name='签名图OSS路径')), + ('signed_at', models.DateTimeField(auto_now_add=True, verbose_name='签署时间')), + ('client_ip', models.CharField(blank=True, default='', max_length=64, verbose_name='客户端IP')), + ('xieyi', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='sign_records', + to='peizhi.xcxxieyi', + verbose_name='协议', + )), + ], + options={ + 'verbose_name': '协议签署记录', + 'verbose_name_plural': '协议签署记录', + 'db_table': 'xcx_xieyi_qianshu', + 'indexes': [models.Index(fields=['yonghuid', 'xieyi'], name='xcx_xieyi_q_yid_xy_idx')], + }, + ), + ] diff --git a/peizhi/models.py b/peizhi/models.py index dd0d87e..56793a1 100644 --- a/peizhi/models.py +++ b/peizhi/models.py @@ -721,4 +721,76 @@ class XcxPageZiyuan(models.Model): indexes = [models.Index(fields=['ziyuan_zu'])] def __str__(self): - return f'{self.ziyuan_zu}.{self.ziyuan_key}' \ No newline at end of file + return f'{self.ziyuan_zu}.{self.ziyuan_key}' + + +class XcxXieyi(models.Model): + """小程序用户协议配置""" + + MODE_IMAGE = 1 + MODE_TEXT = 2 + MODE_CHOICES = ( + (MODE_IMAGE, '纯图片'), + (MODE_TEXT, '文字'), + ) + SHENFEN_CHOICES = ( + ('dashou', '打手'), + ('guanshi', '管事'), + ('zuzhang', '组长'), + ('shangjia', '商家'), + ('normal', '点单老板'), + ) + + title = models.CharField(max_length=200, verbose_name='后台标题') + shenfen = models.CharField(max_length=20, choices=SHENFEN_CHOICES, db_index=True, verbose_name='适用身份') + mode = models.PositiveSmallIntegerField(choices=MODE_CHOICES, default=MODE_TEXT, verbose_name='展示模式') + content_title = models.CharField(max_length=200, blank=True, default='', verbose_name='前端展示标题') + content_text = models.TextField(blank=True, default='', verbose_name='文字内容') + version = models.PositiveIntegerField(default=1, verbose_name='版本号') + is_active = models.BooleanField(default=True, verbose_name='是否启用') + sort_order = models.IntegerField(default=0, verbose_name='排序') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'xcx_xieyi' + verbose_name = '小程序协议' + verbose_name_plural = verbose_name + ordering = ['sort_order', 'id'] + + def __str__(self): + return f'{self.title}({self.get_shenfen_display()})' + + +class XcxXieyiImage(models.Model): + """协议图片(纯图片模式,支持多张)""" + + xieyi = models.ForeignKey(XcxXieyi, on_delete=models.CASCADE, related_name='images', verbose_name='协议') + image_path = models.CharField(max_length=500, verbose_name='OSS相对路径') + sort_order = models.IntegerField(default=0, verbose_name='排序') + + class Meta: + db_table = 'xcx_xieyi_image' + verbose_name = '协议图片' + verbose_name_plural = verbose_name + ordering = ['sort_order', 'id'] + + +class XcxXieyiQianshu(models.Model): + """用户协议签署记录""" + + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + xieyi = models.ForeignKey(XcxXieyi, on_delete=models.CASCADE, related_name='sign_records', verbose_name='协议') + xieyi_version = models.PositiveIntegerField(verbose_name='签署时协议版本') + shenfen = models.CharField(max_length=20, verbose_name='签署时身份') + signature_path = models.CharField(max_length=500, verbose_name='签名图OSS路径') + signed_at = models.DateTimeField(auto_now_add=True, verbose_name='签署时间') + client_ip = models.CharField(max_length=64, blank=True, default='', verbose_name='客户端IP') + + class Meta: + db_table = 'xcx_xieyi_qianshu' + verbose_name = '协议签署记录' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['yonghuid', 'xieyi']), + ] \ No newline at end of file diff --git a/peizhi/urls.py b/peizhi/urls.py index cdcf770..1c0d3ff 100644 --- a/peizhi/urls.py +++ b/peizhi/urls.py @@ -11,6 +11,7 @@ from .xcx_config_views import ( XcxSysPeizhiQueryView, XcxSysPeizhiUpdateView, XcxPageZiyuanQueryView, XcxPageZiyuanUpdateView, XcxPageZiyuanUploadView, ) +from .xieyi_views import XieyiListView, XieyiDetailView, XieyiSignStatusView, XieyiSignView urlpatterns = [ path('shangpingonggao/', ShangpinGonggaoView.as_view(), name='商品公告轮播获取'), @@ -52,4 +53,8 @@ urlpatterns = [ path('xcxyzycx', XcxPageZiyuanQueryView.as_view(), name='管理员查询页面资源'), path('xcxyzgx', XcxPageZiyuanUpdateView.as_view(), name='管理员更新页面资源'), path('xcxyzsc', XcxPageZiyuanUploadView.as_view(), name='管理员上传页面资源'), + path('xieyihq', XieyiListView.as_view(), name='小程序获取协议列表'), + path('xieyixq', XieyiDetailView.as_view(), name='小程序获取协议详情'), + path('xieyizt', XieyiSignStatusView.as_view(), name='小程序查询协议签署状态'), + path('xieyiqm', XieyiSignView.as_view(), name='小程序签署协议'), ] \ No newline at end of file diff --git a/peizhi/xieyi_views.py b/peizhi/xieyi_views.py new file mode 100644 index 0000000..409538e --- /dev/null +++ b/peizhi/xieyi_views.py @@ -0,0 +1,180 @@ +"""小程序协议 — 查询与签署""" +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 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, + '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 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'), + }, + }) diff --git a/utils/page_ziyuan_service.py b/utils/page_ziyuan_service.py index e08a891..eeaf5ce 100644 --- a/utils/page_ziyuan_service.py +++ b/utils/page_ziyuan_service.py @@ -42,6 +42,8 @@ DEFAULT_ZIYUAN = { 'guanshiduan.iconWithdraw': 'beijing/guanshiduan/icon-withdraw.png', 'guanshiduan.iconContact': 'beijing/guanshiduan/icon-contact.png', 'guanshiduan.iconArrow': 'beijing/guanshiduan/icon-arrow.png', + 'haibao.guanshiBg': 'beijing/haibaobeijing.jpg', + 'haibao.zuzhangBg': 'beijing/zuzhangbeijing.jpg', } # 后台配置页展示用中文说明(键名 pageBg 等仅供程序识别) @@ -82,6 +84,8 @@ ZIYUAN_MIAOSHU = { 'guanshiduan.iconWithdraw': '管事端 · 提现图标', 'guanshiduan.iconContact': '管事端 · 联系客服图标', 'guanshiduan.iconArrow': '管事端 · 箭头/跳转图标', + 'haibao.guanshiBg': '管事推广海报 · 背景图', + 'haibao.zuzhangBg': '组长推广海报 · 背景图', } @@ -95,6 +99,7 @@ def clear_page_ziyuan_cache(): f'{_CACHE_PREFIX}:paihangbang', f'{_CACHE_PREFIX}:shangjiaduan', f'{_CACHE_PREFIX}:guanshiduan', + f'{_CACHE_PREFIX}:haibao', ]) @@ -152,4 +157,5 @@ def build_page_assets_for_api(): 'shangjiaduan': get_ziyuan_group('shangjiaduan'), 'paihangbang': get_ziyuan_group('paihangbang'), 'guanshiduan': get_ziyuan_group('guanshiduan'), + 'haibao': get_ziyuan_group('haibao'), }