@@ -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='跨屏要对方平台处罚通知接口'),
|
||||
|
||||
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'])]
|
||||
|
||||
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,
|
||||
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='小程序签署协议'),
|
||||
]
|
||||
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.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'),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user