From e37ecce54c276518fa13610a22e2ef24fdc86b14 Mon Sep 17 00:00:00 2001 From: XingQue Date: Sun, 9 Aug 2026 11:35:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=95=86=E5=AE=B6=E7=BD=9A=E6=AC=BE?= =?UTF-8?q?=E6=8C=89=E5=90=8E=E5=8F=B0=E8=A7=84=E6=A0=BC=E5=AE=9A=E4=BB=B7?= =?UTF-8?q?=EF=BC=8C=E7=A6=81=E6=AD=A2=E5=89=8D=E7=AB=AF=E8=87=AA=E5=A1=AB?= =?UTF-8?q?=E9=87=91=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/urls.py | 3 +- backend/view.py | 4 +- backend/views/penalties.py | 79 +++++++++++- orders/migrations/0019_penalty_spec.py | 70 +++++++++++ orders/models.py | 51 ++++++++ orders/services/penalty_spec.py | 151 ++++++++++++++++++++++ orders/services/team_recruit.py | 2 + orders/urls.py | 3 +- orders/views/__init__.py | 3 +- orders/views/merchant.py | 5 + orders/views/merchant_penalty.py | 168 +++++++++++++++++-------- users/views/shangjia.py | 12 +- utils/penalty_status.py | 17 +++ 13 files changed, 512 insertions(+), 56 deletions(-) create mode 100644 orders/migrations/0019_penalty_spec.py create mode 100644 orders/services/penalty_spec.py diff --git a/backend/urls.py b/backend/urls.py index 0e03945..c532c53 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -13,7 +13,7 @@ from .view import (GetRolePermissionView, ShopProductTypeMappingView, ProductListView, ProductModifyView, ShopCopyCatalogView, KefuChatPermissionsView, KefuImUserClubFilterView, FineApplyView, PunishDashouView, - FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView, FaKuanPingTaiShenHeView, + FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView, FaKuanPingTaiShenHeView, FaKuanGuigeManageView, HqbkxxView,BkxgView,CaiwuView,CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, KhjlglView, KhjlczView, ZxkfghdsView, DashouGiftHuiyuanSaveView, DashouGiftHuiyuanRemoveView) @@ -103,6 +103,7 @@ urlpatterns = [ path('glyclfk', FaKuanChuLiView.as_view(),name='后台罚单操作'), # 处理罚款 path('glyptshfk', FaKuanPingTaiShenHeView.as_view(), name='后台罚款平台审核'), path('htfksc', FaKuanChuangJianView.as_view(),name='后台生成罚单'), # 创建罚款 + path('htfkgg', FaKuanGuigeManageView.as_view(), name='后台罚款规格管理'), path('kffkdssq', FineApplyView.as_view(), name='客服罚款申请'), # 客服罚款申请 path('kpcf', PunishDashouView.as_view(), name='客服处罚打手'), # 客服处罚打手 diff --git a/backend/view.py b/backend/view.py index 7aa1489..a3bec8f 100644 --- a/backend/view.py +++ b/backend/view.py @@ -71,7 +71,7 @@ from .views.shops import ( ) from .views.penalties import ( FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanPingTaiShenHeView, - FaKuanChuangJianView, FineApplyView, PunishDashouView, + FaKuanChuangJianView, FineApplyView, PunishDashouView, FaKuanGuigeManageView, ) from .views.sections import ( HqbkxxView, BkxgView, @@ -129,7 +129,7 @@ __all__ = [ 'ShopCopyCatalogView', # penalties 'FaKuanTongJiView', 'FaKuanLieBiaoView', 'FaKuanChuLiView', 'FaKuanPingTaiShenHeView', - 'FaKuanChuangJianView', 'FineApplyView', 'PunishDashouView', + 'FaKuanChuangJianView', 'FineApplyView', 'PunishDashouView', 'FaKuanGuigeManageView', # sections 'HqbkxxView', 'BkxgView', # finance diff --git a/backend/views/penalties.py b/backend/views/penalties.py index 0a98a51..a27eff9 100644 --- a/backend/views/penalties.py +++ b/backend/views/penalties.py @@ -240,7 +240,9 @@ class FaKuanLieBiaoView(APIView): else: shensu_map.setdefault(fid, []).append(url) - from utils.penalty_status import penalty_display_status, is_penalty_platform_audit_pending + from utils.penalty_status import ( + penalty_display_status, is_penalty_platform_audit_pending, penalty_status_hint, + ) # 组装列表数据 results = [] @@ -254,9 +256,12 @@ class FaKuanLieBiaoView(APIView): 'shenqingren_shenfen': r.ApplicantIdentity, 'chufaliyou': r.Reason or '', 'fakuanjine': str(r.FineAmount), + 'spec_id': getattr(r, 'SpecID', None), + 'spec_name': getattr(r, 'SpecName', None) or '', 'guanliandingdan_id': r.RelatedOrderID or '', 'zhuangtai': r.Status, 'zhuangtai_text': penalty_display_status(r), + 'status_hint': penalty_status_hint(r.Status), 'platform_audit_pending': is_penalty_platform_audit_pending(r), 'yingxiang_qiangdan': r.AffectsGrabbing, 'shensuliyou': r.AppealReason or '', @@ -756,6 +761,78 @@ class PunishDashouView(APIView): return Response({'code': 500, 'msg': '系统繁忙'}) +class FaKuanGuigeManageView(APIView): + """ + 后台罚款规格 CRUD + POST /houtai/htfkgg + action: list | create | update | delete + 权限:罚款查看权限(list)/ 打手罚款处理权限或超管(写) + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + username = (request.data.get('username') or '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if not has_fadan_view_permission(permissions, username): + return Response({'code': 403, 'msg': '无权限管理罚款规格'}) + + from jituan.services.club_context import resolve_club_id_from_request + from orders.services.penalty_spec import ( + list_all_specs, create_spec, update_spec, delete_spec, + ) + club_id = resolve_club_id_from_request(request) or 'xq' + action = (request.data.get('action') or 'list').strip().lower() + + if action == 'list': + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': {'list': list_all_specs(club_id), 'club_id': club_id}, + }) + + # 写操作:需能创建打手罚单或超管 + if not check_fadan_permission(permissions, 1) and '000001' not in permissions: + return Response({'code': 403, 'msg': '无权限修改罚款规格'}) + + if action == 'create': + result = create_spec( + club_id, + request.data.get('name'), + request.data.get('amount'), + enabled=request.data.get('enabled', True), + sort_order=request.data.get('sort_order', 0), + remark=request.data.get('remark', ''), + ) + elif action == 'update': + result = update_spec( + club_id, + request.data.get('id') or request.data.get('spec_id'), + name=request.data.get('name'), + amount=request.data.get('amount'), + enabled=request.data.get('enabled'), + sort_order=request.data.get('sort_order'), + remark=request.data.get('remark'), + ) + elif action == 'delete': + result = delete_spec( + club_id, + request.data.get('id') or request.data.get('spec_id'), + ) + else: + return Response({'code': 400, 'msg': '无效的 action'}) + + if not result.get('ok'): + return Response({'code': result.get('code', 400), 'msg': result.get('msg', '失败')}) + return Response({ + 'code': 0, + 'msg': result.get('msg', 'ok'), + 'data': result.get('data'), + }) diff --git a/orders/migrations/0019_penalty_spec.py b/orders/migrations/0019_penalty_spec.py new file mode 100644 index 0000000..d54ed0a --- /dev/null +++ b/orders/migrations/0019_penalty_spec.py @@ -0,0 +1,70 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0018_refund_evidence_image'), + ] + + operations = [ + migrations.AddField( + model_name='penalty', + name='SpecID', + field=models.IntegerField( + blank=True, db_column='spec_id', db_index=True, + null=True, verbose_name='罚款规格ID', + ), + ), + migrations.AddField( + model_name='penalty', + name='SpecName', + field=models.CharField( + blank=True, db_column='spec_name', default='', + max_length=100, verbose_name='罚款规格名称快照', + ), + ), + migrations.CreateModel( + name='PenaltySpec', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('ClubID', models.CharField( + db_column='club_id', db_index=True, default='xq', + max_length=16, verbose_name='所属俱乐部', + )), + ('Name', models.CharField( + db_column='name', max_length=100, verbose_name='规格名称', + )), + ('Amount', models.DecimalField( + db_column='amount', decimal_places=2, default=0.0, + max_digits=10, verbose_name='固定金额', + )), + ('Enabled', models.BooleanField( + db_column='enabled', db_index=True, default=True, + verbose_name='是否启用', + )), + ('SortOrder', models.IntegerField( + db_column='sort_order', db_index=True, default=0, + verbose_name='排序(小在前)', + )), + ('Remark', models.CharField( + blank=True, db_column='remark', default='', + max_length=200, verbose_name='备注', + )), + ('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '罚款规格', + 'verbose_name_plural': '罚款规格', + 'db_table': 'fadan_guige', + }, + ), + migrations.AddIndex( + model_name='penaltyspec', + index=models.Index( + fields=['ClubID', 'Enabled', 'SortOrder'], + name='fadan_guige_club_en_idx', + ), + ), + ] diff --git a/orders/models.py b/orders/models.py index da341f1..10d55e2 100644 --- a/orders/models.py +++ b/orders/models.py @@ -548,6 +548,14 @@ class Penalty(QModel): max_length=20, blank=True, null=True, db_column='dianpu_kefu_id', verbose_name='店铺客服用户ID', ) + SpecID = models.IntegerField( + blank=True, null=True, db_index=True, + db_column='spec_id', verbose_name='罚款规格ID', + ) + SpecName = models.CharField( + max_length=100, blank=True, default='', + db_column='spec_name', verbose_name='罚款规格名称快照', + ) class Meta: db_table = 'fadan' @@ -562,6 +570,49 @@ class Penalty(QModel): return f"{self.PenalizedUserID} - 罚款{self.FineAmount}元 - 状态{self.Status}" +class PenaltySpec(QModel): + """商家罚款规格(后台定价,商家仅可选择不可手填金额)""" + + id = models.AutoField(primary_key=True) + ClubID = models.CharField( + max_length=16, default='xq', db_index=True, + db_column='club_id', verbose_name='所属俱乐部', + ) + Name = models.CharField( + max_length=100, + db_column='name', verbose_name='规格名称', + ) + Amount = models.DecimalField( + max_digits=10, decimal_places=2, default=0.00, + db_column='amount', verbose_name='固定金额', + ) + Enabled = models.BooleanField( + default=True, db_index=True, + db_column='enabled', verbose_name='是否启用', + ) + SortOrder = models.IntegerField( + default=0, db_index=True, + db_column='sort_order', verbose_name='排序(小在前)', + ) + Remark = models.CharField( + max_length=200, blank=True, default='', + db_column='remark', verbose_name='备注', + ) + CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'fadan_guige' + verbose_name = '罚款规格' + verbose_name_plural = '罚款规格' + indexes = [ + models.Index(fields=['ClubID', 'Enabled', 'SortOrder'], name='fadan_guige_club_en_idx'), + ] + + def __str__(self): + return f'{self.Name} ¥{self.Amount} ({self.ClubID})' + + class PenaltyAppealImage(QModel): """罚款申诉图片,关联罚单。 diff --git a/orders/services/penalty_spec.py b/orders/services/penalty_spec.py new file mode 100644 index 0000000..e73cc1c --- /dev/null +++ b/orders/services/penalty_spec.py @@ -0,0 +1,151 @@ +"""商家罚款规格:解析、序列化、后台维护辅助。""" +from decimal import Decimal, InvalidOperation + +from orders.models import PenaltySpec + + +def serialize_spec(spec): + return { + 'id': spec.id, + 'club_id': spec.ClubID or '', + 'name': spec.Name or '', + 'amount': float(spec.Amount or 0), + 'amount_str': f'{Decimal(str(spec.Amount or 0)):.2f}', + 'enabled': bool(spec.Enabled), + 'sort_order': int(spec.SortOrder or 0), + 'remark': spec.Remark or '', + 'create_time': spec.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if spec.CreateTime else '', + 'update_time': spec.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if spec.UpdateTime else '', + } + + +def list_enabled_specs(club_id): + club_id = (club_id or '').strip() or 'xq' + qs = ( + PenaltySpec.objects + .filter(ClubID=club_id, Enabled=True) + .order_by('SortOrder', 'id') + ) + return [serialize_spec(s) for s in qs] + + +def list_all_specs(club_id): + club_id = (club_id or '').strip() or 'xq' + qs = ( + PenaltySpec.objects + .filter(ClubID=club_id) + .order_by('SortOrder', 'id') + ) + return [serialize_spec(s) for s in qs] + + +def resolve_enabled_spec(club_id, spec_id): + """ + 商家申请用:必须同俱乐部且启用。 + 成功返回 {'ok': True, 'spec': PenaltySpec, 'amount': Decimal, 'name': str} + """ + club_id = (club_id or '').strip() or 'xq' + if spec_id is None or spec_id == '': + return {'ok': False, 'code': 2, 'msg': '请选择罚款规格'} + try: + sid = int(spec_id) + except (TypeError, ValueError): + return {'ok': False, 'code': 2, 'msg': '罚款规格无效'} + try: + spec = PenaltySpec.objects.get(id=sid) + except PenaltySpec.DoesNotExist: + return {'ok': False, 'code': 2, 'msg': '罚款规格不存在'} + if (spec.ClubID or '').strip() != club_id: + return {'ok': False, 'code': 2, 'msg': '罚款规格不属于当前俱乐部'} + if not spec.Enabled: + return {'ok': False, 'code': 2, 'msg': '该罚款规格已停用,请重新选择'} + amount = Decimal(str(spec.Amount or 0)) + if amount <= 0: + return {'ok': False, 'code': 2, 'msg': '罚款规格金额异常,请联系客服'} + return { + 'ok': True, + 'spec': spec, + 'amount': amount, + 'name': (spec.Name or '').strip()[:100], + 'spec_id': spec.id, + } + + +def create_spec(club_id, name, amount, *, enabled=True, sort_order=0, remark=''): + club_id = (club_id or '').strip() or 'xq' + name = (name or '').strip() + if not name: + return {'ok': False, 'code': 400, 'msg': '规格名称不能为空'} + if len(name) > 100: + return {'ok': False, 'code': 400, 'msg': '规格名称最多100字'} + try: + amt = Decimal(str(amount)) + if amt <= 0: + raise InvalidOperation + except (InvalidOperation, TypeError, ValueError): + return {'ok': False, 'code': 400, 'msg': '金额必须为正数'} + try: + sort_order = int(sort_order or 0) + except (TypeError, ValueError): + sort_order = 0 + spec = PenaltySpec.objects.create( + ClubID=club_id[:16], + Name=name, + Amount=amt, + Enabled=bool(enabled), + SortOrder=sort_order, + Remark=(remark or '').strip()[:200], + ) + return {'ok': True, 'code': 0, 'msg': '已创建', 'data': serialize_spec(spec)} + + +def update_spec(club_id, spec_id, *, name=None, amount=None, enabled=None, sort_order=None, remark=None): + club_id = (club_id or '').strip() or 'xq' + try: + sid = int(spec_id) + except (TypeError, ValueError): + return {'ok': False, 'code': 400, 'msg': '规格ID无效'} + try: + spec = PenaltySpec.objects.get(id=sid, ClubID=club_id) + except PenaltySpec.DoesNotExist: + return {'ok': False, 'code': 404, 'msg': '规格不存在'} + + if name is not None: + name = str(name).strip() + if not name: + return {'ok': False, 'code': 400, 'msg': '规格名称不能为空'} + if len(name) > 100: + return {'ok': False, 'code': 400, 'msg': '规格名称最多100字'} + spec.Name = name + if amount is not None: + try: + amt = Decimal(str(amount)) + if amt <= 0: + raise InvalidOperation + except (InvalidOperation, TypeError, ValueError): + return {'ok': False, 'code': 400, 'msg': '金额必须为正数'} + spec.Amount = amt + if enabled is not None: + spec.Enabled = bool(enabled) + if sort_order is not None: + try: + spec.SortOrder = int(sort_order) + except (TypeError, ValueError): + return {'ok': False, 'code': 400, 'msg': '排序须为整数'} + if remark is not None: + spec.Remark = str(remark).strip()[:200] + spec.save() + return {'ok': True, 'code': 0, 'msg': '已更新', 'data': serialize_spec(spec)} + + +def delete_spec(club_id, spec_id): + """物理删除规格;已生成罚单仍保留快照,不受影响。""" + club_id = (club_id or '').strip() or 'xq' + try: + sid = int(spec_id) + except (TypeError, ValueError): + return {'ok': False, 'code': 400, 'msg': '规格ID无效'} + deleted, _ = PenaltySpec.objects.filter(id=sid, ClubID=club_id).delete() + if not deleted: + return {'ok': False, 'code': 404, 'msg': '规格不存在'} + return {'ok': True, 'code': 0, 'msg': '已删除'} diff --git a/orders/services/team_recruit.py b/orders/services/team_recruit.py index 36d81a8..426f6b4 100644 --- a/orders/services/team_recruit.py +++ b/orders/services/team_recruit.py @@ -849,6 +849,7 @@ def maybe_split_penalties(*, order_id: str, total_amount, reason: str, applicant except Exception: order_club = '' cid = (order_club or club_id or rec.club_id or 'xq')[:16] + extras = dict(extra_fields or {}) for i, m in enumerate(members): p = Penalty( PenalizedUserID=m.user_uid, @@ -861,6 +862,7 @@ def maybe_split_penalties(*, order_id: str, total_amount, reason: str, applicant AffectsGrabbing=affects_grabbing, ApplicantIdentity=applicant_identity, ClubID=cid, + **extras, ) p.save() created.append(p) diff --git a/orders/urls.py b/orders/urls.py index 5dccd07..ee57fad 100644 --- a/orders/urls.py +++ b/orders/urls.py @@ -9,7 +9,7 @@ from .views import CreateOrderView, WechatPayNotifyView, AlipayPayNotifyView, al AdTongYiTuiKuanPingTai, AdTongYiTuiKuanShangJia, AdQiangZhiJieDan, AdJuJueTuiKuan,\ AdGengHuanDaShou,ShangjiaCOSZhengshuView, DashouHuoquLeixingView,AdJuJueJieSuan, AdZhuanYiDaTing, AdQuXiaoZhiDing,DashouXiugaiView,DaiLiQunLiaoXiaoXiView,\ ShangjiaLianjieLiuYanZhuanFaView, DingdanXiangqingView2, JiedanView2, ZxsjghdsView, \ - ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView, ShangjiaShujuTongjiView, LianTongDuiHuaZhunBeiView, \ + ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView, ShangjiaFakuanSpecListView, ShangjiaShujuTongjiView, LianTongDuiHuaZhunBeiView, \ LianTongPeiDuiDingDanLieBiaoView, LianTongDingDanZhuangTaiView, LianTongDingDanZhuangTaiPiLiangView, \ ZhiDingChaxunView, ZhiDingHuiFuView, H5ChatSendImageView from jituan.services.fubei_notify import FubeiPayNotifyView @@ -94,6 +94,7 @@ urlpatterns = [ path('zxsjghds', ZxsjghdsView.as_view(), name='商家更换打手'), # 商家更换打手 path('sjfksq', ShangjiaFakuanApplyView.as_view(), name='商家罚款'), path('sjfkxgycx', ShangjiaFakuaiXiugaiView.as_view(), name='商家罚款和退款修改'), + path('sjfkgg', ShangjiaFakuanSpecListView.as_view(), name='商家罚款规格列表'), diff --git a/orders/views/__init__.py b/orders/views/__init__.py index 503cc05..468497d 100644 --- a/orders/views/__init__.py +++ b/orders/views/__init__.py @@ -69,6 +69,7 @@ from .admin import ( from .merchant_penalty import ( ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView, + ShangjiaFakuanSpecListView, ) __all__ = [ @@ -94,5 +95,5 @@ __all__ = [ "AdJuJueTuiKuan", "AdTongYiTuiKuanShangJia", "AdTongYiTuiKuanPingTai", "AdJuJueJieSuan", "AdZhuanYiDaTing", "AdQuXiaoZhiDing", "ZhiDingChaxunView", "ZhiDingHuiFuView", "ZxsjghdsView", - "ShangjiaFakuanApplyView", "ShangjiaFakuaiXiugaiView", + "ShangjiaFakuanApplyView", "ShangjiaFakuaiXiugaiView", "ShangjiaFakuanSpecListView", ] diff --git a/orders/views/merchant.py b/orders/views/merchant.py index eefccf9..5fc9e2f 100644 --- a/orders/views/merchant.py +++ b/orders/views/merchant.py @@ -700,12 +700,17 @@ class ShangjiaDingdanXiangqingView(APIView): for t in PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=2) ] reason = (fadan_obj.Reason or '').replace('【组队均摊】', '', 1) + from utils.penalty_status import penalty_status_hint, penalty_display_status fadan_dict = { 'id': fadan_obj.id, 'chufaliyou': reason, 'fakuanjine': total_fine, + 'spec_id': getattr(fadan_obj, 'SpecID', None), + 'spec_name': getattr(fadan_obj, 'SpecName', None) or '', 'yingxiang_qiangdan': fadan_obj.AffectsGrabbing, 'zhuangtai': fadan_obj.Status, + 'zhuangtai_text': penalty_display_status(fadan_obj), + 'status_hint': penalty_status_hint(fadan_obj.Status), 'shensuliyou': fadan_obj.AppealReason or '', 'tongyi_chufaliyou': fadan_obj.ApproveReason or '', 'bohuiliyou': fadan_obj.RejectReason or '', diff --git a/orders/views/merchant_penalty.py b/orders/views/merchant_penalty.py index 8c84dbb..bc8086a 100644 --- a/orders/views/merchant_penalty.py +++ b/orders/views/merchant_penalty.py @@ -77,6 +77,7 @@ from ..models import ( PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate, CommissionRate, PlayerRating, RefundRecord, ) +from orders.services.penalty_spec import resolve_enabled_spec, list_enabled_specs from jituan.services.club_config import get_commission_rate, get_commission_rate_object from jituan.services.club_context import resolve_club_id_from_request from jituan.services.club_user import get_payment_openid @@ -99,6 +100,23 @@ logger = logging.getLogger(__name__) # 文件路径:dingdan/views.py 或单独文件,根据项目结构放置 +class ShangjiaFakuanSpecListView(APIView): + """ + 商家端拉取启用中的罚款规格 + POST /dingdan/sjfkgg + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + club_id = resolve_club_id_from_request(request) or 'xq' + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': {'list': list_enabled_specs(club_id), 'club_id': club_id}, + }) + + class ShangjiaFakuanApplyView(APIView): """ 商家罚款申请接口 @@ -106,13 +124,10 @@ class ShangjiaFakuanApplyView(APIView): 权限:JWT认证的商家用户 参数: dingdan_id - 订单ID (必填) - chufa_liyou - 罚款原因 (必填) - fakuanjine - 罚款金额,元 (必填) + chufa_liyou - 补充说明 (必填) + spec_id - 罚款规格ID (必填,金额以后台规格为准,忽略 fakuanjine) yingxiang_qiangdan - 是否影响抢单 1是 0否 (必填) - 说明: - 1. 自动从订单中获取被打手ID,防止商家篡改目标。 - 2. 同一订单同一打手不允许重复罚款。 - 3. 跨平台订单若为我方派单,则通知对方平台处罚;否则本地生成罚单。 + evidence_urls - 证据图片 (至少1张) """ permission_classes = [IsAuthenticated] @@ -129,11 +144,11 @@ class ShangjiaFakuanApplyView(APIView): # ========== 2. 参数提取与校验 ========== dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip() chufa_liyou = request.data.get('chufa_liyou', '').strip() - fakuanjine = request.data.get('fakuanjine', 0) + spec_id = request.data.get('spec_id') yingxiang_qiangdan = int(request.data.get('yingxiang_qiangdan', 1)) evidence_urls = request.data.get('evidence_urls') or [] - if not dingdan_id or not chufa_liyou or not fakuanjine: + if not dingdan_id or not chufa_liyou: return Response({'code': 2, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) if not isinstance(evidence_urls, list): @@ -144,13 +159,6 @@ class ShangjiaFakuanApplyView(APIView): if len(evidence_urls) < 1: return Response({'code': 2, 'msg': '请至少上传1张证据图片'}, status=status.HTTP_400_BAD_REQUEST) - try: - fakuanjine = float(fakuanjine) - if fakuanjine <= 0: - raise ValueError - except (ValueError, TypeError): - return Response({'code': 2, 'msg': '罚款金额格式错误'}, status=status.HTTP_400_BAD_REQUEST) - # ========== 3. 获取订单,校验归属商家 ========== try: # 关联查询商家扩展表,确保订单属于当前商家 @@ -176,7 +184,25 @@ class ShangjiaFakuanApplyView(APIView): elif Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=dingdan_id).exists(): return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST) - # ========== 6. 本地创建罚单 ========== + # ========== 6. 规格定价(不信前端金额) ========== + club_for_p = resolve_penalty_club_id( + order=order, + penalized_user_id=dashou_id, + request=request, + applicant_id=shangjia_id, + user=current_user, + ) + spec_res = resolve_enabled_spec(club_for_p, spec_id) + if not spec_res.get('ok'): + return Response( + {'code': spec_res.get('code', 2), 'msg': spec_res.get('msg', '规格无效')}, + status=status.HTTP_400_BAD_REQUEST, + ) + fakuanjine = float(spec_res['amount']) + spec_name = spec_res['name'] + resolved_spec_id = spec_res['spec_id'] + + # ========== 7. 本地创建罚单 ========== staff_kefu_id = None staff_member = getattr(request, '_staff_member', None) if staff_member is not None: @@ -187,13 +213,6 @@ class ShangjiaFakuanApplyView(APIView): with transaction.atomic(): # 组队均摊:有效组队则每人一张罚单 from orders.services.team_recruit import maybe_split_penalties - club_for_p = resolve_penalty_club_id( - order=order, - penalized_user_id=dashou_id, - request=request, - applicant_id=shangjia_id, - user=current_user, - ) split = maybe_split_penalties( order_id=dingdan_id, total_amount=fakuanjine, @@ -202,6 +221,11 @@ class ShangjiaFakuanApplyView(APIView): applicant_identity=5, affects_grabbing=yingxiang_qiangdan, club_id=club_for_p, + extra_fields={ + 'SpecID': resolved_spec_id, + 'SpecName': spec_name, + 'ShopStaffUserID': staff_kefu_id, + }, ) if split: # 均摊单默认待缴纳;若业务需审核,统一改状态 @@ -221,6 +245,8 @@ class ShangjiaFakuanApplyView(APIView): ApplicantIdentity=5, # 申请人是商家 ClubID=club_for_p, ShopStaffUserID=staff_kefu_id, + SpecID=resolved_spec_id, + SpecName=spec_name, ) PenaltyAppealImage.objects.bulk_create([ PenaltyAppealImage( @@ -247,7 +273,10 @@ class ShangjiaFakuanApplyView(APIView): lock_penalty_bonus(p) else: lock_penalty_bonus(fadan) - logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}提交罚款申请(平台审核),金额{fakuanjine}") + logger.info( + '商家%s对订单%s打手%s提交罚款申请(平台审核) 规格=%s(%s) 金额%s', + shangjia_id, dingdan_id, dashou_id, resolved_spec_id, spec_name, fakuanjine, + ) record_order_action_from_request( request, ACTION_PENALTY_APPLY, @@ -255,9 +284,18 @@ class ShangjiaFakuanApplyView(APIView): merchant_id=shangjia_id, amount=fakuanjine, leixing_id=leixing_id_from_order(order), - remark=chufa_liyou[:200] if chufa_liyou else '', + remark=f'{spec_name}|{chufa_liyou}'[:200] if chufa_liyou else spec_name[:200], ) - return Response({'code': 0, 'msg': '罚款申请已提交,等待平台审核', 'data': {'fadan_id': fadan.id}}) + return Response({ + 'code': 0, + 'msg': '罚款申请已提交,等待平台审核', + 'data': { + 'fadan_id': fadan.id, + 'spec_id': resolved_spec_id, + 'spec_name': spec_name, + 'fakuanjine': fakuanjine, + }, + }) class ShangjiaFakuaiXiugaiView(APIView): @@ -326,12 +364,18 @@ class ShangjiaFakuaiXiugaiView(APIView): return Response({'code': 403, 'msg': '罚单与订单不匹配'}, status=status.HTTP_403_FORBIDDEN) if operation == 'modify_penalty': - resp = self._modify_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine'), user.UserUID) + resp = self._modify_penalty( + fadan, + request.data.get('liyou', ''), + request.data.get('spec_id'), + user.UserUID, + club_id=getattr(fadan, 'ClubID', None) or resolve_club_id_from_request(request), + ) if getattr(resp, 'data', {}).get('code') == 0: record_order_action_from_request( request, ACTION_PENALTY_MODIFY, dingdan_id, merchant_id=user.UserUID, - amount=request.data.get('jine') or getattr(fadan, 'FineAmount', None), + amount=getattr(fadan, 'FineAmount', None), leixing_id=leixing_id_from_order(dingdan), remark=str(request.data.get('liyou') or '')[:200], ) @@ -348,12 +392,19 @@ class ShangjiaFakuaiXiugaiView(APIView): ) return resp elif operation == 'resubmit_penalty': - resp = self._resubmit_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine'), user.UserUID) + resp = self._resubmit_penalty( + fadan, + request.data.get('liyou', ''), + request.data.get('spec_id'), + user.UserUID, + club_id=getattr(fadan, 'ClubID', None) or resolve_club_id_from_request(request), + ) if getattr(resp, 'data', {}).get('code') == 0: + fadan.refresh_from_db() record_order_action_from_request( request, ACTION_PENALTY_RESUBMIT, dingdan_id, merchant_id=user.UserUID, - amount=request.data.get('jine') or getattr(fadan, 'FineAmount', None), + amount=getattr(fadan, 'FineAmount', None), leixing_id=leixing_id_from_order(dingdan), remark=str(request.data.get('liyou') or '')[:200], ) @@ -397,8 +448,8 @@ class ShangjiaFakuaiXiugaiView(APIView): return sibs return [fadan] - def _modify_penalty(self, fadan, liyou, jine_str, processor_id=None): - """修改罚款(待缴纳 / 申诉中 / 平台审核中);组队均摊联动改总额并重分。""" + def _modify_penalty(self, fadan, liyou, spec_id, processor_id=None, club_id=None): + """修改罚款(待缴纳 / 申诉中 / 平台审核中);按规格取价,组队均摊联动重分。""" from utils.penalty_status import PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT from orders.services.team_recruit import _d, MONEY from decimal import ROUND_DOWN @@ -406,12 +457,13 @@ class ShangjiaFakuaiXiugaiView(APIView): return Response({'code': 400, 'msg': '当前罚单状态不可修改'}) if not liyou.strip(): return Response({'code': 400, 'msg': '罚款理由不能为空'}) - try: - jine = Decimal(jine_str) - if jine <= 0: - raise ValueError - except Exception: - return Response({'code': 400, 'msg': '罚款金额必须为正数'}) + club_id = (club_id or getattr(fadan, 'ClubID', None) or 'xq').strip() + spec_res = resolve_enabled_spec(club_id, spec_id) + if not spec_res.get('ok'): + return Response({'code': 400, 'msg': spec_res.get('msg', '规格无效')}) + jine = spec_res['amount'] + spec_name = spec_res['name'] + resolved_spec_id = spec_res['spec_id'] siblings = self._team_split_siblings(fadan, include_rejected=False) for s in siblings: if s.Status not in (PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT): @@ -434,6 +486,8 @@ class ShangjiaFakuaiXiugaiView(APIView): for i, s in enumerate(siblings): s.Reason = reason_text s.FineAmount = amounts[i] + s.SpecID = resolved_spec_id + s.SpecName = spec_name if processor_id: s.ProcessorID = processor_id s.ProcessorIdentity = 5 @@ -442,12 +496,18 @@ class ShangjiaFakuaiXiugaiView(APIView): else: fadan.Reason = reason_text fadan.FineAmount = jine + fadan.SpecID = resolved_spec_id + fadan.SpecName = spec_name if processor_id: fadan.ProcessorID = processor_id fadan.ProcessorIdentity = 5 fadan.save() lock_penalty_bonus(fadan) - return Response({'code': 0, 'msg': '罚款修改成功'}) + return Response({ + 'code': 0, + 'msg': '罚款修改成功', + 'data': {'spec_id': resolved_spec_id, 'spec_name': spec_name, 'fakuanjine': float(jine)}, + }) def _cancel_penalty(self, fadan, liyou, processor_id=None): """撤销罚款;组队均摊一并驳回,避免只撤一张仍挡抢单。""" @@ -469,8 +529,8 @@ class ShangjiaFakuaiXiugaiView(APIView): s.save() return Response({'code': 0, 'msg': '罚款已撤销(驳回)'}) - def _resubmit_penalty(self, fadan, liyou, jine_str, processor_id=None): - """再次申请罚款;组队均摊联动总额重分。""" + def _resubmit_penalty(self, fadan, liyou, spec_id, processor_id=None, club_id=None): + """再次申请罚款;按规格取价,组队均摊联动总额重分。""" from utils.penalty_status import PENALTY_REJECTED, PENALTY_PLATFORM_AUDIT from orders.services.team_recruit import _d, MONEY from decimal import ROUND_DOWN @@ -478,12 +538,13 @@ class ShangjiaFakuaiXiugaiView(APIView): return Response({'code': 400, 'msg': '只有已驳回的罚单可再次申请'}) if not liyou.strip(): return Response({'code': 400, 'msg': '罚款理由不能为空'}) - try: - jine = Decimal(jine_str) - if jine <= 0: - raise ValueError - except Exception: - return Response({'code': 400, 'msg': '罚款金额必须为正数'}) + club_id = (club_id or getattr(fadan, 'ClubID', None) or 'xq').strip() + spec_res = resolve_enabled_spec(club_id, spec_id) + if not spec_res.get('ok'): + return Response({'code': 400, 'msg': spec_res.get('msg', '规格无效')}) + jine = spec_res['amount'] + spec_name = spec_res['name'] + resolved_spec_id = spec_res['spec_id'] siblings = self._team_split_siblings(fadan, include_rejected=True) siblings = [s for s in siblings if s.Status == PENALTY_REJECTED] or [fadan] reason_text = liyou[:500] @@ -502,14 +563,23 @@ class ShangjiaFakuaiXiugaiView(APIView): s.Status = PENALTY_PLATFORM_AUDIT s.Reason = reason_text s.FineAmount = amounts[i] if i < len(amounts) else amounts[-1] + s.SpecID = resolved_spec_id + s.SpecName = spec_name s.RejectReason = '' if processor_id: s.ProcessorID = processor_id s.ProcessorIdentity = 5 s.save() lock_penalty_bonus(s) - logger.info(f"商家重新申请组队罚单 order={fadan.RelatedOrderID} 总额={jine} n={k}") - return Response({'code': 0, 'msg': '已重新申请罚款'}) + logger.info( + '商家重新申请组队罚单 order=%s 规格=%s(%s) 总额=%s n=%s', + fadan.RelatedOrderID, resolved_spec_id, spec_name, jine, k, + ) + return Response({ + 'code': 0, + 'msg': '已重新申请罚款', + 'data': {'spec_id': resolved_spec_id, 'spec_name': spec_name, 'fakuanjine': float(jine)}, + }) def _manage_penalty_evidence(self, fadan, data): """管理罚单证据图片(Purpose=1),与申诉图片严格隔离""" diff --git a/users/views/shangjia.py b/users/views/shangjia.py index 15268d1..5deaaba 100644 --- a/users/views/shangjia.py +++ b/users/views/shangjia.py @@ -74,7 +74,7 @@ from ..tixian_shenhe_services import ( check_dashou_trial_blocks_commission_withdraw, WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING, ) -from ..tixian_shenhe_views import process_audit_collect +from ..tixian_shenhe_views import process_audit_collect from .chufa import DashouPermission from orders.models import ( @@ -417,13 +417,18 @@ class DaShouFaKuanLieBiaoView(APIView): chufa_list = [] for r in records: rid = r.id + from utils.penalty_status import penalty_status_hint, penalty_display_status item = { 'id': rid, 'beichufa_id': r.PenalizedUserID, 'guanliandingdan_id': r.RelatedOrderID or '', 'chufaliyou': r.Reason or '', 'fakuanjine': str(r.FineAmount), + 'spec_id': getattr(r, 'SpecID', None), + 'spec_name': getattr(r, 'SpecName', None) or '', 'zhuangtai': r.Status, + 'zhuangtai_text': penalty_display_status(r), + 'status_hint': penalty_status_hint(r.Status), 'yingxiang_qiangdan': r.AffectsGrabbing, 'shensuliyou': r.AppealReason or '', 'bohuiliyou': r.RejectReason or '', @@ -623,6 +628,7 @@ class ShangjiaFakuanLieBiaoView(APIView): else: shensu_map.setdefault(fid, []).append(img) + from utils.penalty_status import penalty_status_hint, penalty_display_status chufa_list = [] for r in records: rid = r.id @@ -632,7 +638,11 @@ class ShangjiaFakuanLieBiaoView(APIView): 'guanliandingdan_id': r.RelatedOrderID or '', 'chufaliyou': r.Reason or '', 'fakuanjine': str(r.FineAmount), + 'spec_id': getattr(r, 'SpecID', None), + 'spec_name': getattr(r, 'SpecName', None) or '', 'zhuangtai': r.Status, + 'zhuangtai_text': penalty_display_status(r), + 'status_hint': penalty_status_hint(r.Status), 'yingxiang_qiangdan': r.AffectsGrabbing, 'shensuliyou': r.AppealReason or '', 'bohuiliyou': r.RejectReason or '', diff --git a/utils/penalty_status.py b/utils/penalty_status.py index e22f182..69a6c54 100644 --- a/utils/penalty_status.py +++ b/utils/penalty_status.py @@ -20,10 +20,27 @@ PENALTY_STATUS_LABELS = { 5: '平台审核中', } +# 售后/两端下一步提示(商家、打手、客服通用) +PENALTY_STATUS_HINTS = { + 1: '打手待缴纳;打手可缴纳或申诉,商家待缴期间可修改/撤销', + 2: '已缴纳完成,分红已按规则处理', + 3: '打手已申诉,等待客服处理;商家申诉期间可修改/撤销', + 4: '已驳回或已撤销;商家可再次申请', + 5: '平台审核中,请耐心等待;勿重复提交,商家可修改/撤销', +} + # 商家身份(ApplicantIdentity) APPLICANT_MERCHANT = 5 +def penalty_status_hint(status): + try: + status = int(status) + except (TypeError, ValueError): + return '' + return PENALTY_STATUS_HINTS.get(status, '') + + def penalty_platform_audit_pending_q(): """待平台审核:仅 zhuangtai=5(商家新流程提交)。""" from django.db.models import Q