feat: 拒绝退款可配置比例结算(full×ratio、组队/管事、Status6)
This commit is contained in:
1 parent
e37ecce54c
commit
26848972ab
13 files changed
+996
-206
No files matched your search
@@ -0,0 +1,23 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('jituan', '0026_dispatch_block_and_merchant_whitelist'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cluborderdealconfig',
|
||||||
|
name='default_partial_settle_ratio',
|
||||||
|
field=models.DecimalField(
|
||||||
|
decimal_places=4,
|
||||||
|
default=Decimal('0.5000'),
|
||||||
|
max_digits=5,
|
||||||
|
verbose_name='拒绝退款默认结算比例',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -700,6 +700,11 @@ class ClubOrderDealConfig(QModel):
|
|||||||
dispatch_block_hours = models.PositiveIntegerField(default=24, verbose_name='禁派单超时小时')
|
dispatch_block_hours = models.PositiveIntegerField(default=24, verbose_name='禁派单超时小时')
|
||||||
stats_enabled = models.BooleanField(default=False, verbose_name='成交指标统计开关')
|
stats_enabled = models.BooleanField(default=False, verbose_name='成交指标统计开关')
|
||||||
stats_since = models.DateTimeField(null=True, blank=True, verbose_name='指标统计起点')
|
stats_since = models.DateTimeField(null=True, blank=True, verbose_name='指标统计起点')
|
||||||
|
# 拒绝退款时客服审核默认比例(可改本次值);0.01~1.00,默认 0.5
|
||||||
|
default_partial_settle_ratio = models.DecimalField(
|
||||||
|
max_digits=5, decimal_places=4, default=Decimal('0.5000'),
|
||||||
|
verbose_name='拒绝退款默认结算比例',
|
||||||
|
)
|
||||||
updated_by = models.CharField(max_length=32, blank=True, default='')
|
updated_by = models.CharField(max_length=32, blank=True, default='')
|
||||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||||
UpdateTime = models.DateTimeField(auto_now=True)
|
UpdateTime = models.DateTimeField(auto_now=True)
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ def order_auto_settle_payload(order) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def config_to_dict(row) -> dict:
|
def config_to_dict(row) -> dict:
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
default_ratio = '0.5000'
|
||||||
if not row:
|
if not row:
|
||||||
return {
|
return {
|
||||||
'club_id': '',
|
'club_id': '',
|
||||||
@@ -99,7 +102,14 @@ def config_to_dict(row) -> dict:
|
|||||||
'dispatch_block_hours': 24,
|
'dispatch_block_hours': 24,
|
||||||
'stats_enabled': False,
|
'stats_enabled': False,
|
||||||
'stats_since': '',
|
'stats_since': '',
|
||||||
|
'default_partial_settle_ratio': default_ratio,
|
||||||
|
'default_partial_settle_percent': 50,
|
||||||
}
|
}
|
||||||
|
ratio = getattr(row, 'default_partial_settle_ratio', None)
|
||||||
|
try:
|
||||||
|
ratio_d = Decimal(str(ratio if ratio is not None else '0.5')).quantize(Decimal('0.0001'))
|
||||||
|
except Exception:
|
||||||
|
ratio_d = Decimal('0.5000')
|
||||||
return {
|
return {
|
||||||
'club_id': row.club_id,
|
'club_id': row.club_id,
|
||||||
'auto_settle_enabled': bool(row.auto_settle_enabled),
|
'auto_settle_enabled': bool(row.auto_settle_enabled),
|
||||||
@@ -110,6 +120,8 @@ def config_to_dict(row) -> dict:
|
|||||||
'dispatch_block_hours': int(getattr(row, 'dispatch_block_hours', None) or 24),
|
'dispatch_block_hours': int(getattr(row, 'dispatch_block_hours', None) or 24),
|
||||||
'stats_enabled': bool(row.stats_enabled),
|
'stats_enabled': bool(row.stats_enabled),
|
||||||
'stats_since': row.stats_since.isoformat() if row.stats_since else '',
|
'stats_since': row.stats_since.isoformat() if row.stats_since else '',
|
||||||
|
'default_partial_settle_ratio': str(ratio_d),
|
||||||
|
'default_partial_settle_percent': int((ratio_d * 100).quantize(Decimal('1'))),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -337,6 +349,21 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict:
|
|||||||
if enabled and was_stats and data.get('reset_stats_since'):
|
if enabled and was_stats and data.get('reset_stats_since'):
|
||||||
row.stats_since = now
|
row.stats_since = now
|
||||||
|
|
||||||
|
if 'default_partial_settle_ratio' in data and data['default_partial_settle_ratio'] is not None:
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
|
raw = data['default_partial_settle_ratio']
|
||||||
|
# 兼容前端传百分比 10~100
|
||||||
|
try:
|
||||||
|
val = Decimal(str(raw).strip())
|
||||||
|
except (InvalidOperation, ValueError, TypeError) as e:
|
||||||
|
raise ValueError('默认比例结算比例无效') from e
|
||||||
|
if val > 1:
|
||||||
|
val = val / Decimal('100')
|
||||||
|
val = val.quantize(Decimal('0.0001'))
|
||||||
|
if val < Decimal('0.01') or val > Decimal('1'):
|
||||||
|
raise ValueError('默认比例结算比例须在 1%~100%')
|
||||||
|
row.default_partial_settle_ratio = val
|
||||||
|
|
||||||
# 若刚打开统计且 since 空
|
# 若刚打开统计且 since 空
|
||||||
if row.stats_enabled and not row.stats_since:
|
if row.stats_enabled and not row.stats_since:
|
||||||
row.stats_since = _default_stats_since()
|
row.stats_since = _default_stats_since()
|
||||||
@@ -378,6 +405,12 @@ def apply_enter_status_8(instance):
|
|||||||
instance.PendingDispatch = False
|
instance.PendingDispatch = False
|
||||||
instance.AutoTaskID = ''
|
instance.AutoTaskID = ''
|
||||||
|
|
||||||
|
# 拒绝退款锁定比例结算:禁止 Celery 按全额自动结
|
||||||
|
if int(getattr(instance, 'PartialSettleStatus', 0) or 0) in (1, 2):
|
||||||
|
instance.AutoSettleEligible = False
|
||||||
|
instance.AutoExpireAt = None
|
||||||
|
return
|
||||||
|
|
||||||
club_id = (getattr(instance, 'ClubID', None) or '').strip()
|
club_id = (getattr(instance, 'ClubID', None) or '').strip()
|
||||||
cfg = get_or_create_deal_config(club_id) if club_id else None
|
cfg = get_or_create_deal_config(club_id) if club_id else None
|
||||||
enabled_at = _db_datetime(getattr(cfg, 'auto_settle_enabled_at', None)) if cfg else None
|
enabled_at = _db_datetime(getattr(cfg, 'auto_settle_enabled_at', None)) if cfg else None
|
||||||
@@ -422,6 +455,11 @@ def mark_order_completed_unified(order, *, source='auto_settle', operator=''):
|
|||||||
return False, '订单不存在'
|
return False, '订单不存在'
|
||||||
if locked.Status != 8:
|
if locked.Status != 8:
|
||||||
return False, f'状态不是结算中({locked.Status})'
|
return False, f'状态不是结算中({locked.Status})'
|
||||||
|
# 已锁定比例结算的单禁止走全额 Status=3 路径(含自动结算/强制全额结)
|
||||||
|
if int(getattr(locked, 'PartialSettleStatus', 0) or 0) == 1:
|
||||||
|
return False, '订单已锁定比例结算,请走比例结算'
|
||||||
|
if int(getattr(locked, 'PartialSettleStatus', 0) or 0) == 2:
|
||||||
|
return False, '订单已比例结算'
|
||||||
if source == 'auto_settle':
|
if source == 'auto_settle':
|
||||||
# 多重闸:资格位 + 到期 + 开关 + 开启时间 + 商家白名单
|
# 多重闸:资格位 + 到期 + 开关 + 开启时间 + 商家白名单
|
||||||
if not locked.AutoSettleEligible:
|
if not locked.AutoSettleEligible:
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('orders', '0019_penalty_spec'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='PartialSettleManagerAmount',
|
||||||
|
field=models.DecimalField(
|
||||||
|
blank=True,
|
||||||
|
db_column='partial_settle_manager_amount',
|
||||||
|
decimal_places=2,
|
||||||
|
max_digits=10,
|
||||||
|
null=True,
|
||||||
|
verbose_name='比例结算管事实付',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='PartialSettlePlayerAmount',
|
||||||
|
field=models.DecimalField(
|
||||||
|
blank=True,
|
||||||
|
db_column='partial_settle_player_amount',
|
||||||
|
decimal_places=2,
|
||||||
|
max_digits=10,
|
||||||
|
null=True,
|
||||||
|
verbose_name='比例结算打手实付',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='PartialSettleRatio',
|
||||||
|
field=models.DecimalField(
|
||||||
|
blank=True,
|
||||||
|
db_column='partial_settle_ratio',
|
||||||
|
decimal_places=4,
|
||||||
|
max_digits=5,
|
||||||
|
null=True,
|
||||||
|
verbose_name='比例结算比例',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='PartialSettleStatus',
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
db_column='partial_settle_status',
|
||||||
|
db_index=True,
|
||||||
|
default=0,
|
||||||
|
verbose_name='比例结算状态',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='PartialSettledAt',
|
||||||
|
field=models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
db_column='partial_settled_at',
|
||||||
|
null=True,
|
||||||
|
verbose_name='比例结算时间',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='PartialSettledBy',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
db_column='partial_settled_by',
|
||||||
|
default='',
|
||||||
|
max_length=32,
|
||||||
|
verbose_name='比例结算操作人',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -162,6 +162,32 @@ class Order(QModel):
|
|||||||
default=False, db_index=True,
|
default=False, db_index=True,
|
||||||
db_column='auto_settle_eligible', verbose_name='允许自动结算',
|
db_column='auto_settle_eligible', verbose_name='允许自动结算',
|
||||||
)
|
)
|
||||||
|
# ---- 拒绝退款后的比例结算快照(原 Amount/分成字段不改写)----
|
||||||
|
# PartialSettleStatus: 0无 / 1已锁定待结 / 2已结清
|
||||||
|
PartialSettleRatio = models.DecimalField(
|
||||||
|
max_digits=5, decimal_places=4, null=True, blank=True,
|
||||||
|
db_column='partial_settle_ratio', verbose_name='比例结算比例',
|
||||||
|
)
|
||||||
|
PartialSettleStatus = models.PositiveSmallIntegerField(
|
||||||
|
default=0, db_index=True,
|
||||||
|
db_column='partial_settle_status', verbose_name='比例结算状态',
|
||||||
|
)
|
||||||
|
PartialSettlePlayerAmount = models.DecimalField(
|
||||||
|
max_digits=10, decimal_places=2, null=True, blank=True,
|
||||||
|
db_column='partial_settle_player_amount', verbose_name='比例结算打手实付',
|
||||||
|
)
|
||||||
|
PartialSettleManagerAmount = models.DecimalField(
|
||||||
|
max_digits=10, decimal_places=2, null=True, blank=True,
|
||||||
|
db_column='partial_settle_manager_amount', verbose_name='比例结算管事实付',
|
||||||
|
)
|
||||||
|
PartialSettledAt = models.DateTimeField(
|
||||||
|
null=True, blank=True,
|
||||||
|
db_column='partial_settled_at', verbose_name='比例结算时间',
|
||||||
|
)
|
||||||
|
PartialSettledBy = models.CharField(
|
||||||
|
max_length=32, blank=True, default='',
|
||||||
|
db_column='partial_settled_by', verbose_name='比例结算操作人',
|
||||||
|
)
|
||||||
# ---- 时间戳 ----
|
# ---- 时间戳 ----
|
||||||
CreateTime = models.DateTimeField(
|
CreateTime = models.DateTimeField(
|
||||||
auto_now_add=True,
|
auto_now_add=True,
|
||||||
|
|||||||
@@ -0,0 +1,581 @@
|
|||||||
|
"""拒绝退款后的可配置比例结算(full 明细 × ratio,Status=6)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import F
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MONEY = Decimal('0.01')
|
||||||
|
RATIO_Q = Decimal('0.0001')
|
||||||
|
|
||||||
|
PARTIAL_STATUS_NONE = 0
|
||||||
|
PARTIAL_STATUS_ARMED = 1
|
||||||
|
PARTIAL_STATUS_DONE = 2
|
||||||
|
|
||||||
|
STATUS_TEXT = {
|
||||||
|
PARTIAL_STATUS_NONE: '',
|
||||||
|
PARTIAL_STATUS_ARMED: '已锁定比例,待结算',
|
||||||
|
PARTIAL_STATUS_DONE: '已按比例结算',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PartialSettleError(Exception):
|
||||||
|
"""比例结算业务错误。"""
|
||||||
|
|
||||||
|
def __init__(self, msg: str, code: int = 400):
|
||||||
|
super().__init__(msg)
|
||||||
|
self.msg = msg
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
|
||||||
|
def _d(v) -> Decimal:
|
||||||
|
try:
|
||||||
|
return Decimal(str(v if v is not None else 0)).quantize(MONEY, rounding=ROUND_HALF_UP)
|
||||||
|
except Exception:
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_settle_ratio(value, *, allow_percent=True) -> Decimal:
|
||||||
|
"""解析 0.01~1.00;allow_percent 时 1~100 视为百分比。"""
|
||||||
|
if value is None or value == '':
|
||||||
|
raise PartialSettleError('请填写结算比例')
|
||||||
|
try:
|
||||||
|
val = Decimal(str(value).strip())
|
||||||
|
except (InvalidOperation, ValueError, TypeError) as e:
|
||||||
|
raise PartialSettleError('结算比例无效') from e
|
||||||
|
if allow_percent and val > 1:
|
||||||
|
val = val / Decimal('100')
|
||||||
|
val = val.quantize(RATIO_Q)
|
||||||
|
if val < Decimal('0.01') or val > Decimal('1.00'):
|
||||||
|
raise PartialSettleError('结算比例须在 1%~100%')
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def club_default_partial_ratio(club_id: str) -> Decimal:
|
||||||
|
from jituan.services.order_deal import get_or_create_deal_config
|
||||||
|
row = get_or_create_deal_config(club_id or '')
|
||||||
|
if not row:
|
||||||
|
return Decimal('0.5000')
|
||||||
|
try:
|
||||||
|
return parse_settle_ratio(getattr(row, 'default_partial_settle_ratio', None) or '0.5')
|
||||||
|
except PartialSettleError:
|
||||||
|
return Decimal('0.5000')
|
||||||
|
|
||||||
|
|
||||||
|
def preview_partial_amounts(order, ratio: Decimal) -> dict:
|
||||||
|
"""按 full×ratio 预览(不入账)。组队每人份额 = full 份额 × ratio。"""
|
||||||
|
ratio = parse_settle_ratio(ratio)
|
||||||
|
full_player = _d(getattr(order, 'PlayerCommission', None))
|
||||||
|
full_manager = _d(getattr(order, 'ManagerCommission', None))
|
||||||
|
pay_player = _d(full_player * ratio)
|
||||||
|
pay_manager = _d(full_manager * ratio)
|
||||||
|
team_shares = []
|
||||||
|
try:
|
||||||
|
from orders.services.team_recruit import (
|
||||||
|
calc_lock_shares, get_active_recruit, team_should_split, _d as team_d,
|
||||||
|
)
|
||||||
|
from orders.models import TeamRecruit, TeamMember
|
||||||
|
|
||||||
|
oid = getattr(order, 'OrderID', '') or ''
|
||||||
|
rec = None
|
||||||
|
if team_should_split(order):
|
||||||
|
rec = TeamRecruit.query.filter(
|
||||||
|
order_id=oid, status=TeamRecruit.STATUS_LOCKED,
|
||||||
|
).first()
|
||||||
|
if not rec:
|
||||||
|
rec = get_active_recruit(oid)
|
||||||
|
if rec:
|
||||||
|
members = list(
|
||||||
|
TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE)
|
||||||
|
)
|
||||||
|
if members:
|
||||||
|
c = full_player
|
||||||
|
if any(team_d(m.share_amount) <= 0 and m.role == TeamMember.ROLE_MEMBER for m in members) or \
|
||||||
|
sum((team_d(m.share_amount) for m in members), Decimal('0')) <= 0:
|
||||||
|
mate_uids = [m.user_uid for m in members if m.role == TeamMember.ROLE_MEMBER]
|
||||||
|
shares = calc_lock_shares(
|
||||||
|
c, rec.share_mode, rec.per_person_amount, rec.leader_uid, mate_uids,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
shares = {m.user_uid: team_d(m.share_amount) for m in members}
|
||||||
|
total = sum(shares.values(), Decimal('0.00'))
|
||||||
|
if total != c:
|
||||||
|
diff = team_d(c - total)
|
||||||
|
if rec.leader_uid in shares:
|
||||||
|
shares[rec.leader_uid] = team_d(shares[rec.leader_uid] + diff)
|
||||||
|
for m in members:
|
||||||
|
full_share = team_d(shares.get(m.user_uid, 0))
|
||||||
|
team_shares.append({
|
||||||
|
'user_uid': m.user_uid,
|
||||||
|
'role': m.role,
|
||||||
|
'full_share': float(full_share),
|
||||||
|
'partial_share': float(_d(full_share * ratio)),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning('比例结算预览组队份额失败 %s: %s', getattr(order, 'OrderID', ''), e)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'settle_ratio': str(ratio),
|
||||||
|
'settle_percent': int((ratio * 100).quantize(Decimal('1'))),
|
||||||
|
'full_player_commission': float(full_player),
|
||||||
|
'full_manager_commission': float(full_manager),
|
||||||
|
'partial_player_amount': float(pay_player),
|
||||||
|
'partial_manager_amount': float(pay_manager),
|
||||||
|
'team_shares': team_shares,
|
||||||
|
'order_amount': float(_d(getattr(order, 'Amount', None))),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_refund_settle_payload(order, *, viewer_uid: str = '') -> dict:
|
||||||
|
"""详情 API 附加:申请/拒绝/同意原因 + 比例结算状态与金额。"""
|
||||||
|
from orders.models import RefundRecord
|
||||||
|
from backend.utils import fmt_datetime
|
||||||
|
|
||||||
|
oid = getattr(order, 'OrderID', '') or ''
|
||||||
|
apply_reason = ''
|
||||||
|
reject_reason = ''
|
||||||
|
approve_reason = ''
|
||||||
|
apply_status = None
|
||||||
|
try:
|
||||||
|
rec = RefundRecord.query.filter(OrderID=oid).order_by('-CreateTime').first()
|
||||||
|
if rec:
|
||||||
|
apply_reason = (rec.Reason or '').strip()
|
||||||
|
reject_reason = (rec.RejectReason or '').strip()
|
||||||
|
apply_status = rec.ApplyStatus
|
||||||
|
if int(rec.ApplyStatus or 0) == 1:
|
||||||
|
approve_reason = (getattr(order, 'RefundReason', None) or '').strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not reject_reason and int(getattr(order, 'Status', 0) or 0) in (6, 8):
|
||||||
|
# 历史单:拒绝理由可能只写在订单 RefundReason
|
||||||
|
if int(getattr(order, 'PartialSettleStatus', 0) or 0) or int(getattr(order, 'Status', 0) or 0) == 6:
|
||||||
|
reject_reason = reject_reason or (getattr(order, 'RefundReason', None) or '').strip()
|
||||||
|
|
||||||
|
st = int(getattr(order, 'PartialSettleStatus', 0) or 0)
|
||||||
|
ratio = getattr(order, 'PartialSettleRatio', None)
|
||||||
|
ratio_d = None
|
||||||
|
if ratio is not None:
|
||||||
|
try:
|
||||||
|
ratio_d = parse_settle_ratio(ratio)
|
||||||
|
except PartialSettleError:
|
||||||
|
ratio_d = None
|
||||||
|
|
||||||
|
full_player = _d(getattr(order, 'PlayerCommission', None))
|
||||||
|
full_manager = _d(getattr(order, 'ManagerCommission', None))
|
||||||
|
if st == PARTIAL_STATUS_DONE:
|
||||||
|
pay_player = _d(getattr(order, 'PartialSettlePlayerAmount', None))
|
||||||
|
pay_manager = _d(getattr(order, 'PartialSettleManagerAmount', None))
|
||||||
|
elif ratio_d is not None:
|
||||||
|
pay_player = _d(full_player * ratio_d)
|
||||||
|
pay_manager = _d(full_manager * ratio_d)
|
||||||
|
else:
|
||||||
|
pay_player = Decimal('0.00')
|
||||||
|
pay_manager = Decimal('0.00')
|
||||||
|
|
||||||
|
can_partial = (
|
||||||
|
st == PARTIAL_STATUS_ARMED
|
||||||
|
and int(getattr(order, 'Status', 0) or 0) == 8
|
||||||
|
and ratio_d is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
my_partial_share = None
|
||||||
|
if viewer_uid and ratio_d is not None:
|
||||||
|
try:
|
||||||
|
from orders.services.team_recruit import get_team_role_for_user
|
||||||
|
ctx = get_team_role_for_user(oid, viewer_uid)
|
||||||
|
if ctx.get('my_share') is not None:
|
||||||
|
my_partial_share = float(_d(Decimal(str(ctx['my_share'])) * ratio_d))
|
||||||
|
elif (getattr(order, 'PlayerID', None) or '') == viewer_uid and not ctx.get('has_team'):
|
||||||
|
my_partial_share = float(pay_player)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
settled_at = getattr(order, 'PartialSettledAt', None)
|
||||||
|
# tuikuan_liyou:优先展示商家申请理由;无记录时回落订单字段(兼容历史)
|
||||||
|
show_apply = apply_reason or (
|
||||||
|
(getattr(order, 'RefundReason', None) or '').strip()
|
||||||
|
if apply_status in (0, None) and not reject_reason else ''
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
'tuikuan_liyou': show_apply,
|
||||||
|
'apply_reason': apply_reason,
|
||||||
|
'reject_reason': reject_reason,
|
||||||
|
'approve_reason': approve_reason,
|
||||||
|
'refund_apply_status': apply_status,
|
||||||
|
'settle_ratio': str(ratio_d) if ratio_d is not None else '',
|
||||||
|
'settle_percent': int((ratio_d * 100).quantize(Decimal('1'))) if ratio_d is not None else None,
|
||||||
|
'partial_settle_status': st,
|
||||||
|
'partial_settle_status_text': STATUS_TEXT.get(st, ''),
|
||||||
|
'full_player_commission': float(full_player),
|
||||||
|
'full_manager_commission': float(full_manager),
|
||||||
|
'partial_player_amount': float(pay_player) if (st or ratio_d) else None,
|
||||||
|
'partial_manager_amount': float(pay_manager) if (st or ratio_d) else None,
|
||||||
|
'partial_settled_at': fmt_datetime(settled_at) if settled_at else '',
|
||||||
|
'partial_settled_by': getattr(order, 'PartialSettledBy', '') or '',
|
||||||
|
'can_partial_settle': can_partial,
|
||||||
|
'my_partial_share': my_partial_share,
|
||||||
|
'default_partial_settle_ratio': str(club_default_partial_ratio(getattr(order, 'ClubID', '') or '')),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _upsert_refund_reject(order, *, processor_id: str, jujue_liyou: str):
|
||||||
|
from orders.models import RefundRecord
|
||||||
|
from orders.services.merchant_refund_approve import normalize_processor_id
|
||||||
|
|
||||||
|
dingdan_id = order.OrderID
|
||||||
|
phone = normalize_processor_id(processor_id)
|
||||||
|
try:
|
||||||
|
refund_record = RefundRecord.query.get(OrderID=dingdan_id)
|
||||||
|
refund_record.ApplyStatus = 2
|
||||||
|
refund_record.ProcessorID = phone
|
||||||
|
refund_record.RejectReason = jujue_liyou or ''
|
||||||
|
refund_record.save()
|
||||||
|
except RefundRecord.DoesNotExist:
|
||||||
|
RefundRecord.query.create(
|
||||||
|
OrderID=dingdan_id,
|
||||||
|
PlayerID=order.PlayerID or '',
|
||||||
|
ApplicantID='',
|
||||||
|
ProcessorID=phone,
|
||||||
|
Reason=jujue_liyou or '客服拒绝退款',
|
||||||
|
ApplyStatus=2,
|
||||||
|
Amount=order.Amount,
|
||||||
|
PlayerCommission=order.PlayerCommission or 0,
|
||||||
|
Description=order.Description or '',
|
||||||
|
Remark='客服拒绝退款',
|
||||||
|
Nickname=order.Nickname or '',
|
||||||
|
RejectReason=jujue_liyou or '',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def arm_partial_settle(
|
||||||
|
order,
|
||||||
|
*,
|
||||||
|
ratio: Decimal,
|
||||||
|
jujue_liyou: str,
|
||||||
|
operator: str,
|
||||||
|
) -> dict:
|
||||||
|
"""锁定比例,回 Status=8,清自动结算,待商家/客服强制比例结。"""
|
||||||
|
ratio = parse_settle_ratio(ratio)
|
||||||
|
if int(order.Status) != 4:
|
||||||
|
raise PartialSettleError('订单状态不是退款审核中')
|
||||||
|
if int(getattr(order, 'PartialSettleStatus', 0) or 0) == PARTIAL_STATUS_DONE:
|
||||||
|
raise PartialSettleError('订单已比例结算,无法重复操作')
|
||||||
|
|
||||||
|
preview = preview_partial_amounts(order, ratio)
|
||||||
|
order.PartialSettleRatio = ratio
|
||||||
|
order.PartialSettleStatus = PARTIAL_STATUS_ARMED
|
||||||
|
order.PartialSettlePlayerAmount = _d(preview['partial_player_amount'])
|
||||||
|
order.PartialSettleManagerAmount = _d(preview['partial_manager_amount'])
|
||||||
|
order.Status = 8
|
||||||
|
order.AutoSettleEligible = False
|
||||||
|
order.AutoExpireAt = None
|
||||||
|
order.PendingDispatch = False
|
||||||
|
order.AutoTaskID = ''
|
||||||
|
order.SettlementTime = timezone.now()
|
||||||
|
if jujue_liyou:
|
||||||
|
order.RefundReason = jujue_liyou
|
||||||
|
if operator:
|
||||||
|
order.AssignedCS = (operator or '')[:20]
|
||||||
|
order.save(update_fields=[
|
||||||
|
'PartialSettleRatio', 'PartialSettleStatus',
|
||||||
|
'PartialSettlePlayerAmount', 'PartialSettleManagerAmount',
|
||||||
|
'Status', 'AutoSettleEligible', 'AutoExpireAt', 'PendingDispatch', 'AutoTaskID',
|
||||||
|
'SettlementTime', 'RefundReason', 'AssignedCS', 'UpdateTime',
|
||||||
|
])
|
||||||
|
_upsert_refund_reject(order, processor_id=operator, jujue_liyou=jujue_liyou)
|
||||||
|
return preview
|
||||||
|
|
||||||
|
|
||||||
|
def _credit_team_or_solo_partial(order, *, ratio: Decimal, pay_player: Decimal) -> bool:
|
||||||
|
"""组队:full 份额 × ratio 入账;否则单人折后入账。返回是否组队。"""
|
||||||
|
from orders.services.team_recruit import (
|
||||||
|
calc_lock_shares, get_active_recruit, lock_recruit, team_should_split, _d as team_d, _log,
|
||||||
|
)
|
||||||
|
from orders.models import TeamRecruit, TeamMember, TeamSettleLedger
|
||||||
|
from users.business_models import User
|
||||||
|
from jituan.services.fund_freeze import credit_dashou_order_settle
|
||||||
|
|
||||||
|
oid = order.OrderID
|
||||||
|
rec = get_active_recruit(oid)
|
||||||
|
if rec and rec.status == TeamRecruit.STATUS_RECRUITING:
|
||||||
|
lock_recruit(rec.id, actor_uid='system', reason='partial_settle_force_lock')
|
||||||
|
|
||||||
|
if not team_should_split(order):
|
||||||
|
# 仍有有效组队则禁止全额打队长
|
||||||
|
if get_active_recruit(oid):
|
||||||
|
raise PartialSettleError('存在有效组队但未锁定,无法比例结算')
|
||||||
|
return False
|
||||||
|
|
||||||
|
rec = TeamRecruit.objects.select_for_update().filter(
|
||||||
|
order_id=oid, status=TeamRecruit.STATUS_LOCKED,
|
||||||
|
).first()
|
||||||
|
if not rec:
|
||||||
|
return False
|
||||||
|
|
||||||
|
members = list(
|
||||||
|
TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE)
|
||||||
|
)
|
||||||
|
if not members:
|
||||||
|
return False
|
||||||
|
|
||||||
|
c = team_d(order.PlayerCommission)
|
||||||
|
if any(team_d(m.share_amount) <= 0 and m.role == TeamMember.ROLE_MEMBER for m in members) or \
|
||||||
|
sum((team_d(m.share_amount) for m in members), Decimal('0')) <= 0:
|
||||||
|
mate_uids = [m.user_uid for m in members if m.role == TeamMember.ROLE_MEMBER]
|
||||||
|
shares = calc_lock_shares(c, rec.share_mode, rec.per_person_amount, rec.leader_uid, mate_uids)
|
||||||
|
for m in members:
|
||||||
|
m.share_amount = shares.get(m.user_uid, Decimal('0.00'))
|
||||||
|
m.save(update_fields=['share_amount', 'UpdateTime'])
|
||||||
|
|
||||||
|
total = sum((team_d(m.share_amount) for m in members), Decimal('0.00'))
|
||||||
|
if total != c:
|
||||||
|
diff = team_d(c - total)
|
||||||
|
for m in members:
|
||||||
|
if m.role == TeamMember.ROLE_LEADER:
|
||||||
|
m.share_amount = team_d(m.share_amount) + diff
|
||||||
|
m.save(update_fields=['share_amount', 'UpdateTime'])
|
||||||
|
break
|
||||||
|
|
||||||
|
# 折后份额合计对齐 pay_player(尾差给队长)
|
||||||
|
partial_map = {}
|
||||||
|
for m in members:
|
||||||
|
partial_map[m.user_uid] = _d(team_d(m.share_amount) * ratio)
|
||||||
|
sum_partial = sum(partial_map.values(), Decimal('0.00'))
|
||||||
|
if sum_partial != pay_player:
|
||||||
|
for m in members:
|
||||||
|
if m.role == TeamMember.ROLE_LEADER:
|
||||||
|
partial_map[m.user_uid] = _d(partial_map[m.user_uid] + (pay_player - sum_partial))
|
||||||
|
break
|
||||||
|
|
||||||
|
for m in members:
|
||||||
|
amt = partial_map.get(m.user_uid, Decimal('0.00'))
|
||||||
|
biz_id = f'{oid}:team:{m.user_uid}:partial_settle'
|
||||||
|
if TeamSettleLedger.query.filter(biz_id=biz_id).exists():
|
||||||
|
continue
|
||||||
|
# 禁止与全额结单幂等键混用:若已有全额入账则报错
|
||||||
|
full_biz = f'{oid}:team:{m.user_uid}'
|
||||||
|
if TeamSettleLedger.query.filter(biz_id=full_biz).exists():
|
||||||
|
raise PartialSettleError('订单已全额组队结算,无法比例结算')
|
||||||
|
try:
|
||||||
|
user = User.query.get(UserUID=m.user_uid)
|
||||||
|
profile = user.DashouProfile
|
||||||
|
except Exception:
|
||||||
|
logger.error('比例结算组队打手不存在 %s %s', oid, m.user_uid)
|
||||||
|
continue
|
||||||
|
credit_dashou_order_settle(
|
||||||
|
order=order,
|
||||||
|
dashou_profile=profile,
|
||||||
|
user_id=m.user_uid,
|
||||||
|
amount=amt,
|
||||||
|
set_idle_status=True,
|
||||||
|
increment_chengjiao=True,
|
||||||
|
settle_biz_id=biz_id,
|
||||||
|
)
|
||||||
|
TeamSettleLedger(
|
||||||
|
club_id=rec.club_id,
|
||||||
|
order_id=oid,
|
||||||
|
user_uid=m.user_uid,
|
||||||
|
amount=amt,
|
||||||
|
biz_id=biz_id,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
rec.status = TeamRecruit.STATUS_FINISHED
|
||||||
|
rec.closed_at = now
|
||||||
|
rec.save(update_fields=['status', 'closed_at', 'UpdateTime'])
|
||||||
|
_log(rec.club_id, oid, rec.id, 'system', 'partial_settle', f'ratio={ratio} total={pay_player}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def settle_merchant_order_partial(
|
||||||
|
order,
|
||||||
|
*,
|
||||||
|
ratio: Optional[Decimal] = None,
|
||||||
|
operator: str = '',
|
||||||
|
source: str = '',
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
统一比例结算:full×ratio 入账打手/组队/管事,Status=6,写快照。
|
||||||
|
调用方须保证在合适的业务状态(4 立即结 / 8 且已锁定)。
|
||||||
|
"""
|
||||||
|
from orders.models import Order
|
||||||
|
from users.business_models import User
|
||||||
|
from users.models import UserShangjia
|
||||||
|
from jituan.services.fund_freeze import credit_dashou_order_settle
|
||||||
|
from orders.utils import settle_shangjia_order_guanshi_fenhong
|
||||||
|
from backend.utils import update_dashou_daily_by_action, update_shangjia_daily
|
||||||
|
|
||||||
|
locked = Order.objects.select_for_update().select_related('shangjia_kuozhan').get(pk=order.pk)
|
||||||
|
st = int(getattr(locked, 'PartialSettleStatus', 0) or 0)
|
||||||
|
if st == PARTIAL_STATUS_DONE:
|
||||||
|
raise PartialSettleError('订单已比例结算', code=400)
|
||||||
|
if int(locked.Status or 0) == 3:
|
||||||
|
raise PartialSettleError('订单已全额结单,无法比例结算')
|
||||||
|
if int(locked.Status or 0) == 5:
|
||||||
|
raise PartialSettleError('订单已退款,无法比例结算')
|
||||||
|
if int(locked.Status or 0) == 6:
|
||||||
|
raise PartialSettleError('订单已关闭(退款失败),无法再结算')
|
||||||
|
|
||||||
|
if ratio is not None:
|
||||||
|
ratio_d = parse_settle_ratio(ratio)
|
||||||
|
elif locked.PartialSettleRatio is not None:
|
||||||
|
ratio_d = parse_settle_ratio(locked.PartialSettleRatio)
|
||||||
|
else:
|
||||||
|
raise PartialSettleError('缺少结算比例')
|
||||||
|
|
||||||
|
# 允许:退款审核中立即结;或已锁定待结且 Status=8
|
||||||
|
status = int(locked.Status or 0)
|
||||||
|
if status == 4:
|
||||||
|
pass
|
||||||
|
elif status == 8 and st in (PARTIAL_STATUS_NONE, PARTIAL_STATUS_ARMED):
|
||||||
|
# NONE 兼容强制比例结传入 ratio
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise PartialSettleError(f'当前状态不允许比例结算(status={status}, partial={st})')
|
||||||
|
|
||||||
|
if not locked.PlayerID:
|
||||||
|
raise PartialSettleError('订单没有接单打手')
|
||||||
|
if locked.PlayerCommission is None:
|
||||||
|
raise PartialSettleError('订单缺少打手分成金额')
|
||||||
|
|
||||||
|
preview = preview_partial_amounts(locked, ratio_d)
|
||||||
|
pay_player = _d(preview['partial_player_amount'])
|
||||||
|
pay_manager = _d(preview['partial_manager_amount'])
|
||||||
|
dashou_id = locked.PlayerID
|
||||||
|
oid = locked.OrderID
|
||||||
|
|
||||||
|
team_done = _credit_team_or_solo_partial(locked, ratio=ratio_d, pay_player=pay_player)
|
||||||
|
if not team_done:
|
||||||
|
try:
|
||||||
|
dashou_user = User.query.get(UserUID=dashou_id)
|
||||||
|
dashou_profile = dashou_user.DashouProfile
|
||||||
|
except (User.DoesNotExist, AttributeError) as e:
|
||||||
|
raise PartialSettleError('接单打手不存在或信息不完整') from e
|
||||||
|
credit_dashou_order_settle(
|
||||||
|
order=locked,
|
||||||
|
dashou_profile=dashou_profile,
|
||||||
|
user_id=dashou_id,
|
||||||
|
amount=pay_player,
|
||||||
|
set_idle_status=True,
|
||||||
|
increment_chengjiao=True,
|
||||||
|
settle_biz_id=f'{oid}:partial_settle',
|
||||||
|
)
|
||||||
|
|
||||||
|
# 商家成交 +1(拒绝路径不退商家款)
|
||||||
|
if int(locked.Platform or 0) == 2:
|
||||||
|
try:
|
||||||
|
mid = locked.shangjia_kuozhan.MerchantID
|
||||||
|
if mid:
|
||||||
|
try:
|
||||||
|
sj_user = User.query.get(UserUID=mid)
|
||||||
|
UserShangjia.query.filter(id=sj_user.ShopProfile.id).update(
|
||||||
|
chengjiao=F('chengjiao') + 1,
|
||||||
|
)
|
||||||
|
except (User.DoesNotExist, AttributeError):
|
||||||
|
logger.warning('比例结算:商家 %s 不存在,跳过成交+1', mid)
|
||||||
|
try:
|
||||||
|
update_shangjia_daily(
|
||||||
|
yonghuid=mid,
|
||||||
|
amount=_d(locked.Amount),
|
||||||
|
action=2,
|
||||||
|
order_id=oid,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('比例结算商家日统计失败: %s', e)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning('比例结算更新商家成交失败: %s', e)
|
||||||
|
|
||||||
|
settle_shangjia_order_guanshi_fenhong(
|
||||||
|
locked,
|
||||||
|
dashou_id,
|
||||||
|
amount=pay_manager,
|
||||||
|
biz_id=f'{oid}:guanshi:merchant:partial_settle',
|
||||||
|
shuoming='商家订单比例结算分红',
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
update_dashou_daily_by_action(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
amount=pay_player,
|
||||||
|
action=2,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('比例结算打手日统计失败: %s', e)
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
locked.PartialSettleRatio = ratio_d
|
||||||
|
locked.PartialSettleStatus = PARTIAL_STATUS_DONE
|
||||||
|
locked.PartialSettlePlayerAmount = pay_player
|
||||||
|
locked.PartialSettleManagerAmount = pay_manager
|
||||||
|
locked.PartialSettledAt = now
|
||||||
|
locked.PartialSettledBy = (operator or source or '')[:32]
|
||||||
|
locked.Status = 6
|
||||||
|
locked.AutoSettleEligible = False
|
||||||
|
locked.AutoExpireAt = None
|
||||||
|
locked.PendingDispatch = False
|
||||||
|
locked.AutoTaskID = ''
|
||||||
|
if operator:
|
||||||
|
locked.AssignedCS = (operator or '')[:20]
|
||||||
|
locked.save(update_fields=[
|
||||||
|
'PartialSettleRatio', 'PartialSettleStatus',
|
||||||
|
'PartialSettlePlayerAmount', 'PartialSettleManagerAmount',
|
||||||
|
'PartialSettledAt', 'PartialSettledBy',
|
||||||
|
'Status', 'AutoSettleEligible', 'AutoExpireAt', 'PendingDispatch', 'AutoTaskID',
|
||||||
|
'AssignedCS', 'UpdateTime',
|
||||||
|
])
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'比例结算完成 order=%s ratio=%s player=%s manager=%s source=%s team=%s',
|
||||||
|
oid, ratio_d, pay_player, pay_manager, source, team_done,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
**preview,
|
||||||
|
'partial_settle_status': PARTIAL_STATUS_DONE,
|
||||||
|
'team_settled': team_done,
|
||||||
|
'source': source,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def reject_refund_partial(
|
||||||
|
order,
|
||||||
|
*,
|
||||||
|
jujue_liyou: str,
|
||||||
|
settle_ratio,
|
||||||
|
settle_timing: str,
|
||||||
|
operator: str,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
客服/后台拒绝退款入口。
|
||||||
|
settle_timing: immediate | merchant
|
||||||
|
"""
|
||||||
|
liyou = (jujue_liyou or '').strip()
|
||||||
|
if not liyou:
|
||||||
|
raise PartialSettleError('请填写拒绝理由')
|
||||||
|
timing = (settle_timing or 'immediate').strip().lower()
|
||||||
|
if timing not in ('immediate', 'merchant'):
|
||||||
|
raise PartialSettleError('settle_timing 须为 immediate 或 merchant')
|
||||||
|
ratio = parse_settle_ratio(settle_ratio)
|
||||||
|
|
||||||
|
if timing == 'merchant':
|
||||||
|
preview = arm_partial_settle(
|
||||||
|
order, ratio=ratio, jujue_liyou=liyou, operator=operator,
|
||||||
|
)
|
||||||
|
return {'timing': 'merchant', 'status': 8, **preview}
|
||||||
|
|
||||||
|
# 立即比例结:先写拒绝理由到订单,再结算
|
||||||
|
if liyou:
|
||||||
|
order.RefundReason = liyou
|
||||||
|
order.save(update_fields=['RefundReason', 'UpdateTime'])
|
||||||
|
_upsert_refund_reject(order, processor_id=operator, jujue_liyou=liyou)
|
||||||
|
result = settle_merchant_order_partial(
|
||||||
|
order, ratio=ratio, operator=operator, source='reject_immediate',
|
||||||
|
)
|
||||||
|
return {'timing': 'immediate', 'status': 6, **result}
|
||||||
+22
-6
@@ -149,15 +149,28 @@ def calc_shangjia_order_fencheng(jine, club_id=None):
|
|||||||
return dashou_fencheng, guanshi_fencheng
|
return dashou_fencheng, guanshi_fencheng
|
||||||
|
|
||||||
|
|
||||||
def settle_shangjia_order_guanshi_fenhong(order, dashou_id):
|
def settle_shangjia_order_guanshi_fenhong(
|
||||||
|
order,
|
||||||
|
dashou_id,
|
||||||
|
*,
|
||||||
|
amount=None,
|
||||||
|
biz_id=None,
|
||||||
|
shuoming=None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
商家订单结单时结算管事分红(幂等,失败不抛异常阻断主流程)。
|
商家订单结单时结算管事分红(幂等,失败不抛异常阻断主流程)。
|
||||||
|
|
||||||
|
amount / biz_id:比例结算时传入折后额与带 partial 语义的幂等键;
|
||||||
|
未传则按订单 ManagerCommission 全额、biz_id=`{order}:guanshi:merchant`。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if getattr(order, 'Platform', None) != 2:
|
if getattr(order, 'Platform', None) != 2:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if amount is None:
|
||||||
guanshi_fencheng = order.ManagerCommission or Decimal('0')
|
guanshi_fencheng = order.ManagerCommission or Decimal('0')
|
||||||
|
else:
|
||||||
|
guanshi_fencheng = Decimal(str(amount or 0))
|
||||||
if guanshi_fencheng <= 0:
|
if guanshi_fencheng <= 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -179,18 +192,21 @@ def settle_shangjia_order_guanshi_fenhong(order, dashou_id):
|
|||||||
logger.info(f"商家订单管事分红跳过: 打手{dashou_id}无邀请管事")
|
logger.info(f"商家订单管事分红跳过: 打手{dashou_id}无邀请管事")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
order_id = str(getattr(order, 'OrderID', '') or '')
|
||||||
|
credit_biz_id = (biz_id or f'{order_id}:guanshi:merchant').strip()
|
||||||
|
note = (shuoming or '商家订单分红').strip() or '商家订单分红'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
guanshi = UserGuanshi.objects.select_for_update().get(user__UserUID=guanshi_id)
|
guanshi = UserGuanshi.objects.select_for_update().get(user__UserUID=guanshi_id)
|
||||||
from jituan.services.fund_freeze import credit_guanshi_fenhong
|
from jituan.services.fund_freeze import credit_guanshi_fenhong
|
||||||
order_id = str(getattr(order, 'OrderID', '') or '')
|
|
||||||
result = credit_guanshi_fenhong(
|
result = credit_guanshi_fenhong(
|
||||||
user_id=str(guanshi_id),
|
user_id=str(guanshi_id),
|
||||||
amount=guanshi_fencheng,
|
amount=guanshi_fencheng,
|
||||||
biz_id=f'{order_id}:guanshi:merchant',
|
biz_id=credit_biz_id,
|
||||||
profile=guanshi,
|
profile=guanshi,
|
||||||
order_id=order_id,
|
order_id=order_id,
|
||||||
freeze_reason=f'商家订单分红 {order_id}',
|
freeze_reason=f'{note} {order_id}',
|
||||||
)
|
)
|
||||||
if not result.get('idempotent'):
|
if not result.get('idempotent'):
|
||||||
UserGuanshi.objects.filter(id=guanshi.id).update(
|
UserGuanshi.objects.filter(id=guanshi.id).update(
|
||||||
@@ -202,7 +218,7 @@ def settle_shangjia_order_guanshi_fenhong(order, dashou_id):
|
|||||||
dingdan_id=order.OrderID,
|
dingdan_id=order.OrderID,
|
||||||
guanshi=guanshi_id,
|
guanshi=guanshi_id,
|
||||||
dashouid=dashou_id,
|
dashouid=dashou_id,
|
||||||
shuoming='商家订单分红',
|
shuoming=note[:100],
|
||||||
fenhong=guanshi_fencheng,
|
fenhong=guanshi_fencheng,
|
||||||
avatar=user_main.Avatar if user_main else None,
|
avatar=user_main.Avatar if user_main else None,
|
||||||
nicheng=dashou.nicheng or '未知打手',
|
nicheng=dashou.nicheng or '未知打手',
|
||||||
@@ -211,7 +227,7 @@ def settle_shangjia_order_guanshi_fenhong(order, dashou_id):
|
|||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"商家订单管事分红成功: 订单{order.OrderID}, 管事{guanshi_id}, "
|
f"商家订单管事分红成功: 订单{order.OrderID}, 管事{guanshi_id}, "
|
||||||
f"打手{dashou_id}, 金额{guanshi_fencheng}元"
|
f"打手{dashou_id}, 金额{guanshi_fencheng}元 biz={credit_biz_id}"
|
||||||
)
|
)
|
||||||
except UserGuanshi.DoesNotExist:
|
except UserGuanshi.DoesNotExist:
|
||||||
logger.warning(f"商家订单管事分红跳过: 管事{guanshi_id}不存在")
|
logger.warning(f"商家订单管事分红跳过: 管事{guanshi_id}不存在")
|
||||||
|
|||||||
+19
-94
@@ -767,111 +767,36 @@ class AdJuJueTuiKuan(APIView):
|
|||||||
status=status.HTTP_404_NOT_FOUND
|
status=status.HTTP_404_NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
# 7. 获取订单的重要字段
|
if dingdan_obj.Status != 4:
|
||||||
jiedan_dashou_id = dingdan_obj.PlayerID # 接单打手ID
|
|
||||||
dashou_fencheng = dingdan_obj.PlayerCommission # 打手分成金额
|
|
||||||
fadan_pingtai = dingdan_obj.Platform # 发单平台
|
|
||||||
|
|
||||||
# 8. 验证打手分成金额是否存在
|
|
||||||
if dashou_fencheng is None:
|
|
||||||
return Response(
|
return Response(
|
||||||
{'code': 400, 'message': '订单缺少打手分成金额', 'data': None},
|
{'code': 400, 'message': '订单状态不是退款审核中', 'data': None},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
# 9. 使用事务保证数据一致性
|
settle_ratio = request.data.get('settle_ratio', request.data.get('ratio'))
|
||||||
|
settle_timing = (request.data.get('settle_timing') or 'immediate').strip()
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# 9.1 更新订单状态为6(退款失败)
|
locked = Order.query.select_for_update().get(OrderID=dingdan_id)
|
||||||
dingdan_obj.Status = 6
|
from orders.services.partial_settle import PartialSettleError, reject_refund_partial
|
||||||
dingdan_obj.save()
|
|
||||||
|
|
||||||
# 9.2 处理打手结算
|
|
||||||
if jiedan_dashou_id:
|
|
||||||
try:
|
try:
|
||||||
# 查询打手用户主表
|
result = reject_refund_partial(
|
||||||
dashou_user = User.query.select_related('DashouProfile').get(
|
locked,
|
||||||
UserUID=jiedan_dashou_id,
|
jujue_liyou=jujue_liyou,
|
||||||
#user_type='PlayerID'
|
settle_ratio=settle_ratio,
|
||||||
|
settle_timing=settle_timing,
|
||||||
|
operator=qianzhanghao,
|
||||||
|
)
|
||||||
|
except PartialSettleError as pe:
|
||||||
|
return Response(
|
||||||
|
{'code': pe.code, 'message': pe.msg, 'data': None},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 获取打手扩展表
|
|
||||||
dashou_profile = dashou_user.DashouProfile
|
|
||||||
|
|
||||||
from jituan.services.fund_freeze import credit_dashou_order_settle
|
|
||||||
credit_dashou_order_settle(
|
|
||||||
order=dingdan_obj,
|
|
||||||
dashou_profile=dashou_profile,
|
|
||||||
user_id=jiedan_dashou_id,
|
|
||||||
amount=dashou_fencheng,
|
|
||||||
)
|
|
||||||
logger.info(f"拒绝退款成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}")
|
|
||||||
|
|
||||||
except User.DoesNotExist:
|
|
||||||
# 打手用户不存在,记录日志但不影响操作
|
|
||||||
logger.warning(f"拒绝退款:打手{jiedan_dashou_id}不存在,跳过打手结算")
|
|
||||||
except AttributeError:
|
|
||||||
# 打手扩展表不存在,记录日志但不影响操作
|
|
||||||
logger.warning(f"拒绝退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算")
|
|
||||||
|
|
||||||
# 9.3 如果是商家发单,还需要更新商家扩展表
|
|
||||||
if fadan_pingtai == 2:
|
|
||||||
try:
|
|
||||||
# 获取商家订单扩展表
|
|
||||||
shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
|
|
||||||
shangjia_id = shangjia_kuozhan.MerchantID
|
|
||||||
|
|
||||||
if shangjia_id:
|
|
||||||
# 查询商家用户主表
|
|
||||||
shangjia_user = User.query.select_related('ShopProfile').get(
|
|
||||||
UserUID=shangjia_id,
|
|
||||||
#user_type='shop'
|
|
||||||
)
|
|
||||||
|
|
||||||
# 获取商家扩展表
|
|
||||||
shangjia_profile = shangjia_user.ShopProfile
|
|
||||||
|
|
||||||
# ✅ 确认字段:chengjiao 商家成交订单总量
|
|
||||||
shangjia_profile.chengjiao += 1
|
|
||||||
shangjia_profile.save()
|
|
||||||
|
|
||||||
logger.info(f"拒绝退款成功,商家{shangjia_id}成交订单+1")
|
|
||||||
|
|
||||||
except MerchantOrderExt.DoesNotExist:
|
|
||||||
# 商家订单扩展表不存在,跳过商家更新
|
|
||||||
logger.warning(f"拒绝退款:订单{dingdan_id}商家扩展表不存在,跳过商家更新")
|
|
||||||
except User.DoesNotExist:
|
|
||||||
# 商家用户不存在,跳过商家更新
|
|
||||||
logger.warning(f"拒绝退款:商家{shangjia_id}不存在,跳过商家更新")
|
|
||||||
except AttributeError:
|
|
||||||
# 商家扩展表不存在,跳过商家更新
|
|
||||||
logger.warning(f"拒绝退款:商家{shangjia_id}扩展信息不存在,跳过商家更新")
|
|
||||||
|
|
||||||
# 9.4 更新退款记录表
|
|
||||||
try:
|
|
||||||
# 查询退款记录表
|
|
||||||
tuikuan_jilu = RefundRecord.query.get(OrderID=dingdan_id)
|
|
||||||
|
|
||||||
# ✅ 确认字段:sqzhuangtai 申请状态,改为2(驳回)
|
|
||||||
tuikuan_jilu.ApplyStatus = 2
|
|
||||||
|
|
||||||
# ✅ 确认字段:bhliyou 驳回理由
|
|
||||||
tuikuan_jilu.RejectReason = jujue_liyou
|
|
||||||
|
|
||||||
# ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号)
|
|
||||||
tuikuan_jilu.ProcessorID = qianzhanghao
|
|
||||||
|
|
||||||
tuikuan_jilu.save()
|
|
||||||
logger.info(f"拒绝退款成功,更新退款记录表,订单:{dingdan_id}")
|
|
||||||
|
|
||||||
except RefundRecord.DoesNotExist:
|
|
||||||
# 退款记录表不存在,记录日志但不影响主流程
|
|
||||||
logger.warning(f"拒绝退款:订单{dingdan_id}退款记录不存在,跳过更新")
|
|
||||||
|
|
||||||
# 10. 返回成功响应
|
|
||||||
return Response({
|
return Response({
|
||||||
'code': 0,
|
'code': 0,
|
||||||
'message': '拒绝退款成功',
|
'message': '拒绝退款成功',
|
||||||
'data': None
|
'data': result,
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1510,6 +1510,13 @@ class DashouDingdanXiangqingView(APIView):
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
dingdan_data['tuikuan_tupian'] = []
|
dingdan_data['tuikuan_tupian'] = []
|
||||||
|
try:
|
||||||
|
from orders.services.partial_settle import build_refund_settle_payload
|
||||||
|
dingdan_data.update(build_refund_settle_payload(dingdan, viewer_uid=yonghuid))
|
||||||
|
if dingdan_data.get('apply_reason'):
|
||||||
|
dingdan_data['tuikuan_liyou'] = dingdan_data['apply_reason']
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
from jituan.services.order_deal import order_auto_settle_payload
|
from jituan.services.order_deal import order_auto_settle_payload
|
||||||
dingdan_data.update(order_auto_settle_payload(dingdan))
|
dingdan_data.update(order_auto_settle_payload(dingdan))
|
||||||
|
|||||||
@@ -782,6 +782,8 @@ class ShangjiaDingdanXiangqingView(APIView):
|
|||||||
'sqzhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0,
|
'sqzhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0,
|
||||||
'bhliyou': shangjia_kuozhan.RejectReason or '',
|
'bhliyou': shangjia_kuozhan.RejectReason or '',
|
||||||
'chufa_jieguo': '', # 已弃用
|
'chufa_jieguo': '', # 已弃用
|
||||||
|
'dashou_fencheng': float(order.PlayerCommission or 0),
|
||||||
|
'guanshi_fencheng': float(order.ManagerCommission or 0),
|
||||||
|
|
||||||
# 新增:罚款数据
|
# 新增:罚款数据
|
||||||
'fadan': fadan_dict,
|
'fadan': fadan_dict,
|
||||||
@@ -813,6 +815,16 @@ class ShangjiaDingdanXiangqingView(APIView):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('自动结算展示字段附加失败: %s', e)
|
logger.warning('自动结算展示字段附加失败: %s', e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from orders.services.partial_settle import build_refund_settle_payload
|
||||||
|
response_data.update(build_refund_settle_payload(order))
|
||||||
|
# 申请理由优先;拒绝理由单独字段(勿与罚单 bhliyou 混用)
|
||||||
|
if response_data.get('apply_reason'):
|
||||||
|
response_data['tuikuan_liyou'] = response_data['apply_reason']
|
||||||
|
response_data['tkliyou'] = response_data['apply_reason']
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning('退款/比例结算展示字段附加失败: %s', e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from merchant_ops.services.order_enrich import enrich_order_detail_data
|
from merchant_ops.services.order_enrich import enrich_order_detail_data
|
||||||
enrich_order_detail_data(response_data, dingdan_id)
|
enrich_order_detail_data(response_data, dingdan_id)
|
||||||
@@ -919,6 +931,7 @@ class ShangjiaJiesuanView(APIView):
|
|||||||
jine = order.Amount or Decimal('0')
|
jine = order.Amount or Decimal('0')
|
||||||
dashou_fencheng = order.PlayerCommission or Decimal('0')
|
dashou_fencheng = order.PlayerCommission or Decimal('0')
|
||||||
jiedan_dashou_id = order.PlayerID
|
jiedan_dashou_id = order.PlayerID
|
||||||
|
partial_st = int(getattr(order, 'PartialSettleStatus', 0) or 0)
|
||||||
|
|
||||||
if not jiedan_dashou_id:
|
if not jiedan_dashou_id:
|
||||||
return Response({
|
return Response({
|
||||||
@@ -927,6 +940,68 @@ class ShangjiaJiesuanView(APIView):
|
|||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if partial_st == 2:
|
||||||
|
return Response({
|
||||||
|
'code': 6,
|
||||||
|
'msg': '订单已按比例结算,无法再次结单',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# ---- 已锁定比例:强制走比例结算(Status→6),禁止全额 Status=3 ----
|
||||||
|
if partial_st == 1:
|
||||||
|
from orders.services.partial_settle import (
|
||||||
|
PartialSettleError, settle_merchant_order_partial,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
partial_result = settle_merchant_order_partial(
|
||||||
|
order,
|
||||||
|
operator=yonghuid,
|
||||||
|
source='merchant_partial',
|
||||||
|
)
|
||||||
|
except PartialSettleError as pe:
|
||||||
|
return Response({
|
||||||
|
'code': 8,
|
||||||
|
'msg': pe.msg,
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
order.refresh_from_db()
|
||||||
|
if pingfen > 0:
|
||||||
|
pingfen_obj, created = PlayerRating.query.get_or_create(
|
||||||
|
PlayerID=jiedan_dashou_id,
|
||||||
|
defaults={'TotalScore': pingfen, 'RatingCount': 1}
|
||||||
|
)
|
||||||
|
if not created:
|
||||||
|
PlayerRating.query.filter(id=pingfen_obj.id).update(
|
||||||
|
TotalScore=F('TotalScore') + pingfen,
|
||||||
|
RatingCount=F('RatingCount') + 1,
|
||||||
|
)
|
||||||
|
MerchantOrderExt.query.filter(id=shangjia_kuozhan.id).update(
|
||||||
|
MerchantReview=liuyan,
|
||||||
|
)
|
||||||
|
record_order_action_from_request(
|
||||||
|
request,
|
||||||
|
ACTION_SETTLE,
|
||||||
|
dingdan_id,
|
||||||
|
merchant_id=yonghuid,
|
||||||
|
amount=jine,
|
||||||
|
leixing_id=leixing_id_from_order(order),
|
||||||
|
)
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '按比例结算成功',
|
||||||
|
'data': {
|
||||||
|
'dingdan_id': dingdan_id,
|
||||||
|
'new_zhuangtai': 6,
|
||||||
|
'jine': float(jine),
|
||||||
|
'dashou_fencheng': float(partial_result.get('partial_player_amount') or 0),
|
||||||
|
'jiedan_dashou_id': jiedan_dashou_id,
|
||||||
|
'pingfen': pingfen,
|
||||||
|
'liuyan': liuyan,
|
||||||
|
'partial_settle': partial_result,
|
||||||
|
}
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
# ========== 本地订单正常结算给打手 ==========
|
# ========== 本地订单正常结算给打手 ==========
|
||||||
# 查询打手信息
|
# 查询打手信息
|
||||||
try:
|
try:
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,7 @@ from .views import WechatMiniProgramLoginView, UserInfoUpdateView, DashouXinxiAP
|
|||||||
YonghuTixianShenheXiugaiView, ChufaJiluHuoquView, DashouShensuView, ShangjiaChufaJiluHuoquView, \
|
YonghuTixianShenheXiugaiView, ChufaJiluHuoquView, DashouShensuView, ShangjiaChufaJiluHuoquView, \
|
||||||
WeixinOfficialCallbackView,KefuLoginView, KefuStatsView, KefuGetOrderTypesView, KefuGetOrderListView, \
|
WeixinOfficialCallbackView,KefuLoginView, KefuStatsView, KefuGetOrderTypesView, KefuGetOrderListView, \
|
||||||
KefuGetShangjiaOrderListView, KefuGetOrderDetailView, KefuChangeDashouView, KefuRecoverOrderView, \
|
KefuGetShangjiaOrderListView, KefuGetOrderDetailView, KefuChangeDashouView, KefuRecoverOrderView, \
|
||||||
KefuPlatformRefundView, KefuRejectRefundView, KefuMerchantRefundView, KefuRejectSettlementView, \
|
KefuPlatformRefundView, KefuRejectRefundView, KefuMerchantRefundView, KefuPartialSettleView, KefuRejectSettlementView, \
|
||||||
KefuTransferHallView, KefuCancelDesignationView, KefuUpdateOrderGrabRequirementView, \
|
KefuTransferHallView, KefuCancelDesignationView, KefuUpdateOrderGrabRequirementView, \
|
||||||
KefuBatchProductGrabRequirementView, KefuGetDashouListView, KefuGetDashouDetailView, \
|
KefuBatchProductGrabRequirementView, KefuGetDashouListView, KefuGetDashouDetailView, \
|
||||||
KefuUpdateDashouView, KefuPunishmentListView, KefuPunishmentActionView, KefuPunishmentDetailView, \
|
KefuUpdateDashouView, KefuPunishmentListView, KefuPunishmentActionView, KefuPunishmentDetailView, \
|
||||||
@@ -107,6 +107,7 @@ urlpatterns = [
|
|||||||
path('kefupttk', KefuPlatformRefundView.as_view(), name='客服给平台订单退款'),
|
path('kefupttk', KefuPlatformRefundView.as_view(), name='客服给平台订单退款'),
|
||||||
path('kefujjtk', KefuRejectRefundView.as_view(), name='客服拒绝商家退款'),
|
path('kefujjtk', KefuRejectRefundView.as_view(), name='客服拒绝商家退款'),
|
||||||
path('kefusjtk', KefuMerchantRefundView.as_view(), name='客服同意商家退款'),
|
path('kefusjtk', KefuMerchantRefundView.as_view(), name='客服同意商家退款'),
|
||||||
|
path('kefubljs', KefuPartialSettleView.as_view(), name='客服强制比例结算'),
|
||||||
path('kefujjjs', KefuRejectSettlementView.as_view(), name='客服拒绝结算'),
|
path('kefujjjs', KefuRejectSettlementView.as_view(), name='客服拒绝结算'),
|
||||||
path('kefuzydt', KefuTransferHallView.as_view(), name='客服订单返回大厅'),
|
path('kefuzydt', KefuTransferHallView.as_view(), name='客服订单返回大厅'),
|
||||||
path('kefuqxzd', KefuCancelDesignationView.as_view(), name='客服取消指定'),
|
path('kefuqxzd', KefuCancelDesignationView.as_view(), name='客服取消指定'),
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ from .kefu_orders import (
|
|||||||
KefuGetOrderListView,
|
KefuGetOrderListView,
|
||||||
KefuGetShangjiaOrderListView,
|
KefuGetShangjiaOrderListView,
|
||||||
KefuMerchantRefundView,
|
KefuMerchantRefundView,
|
||||||
|
KefuPartialSettleView,
|
||||||
KefuPlatformRefundView,
|
KefuPlatformRefundView,
|
||||||
KefuRecoverOrderView,
|
KefuRecoverOrderView,
|
||||||
KefuRejectRefundView,
|
KefuRejectRefundView,
|
||||||
|
|||||||
+119
-105
@@ -980,24 +980,24 @@ class KefuRejectRefundView(APIView):
|
|||||||
参数:{
|
参数:{
|
||||||
"phone": "客服手机号",
|
"phone": "客服手机号",
|
||||||
"dingdan_id": "订单ID",
|
"dingdan_id": "订单ID",
|
||||||
"jujue_liyou": "拒绝理由(可选)"
|
"jujue_liyou": "拒绝理由(必填)",
|
||||||
|
"settle_ratio": "结算比例 0.01~1 或 1~100 百分比(必填)",
|
||||||
|
"settle_timing": "immediate|merchant(必填)"
|
||||||
}
|
}
|
||||||
认证:JWT
|
|
||||||
返回:code=0 成功
|
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
parser_classes = [JSONParser]
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
# 1. 获取参数
|
|
||||||
phone = request.data.get('phone', '').strip()
|
phone = request.data.get('phone', '').strip()
|
||||||
dingdan_id = request.data.get('dingdan_id', '').strip()
|
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||||||
jujue_liyou = request.data.get('jujue_liyou', '').strip()
|
jujue_liyou = request.data.get('jujue_liyou', '').strip()
|
||||||
|
settle_ratio = request.data.get('settle_ratio', request.data.get('ratio'))
|
||||||
|
settle_timing = (request.data.get('settle_timing') or 'immediate').strip()
|
||||||
|
|
||||||
if not phone or not dingdan_id:
|
if not phone or not dingdan_id:
|
||||||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
# 2. 客服身份验证
|
|
||||||
current_user = request.user
|
current_user = request.user
|
||||||
if getattr(current_user, 'Phone', '') != phone:
|
if getattr(current_user, 'Phone', '') != phone:
|
||||||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
@@ -1016,7 +1016,6 @@ class KefuRejectRefundView(APIView):
|
|||||||
if kefu.zhuangtai != 1:
|
if kefu.zhuangtai != 1:
|
||||||
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
# 3. 查询订单并加锁
|
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
|
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
|
||||||
@@ -1026,112 +1025,33 @@ class KefuRejectRefundView(APIView):
|
|||||||
if _ms:
|
if _ms:
|
||||||
return _ms
|
return _ms
|
||||||
|
|
||||||
# 4. 验证订单状态为退款审核中(4)
|
|
||||||
if order.Status != 4:
|
if order.Status != 4:
|
||||||
return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# 获取必要字段
|
from orders.services.partial_settle import PartialSettleError, reject_refund_partial
|
||||||
jiedan_dashou_id = order.PlayerID
|
|
||||||
dashou_fencheng = order.PlayerCommission
|
|
||||||
fadan_pingtai = order.Platform
|
|
||||||
|
|
||||||
# 5. 验证打手分成金额是否存在
|
|
||||||
if dashou_fencheng is None:
|
|
||||||
return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# 6. 更新订单状态为退款失败(6)
|
|
||||||
order.Status = 6
|
|
||||||
order.AssignedCS = current_user.Phone # 记录处理客服手机号
|
|
||||||
if jujue_liyou:
|
|
||||||
order.RefundReason = jujue_liyou
|
|
||||||
order.save()
|
|
||||||
|
|
||||||
# 7. 给打手结算(返还分成)
|
|
||||||
if jiedan_dashou_id:
|
|
||||||
try:
|
try:
|
||||||
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
|
result = reject_refund_partial(
|
||||||
dashou_profile = dashou_user.DashouProfile
|
order,
|
||||||
from jituan.services.fund_freeze import credit_dashou_order_settle
|
jujue_liyou=jujue_liyou,
|
||||||
credit_dashou_order_settle(
|
settle_ratio=settle_ratio,
|
||||||
order=order,
|
settle_timing=settle_timing,
|
||||||
dashou_profile=dashou_profile,
|
operator=phone,
|
||||||
user_id=jiedan_dashou_id,
|
|
||||||
amount=dashou_fencheng,
|
|
||||||
)
|
)
|
||||||
logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}")
|
except PartialSettleError as pe:
|
||||||
except (User.DoesNotExist, AttributeError):
|
return Response({'code': pe.code, 'msg': pe.msg}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
|
|
||||||
|
|
||||||
# 8. 如果是商家发单,更新商家成交订单数量
|
|
||||||
if fadan_pingtai == 2:
|
|
||||||
try:
|
|
||||||
# 获取商家订单扩展表
|
|
||||||
shangjia_kuozhan = order.shangjia_kuozhan
|
|
||||||
shangjia_id = shangjia_kuozhan.MerchantID
|
|
||||||
if shangjia_id:
|
|
||||||
shangjia_user = User.query.get(UserUID=shangjia_id)
|
|
||||||
shangjia_profile = shangjia_user.ShopProfile
|
|
||||||
shangjia_profile.chengjiao += 1
|
|
||||||
shangjia_profile.save()
|
|
||||||
logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1")
|
|
||||||
|
|
||||||
# ✅ 新方法(行为驱动,更安全)
|
|
||||||
update_shangjia_daily(
|
|
||||||
yonghuid=shangjia_id,
|
|
||||||
amount=Decimal(str(order.Amount)),
|
|
||||||
action=2, # 2 = 结算
|
|
||||||
order_id=dingdan_id,
|
|
||||||
)
|
|
||||||
except (ObjectDoesNotExist, User.DoesNotExist, AttributeError):
|
|
||||||
logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新")
|
|
||||||
|
|
||||||
# 9. 更新退款记录表
|
|
||||||
try:
|
|
||||||
refund_record = RefundRecord.query.get(OrderID=dingdan_id)
|
|
||||||
refund_record.ApplyStatus = 2 # 2表示拒绝
|
|
||||||
refund_record.ProcessorID = phone
|
|
||||||
refund_record.RejectReason = jujue_liyou # 记录驳回理由(假设模型有此字段)
|
|
||||||
refund_record.save()
|
|
||||||
logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}")
|
|
||||||
except RefundRecord.DoesNotExist:
|
|
||||||
# 如果退款记录不存在,可以创建一条记录(可选)
|
|
||||||
RefundRecord.query.create(
|
|
||||||
OrderID=dingdan_id,
|
|
||||||
PlayerID=jiedan_dashou_id or '',
|
|
||||||
ApplicantID='',
|
|
||||||
ProcessorID=phone,
|
|
||||||
Reason=jujue_liyou or '客服拒绝退款',
|
|
||||||
ApplyStatus=2,
|
|
||||||
Amount=order.Amount,
|
|
||||||
PlayerCommission=dashou_fencheng or 0,
|
|
||||||
Description=order.Description or '',
|
|
||||||
Remark='客服拒绝退款',
|
|
||||||
Nickname=order.Nickname or '',
|
|
||||||
RejectReason=jujue_liyou or '',
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True)
|
|
||||||
|
|
||||||
# 🔥 新增:调用打手每日统计(成交)
|
|
||||||
try:
|
|
||||||
update_dashou_daily_by_action(
|
|
||||||
yonghuid=order.PlayerID,
|
|
||||||
amount=dashou_fencheng,
|
|
||||||
action=2 # 2 表示成交(结算)
|
|
||||||
)
|
|
||||||
logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"打手每日统计更新失败: {e}")
|
|
||||||
# 不影响主流程
|
|
||||||
|
|
||||||
# 10. 更新客服统计数据(管理员无客服扩展表,跳过)
|
|
||||||
if kefu is not None:
|
if kefu is not None:
|
||||||
kefu.jinrichuli += 1
|
kefu.jinrichuli += 1
|
||||||
kefu.jinyuechuli += 1
|
kefu.jinyuechuli += 1
|
||||||
kefu.zongchuli += 1
|
kefu.zongchuli += 1
|
||||||
kefu.save()
|
kefu.save()
|
||||||
|
|
||||||
return Response({'code': 0, 'msg': '拒绝退款成功'})
|
timing_msg = '已锁定比例,待商家结算' if result.get('timing') == 'merchant' else '已按比例结算'
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': f'拒绝退款成功,{timing_msg}',
|
||||||
|
'data': result,
|
||||||
|
})
|
||||||
|
|
||||||
except Order.DoesNotExist:
|
except Order.DoesNotExist:
|
||||||
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
@@ -1140,6 +1060,72 @@ class KefuRejectRefundView(APIView):
|
|||||||
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class KefuPartialSettleView(APIView):
|
||||||
|
"""
|
||||||
|
客服强制按比例结算(已锁定比例的 Status=8 单)
|
||||||
|
POST /yonghu/kefubljs
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||||||
|
settle_ratio = request.data.get('settle_ratio', request.data.get('ratio'))
|
||||||
|
|
||||||
|
if not phone or not dingdan_id:
|
||||||
|
return Response({'code': 401, 'msg': '参数缺失'})
|
||||||
|
|
||||||
|
current_user = request.user
|
||||||
|
if getattr(current_user, 'Phone', '') != phone:
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'})
|
||||||
|
if not is_kefu_backend_account(current_user):
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'})
|
||||||
|
|
||||||
|
is_admin = is_system_super_admin(current_user) or current_user.UserType == 'admin'
|
||||||
|
if not is_admin:
|
||||||
|
try:
|
||||||
|
kefu = current_user.KefuProfile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'})
|
||||||
|
if kefu.zhuangtai != 1:
|
||||||
|
return Response({'code': 401, 'msg': '客服状态异常'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
|
||||||
|
from jituan.services.club_user_access import forbid_if_order_mutate_out_of_scope
|
||||||
|
_ms = forbid_if_order_mutate_out_of_scope(request, order)
|
||||||
|
if _ms:
|
||||||
|
return _ms
|
||||||
|
|
||||||
|
from orders.services.partial_settle import (
|
||||||
|
PartialSettleError, settle_merchant_order_partial, PARTIAL_STATUS_ARMED,
|
||||||
|
)
|
||||||
|
st = int(getattr(order, 'PartialSettleStatus', 0) or 0)
|
||||||
|
if st != PARTIAL_STATUS_ARMED and settle_ratio is None:
|
||||||
|
return Response({'code': 400, 'msg': '订单未锁定比例结算,无法强制比例结'})
|
||||||
|
if order.Status not in (4, 8):
|
||||||
|
return Response({'code': 400, 'msg': f'当前状态不允许比例结算: {order.Status}'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = settle_merchant_order_partial(
|
||||||
|
order,
|
||||||
|
ratio=settle_ratio,
|
||||||
|
operator=phone,
|
||||||
|
source='kefu_force_partial',
|
||||||
|
)
|
||||||
|
except PartialSettleError as pe:
|
||||||
|
return Response({'code': pe.code, 'msg': pe.msg})
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': '比例结算成功', 'data': result})
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '订单不存在'})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('客服强制比例结算异常: %s', e, exc_info=True)
|
||||||
|
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||||||
|
|
||||||
|
|
||||||
class KefuMerchantRefundView(APIView):
|
class KefuMerchantRefundView(APIView):
|
||||||
"""
|
"""
|
||||||
客服商家订单退款接口(发单平台=2)
|
客服商家订单退款接口(发单平台=2)
|
||||||
@@ -1694,13 +1680,14 @@ class KefuGetOrderDetailView(APIView):
|
|||||||
logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}")
|
logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}")
|
||||||
response_data['dashou_images'] = []
|
response_data['dashou_images'] = []
|
||||||
|
|
||||||
# 7. 退款理由 + 证据图
|
# 7. 退款理由 + 证据图 + 比例结算明细
|
||||||
try:
|
try:
|
||||||
tuikuan_record = RefundRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
|
from orders.services.partial_settle import build_refund_settle_payload
|
||||||
response_data['tuikuan_liyou'] = tuikuan_record.Reason if tuikuan_record and tuikuan_record.Reason else ''
|
response_data.update(build_refund_settle_payload(dingdan_obj))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"[kefuhqddxq] 退款理由查询异常 dingdan_id={dingdan_id}")
|
logger.exception(f"[kefuhqddxq] 退款/比例结算字段异常 dingdan_id={dingdan_id}")
|
||||||
response_data['tuikuan_liyou'] = ''
|
response_data.setdefault('tuikuan_liyou', '')
|
||||||
|
response_data.setdefault('reject_reason', '')
|
||||||
try:
|
try:
|
||||||
from orders.models import RefundEvidenceImage
|
from orders.models import RefundEvidenceImage
|
||||||
response_data['tuikuan_tupian'] = list(
|
response_data['tuikuan_tupian'] = list(
|
||||||
@@ -1712,6 +1699,11 @@ class KefuGetOrderDetailView(APIView):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"[kefuhqddxq] 退款图片查询异常 dingdan_id={dingdan_id}")
|
logger.exception(f"[kefuhqddxq] 退款图片查询异常 dingdan_id={dingdan_id}")
|
||||||
response_data['tuikuan_tupian'] = []
|
response_data['tuikuan_tupian'] = []
|
||||||
|
# 管事分成 full 供客服预览
|
||||||
|
try:
|
||||||
|
response_data['guanshi_fencheng'] = float(dingdan_obj.ManagerCommission or 0)
|
||||||
|
except Exception:
|
||||||
|
response_data['guanshi_fencheng'] = 0.0
|
||||||
|
|
||||||
# 8. 指定打手响应
|
# 8. 指定打手响应
|
||||||
ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}
|
ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}
|
||||||
@@ -1822,6 +1814,28 @@ class KefuForceCompleteView(APIView):
|
|||||||
if order.Status != 8:
|
if order.Status != 8:
|
||||||
return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})
|
return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})
|
||||||
|
|
||||||
|
# 已锁定比例:强制结单改为比例结算(Status=6),禁止全额 Status=3
|
||||||
|
if int(getattr(order, 'PartialSettleStatus', 0) or 0) == 1:
|
||||||
|
from orders.services.partial_settle import (
|
||||||
|
PartialSettleError, settle_merchant_order_partial,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
partial_result = settle_merchant_order_partial(
|
||||||
|
order, operator=phone, source='kefu_force_partial',
|
||||||
|
)
|
||||||
|
except PartialSettleError as pe:
|
||||||
|
return Response({'code': pe.code, 'msg': pe.msg})
|
||||||
|
if kefu is not None:
|
||||||
|
kefu.jinrichuli += 1
|
||||||
|
kefu.jinyuechuli += 1
|
||||||
|
kefu.zongchuli += 1
|
||||||
|
kefu.save()
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '强制比例结算成功',
|
||||||
|
'data': partial_result,
|
||||||
|
})
|
||||||
|
|
||||||
# 4. 校验必要字段
|
# 4. 校验必要字段
|
||||||
dashou_id = order.PlayerID
|
dashou_id = order.PlayerID
|
||||||
dashou_fencheng = order.PlayerCommission
|
dashou_fencheng = order.PlayerCommission
|
||||||
|
|||||||
Reference in new issue
Block a user