@@ -18,6 +18,10 @@ from .view import GetClubConfigView, GetCrossOrderListView, PartnerGetOrderDataV
|
|||||||
CaiwuView, CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView, \
|
CaiwuView, CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView, \
|
||||||
KhgglView, ShgxgsjView, ZxkfghdsView, KptxwztjbView
|
KhgglView, ShgxgsjView, ZxkfghdsView, KptxwztjbView
|
||||||
from .huashu_views import HuashuListAPIView, HuashuModifyAPIView
|
from .huashu_views import HuashuListAPIView, HuashuModifyAPIView
|
||||||
|
from .xieyi_views import (
|
||||||
|
XieyiListAPIView, XieyiModifyAPIView, XieyiImageUploadAPIView,
|
||||||
|
XieyiImageDeleteAPIView, XieyiSignRecordAPIView,
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
@@ -89,6 +93,11 @@ urlpatterns = [
|
|||||||
path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'),
|
path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'),
|
||||||
path('hthqhs', HuashuListAPIView.as_view(), name='后台获取话术配置'),
|
path('hthqhs', HuashuListAPIView.as_view(), name='后台获取话术配置'),
|
||||||
path('htxghs', HuashuModifyAPIView.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', FineApplyView.as_view(), name='跨平台处罚罚款申请接口'),
|
||||||
path('kffkdssq_notify', PartnerFineNotifyView.as_view(), name='跨屏要对方平台处罚通知接口'),
|
path('kffkdssq_notify', PartnerFineNotifyView.as_view(), name='跨屏要对方平台处罚通知接口'),
|
||||||
|
|||||||
258
houtai/xieyi_views.py
Normal file
258
houtai/xieyi_views.py
Normal file
@@ -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}})
|
||||||
93
peizhi/migrations/0017_xcx_xieyi.py
Normal file
93
peizhi/migrations/0017_xcx_xieyi.py
Normal file
@@ -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')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -721,4 +721,76 @@ class XcxPageZiyuan(models.Model):
|
|||||||
indexes = [models.Index(fields=['ziyuan_zu'])]
|
indexes = [models.Index(fields=['ziyuan_zu'])]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.ziyuan_zu}.{self.ziyuan_key}'
|
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']),
|
||||||
|
]
|
||||||
@@ -11,6 +11,7 @@ from .xcx_config_views import (
|
|||||||
XcxSysPeizhiQueryView, XcxSysPeizhiUpdateView,
|
XcxSysPeizhiQueryView, XcxSysPeizhiUpdateView,
|
||||||
XcxPageZiyuanQueryView, XcxPageZiyuanUpdateView, XcxPageZiyuanUploadView,
|
XcxPageZiyuanQueryView, XcxPageZiyuanUpdateView, XcxPageZiyuanUploadView,
|
||||||
)
|
)
|
||||||
|
from .xieyi_views import XieyiListView, XieyiDetailView, XieyiSignStatusView, XieyiSignView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('shangpingonggao/', ShangpinGonggaoView.as_view(), name='商品公告轮播获取'),
|
path('shangpingonggao/', ShangpinGonggaoView.as_view(), name='商品公告轮播获取'),
|
||||||
@@ -52,4 +53,8 @@ urlpatterns = [
|
|||||||
path('xcxyzycx', XcxPageZiyuanQueryView.as_view(), name='管理员查询页面资源'),
|
path('xcxyzycx', XcxPageZiyuanQueryView.as_view(), name='管理员查询页面资源'),
|
||||||
path('xcxyzgx', XcxPageZiyuanUpdateView.as_view(), name='管理员更新页面资源'),
|
path('xcxyzgx', XcxPageZiyuanUpdateView.as_view(), name='管理员更新页面资源'),
|
||||||
path('xcxyzsc', XcxPageZiyuanUploadView.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='小程序签署协议'),
|
||||||
]
|
]
|
||||||
180
peizhi/xieyi_views.py
Normal file
180
peizhi/xieyi_views.py
Normal file
@@ -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'),
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -42,6 +42,8 @@ DEFAULT_ZIYUAN = {
|
|||||||
'guanshiduan.iconWithdraw': 'beijing/guanshiduan/icon-withdraw.png',
|
'guanshiduan.iconWithdraw': 'beijing/guanshiduan/icon-withdraw.png',
|
||||||
'guanshiduan.iconContact': 'beijing/guanshiduan/icon-contact.png',
|
'guanshiduan.iconContact': 'beijing/guanshiduan/icon-contact.png',
|
||||||
'guanshiduan.iconArrow': 'beijing/guanshiduan/icon-arrow.png',
|
'guanshiduan.iconArrow': 'beijing/guanshiduan/icon-arrow.png',
|
||||||
|
'haibao.guanshiBg': 'beijing/haibaobeijing.jpg',
|
||||||
|
'haibao.zuzhangBg': 'beijing/zuzhangbeijing.jpg',
|
||||||
}
|
}
|
||||||
|
|
||||||
# 后台配置页展示用中文说明(键名 pageBg 等仅供程序识别)
|
# 后台配置页展示用中文说明(键名 pageBg 等仅供程序识别)
|
||||||
@@ -82,6 +84,8 @@ ZIYUAN_MIAOSHU = {
|
|||||||
'guanshiduan.iconWithdraw': '管事端 · 提现图标',
|
'guanshiduan.iconWithdraw': '管事端 · 提现图标',
|
||||||
'guanshiduan.iconContact': '管事端 · 联系客服图标',
|
'guanshiduan.iconContact': '管事端 · 联系客服图标',
|
||||||
'guanshiduan.iconArrow': '管事端 · 箭头/跳转图标',
|
'guanshiduan.iconArrow': '管事端 · 箭头/跳转图标',
|
||||||
|
'haibao.guanshiBg': '管事推广海报 · 背景图',
|
||||||
|
'haibao.zuzhangBg': '组长推广海报 · 背景图',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -95,6 +99,7 @@ def clear_page_ziyuan_cache():
|
|||||||
f'{_CACHE_PREFIX}:paihangbang',
|
f'{_CACHE_PREFIX}:paihangbang',
|
||||||
f'{_CACHE_PREFIX}:shangjiaduan',
|
f'{_CACHE_PREFIX}:shangjiaduan',
|
||||||
f'{_CACHE_PREFIX}:guanshiduan',
|
f'{_CACHE_PREFIX}:guanshiduan',
|
||||||
|
f'{_CACHE_PREFIX}:haibao',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@@ -152,4 +157,5 @@ def build_page_assets_for_api():
|
|||||||
'shangjiaduan': get_ziyuan_group('shangjiaduan'),
|
'shangjiaduan': get_ziyuan_group('shangjiaduan'),
|
||||||
'paihangbang': get_ziyuan_group('paihangbang'),
|
'paihangbang': get_ziyuan_group('paihangbang'),
|
||||||
'guanshiduan': get_ziyuan_group('guanshiduan'),
|
'guanshiduan': get_ziyuan_group('guanshiduan'),
|
||||||
|
'haibao': get_ziyuan_group('haibao'),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user