feat: 商家罚款按后台规格定价,禁止前端自填金额

This commit is contained in:
XingQue committed 2026-08-09 11:35:12 +08:00
1 parent c1a5a3e988
commit e37ecce54c
13 files changed
+512 -56

No files matched your search

+2 -1
View File
@@ -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='客服处罚打手'), # 客服处罚打手
+2 -2
View File
@@ -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
+78 -1
View File
@@ -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'),
})
+70
View File
@@ -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',
),
),
]
+51
View File
@@ -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):
"""罚款申诉图片,关联罚单。
+151
View File
@@ -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': '已删除'}
+2
View File
@@ -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)
+2 -1
View File
@@ -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='商家罚款规格列表'),
+2 -1
View File
@@ -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",
]
+5
View File
@@ -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 '',
+119 -49
View File
@@ -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),与申诉图片严格隔离"""
+11 -1
View File
@@ -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 '',
+17
View File
@@ -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