feat: 商家罚款按后台规格定价,禁止前端自填金额
This commit is contained in:
1 parent
c1a5a3e988
commit
e37ecce54c
13 files changed
+512
-56
No files matched your search
+2
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
"""罚款申诉图片,关联罚单。
|
||||
|
||||
|
||||
@@ -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': '已删除'}
|
||||
@@ -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
@@ -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='商家罚款规格列表'),
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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 '',
|
||||
|
||||
@@ -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
@@ -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 '',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user