Files

724 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""拒绝退款后的可配置比例结算(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 预览(不入账)。
- 打手/管事:分成 × ratio
- 退商家:订单金额 Amount × (1-ratio) 回可提现余额
"""
ratio = parse_settle_ratio(ratio)
order_amount = _d(getattr(order, 'Amount', None))
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)
merchant_refund = _d(order_amount * (Decimal('1') - 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'))),
'refund_percent': int(((Decimal('1') - 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),
'merchant_refund_amount': float(merchant_refund),
'team_shares': team_shares,
'order_amount': float(order_amount),
}
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))
order_amount = _d(getattr(order, 'Amount', None))
if st == PARTIAL_STATUS_DONE:
pay_player = _d(getattr(order, 'PartialSettlePlayerAmount', None))
pay_manager = _d(getattr(order, 'PartialSettleManagerAmount', None))
merchant_refund = _d(getattr(order, 'PartialSettleMerchantRefund', None))
elif ratio_d is not None:
pay_player = _d(full_player * ratio_d)
pay_manager = _d(full_manager * ratio_d)
merchant_refund = _d(order_amount * (Decimal('1') - ratio_d))
else:
pay_player = Decimal('0.00')
pay_manager = Decimal('0.00')
merchant_refund = 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 ''
)
team_shares = []
if ratio_d is not None:
try:
team_shares = preview_partial_amounts(order, ratio_d).get('team_shares') or []
except Exception as e:
logger.warning('比例结算详情组队份额失败 %s: %s', oid, e)
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,
'merchant_refund_amount': float(merchant_refund) if (st or ratio_d) else None,
'refund_percent': int(((Decimal('1') - ratio_d) * 100).quantize(Decimal('1'))) if ratio_d is not None 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,
'team_shares': team_shares,
'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.PartialSettleMerchantRefund = _d(preview['merchant_refund_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',
'PartialSettleMerchantRefund',
'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
def _credit_merchant_partial_refund(
order,
*,
amount: Decimal,
ratio: Decimal,
reason: str,
source: str,
operator: str,
) -> dict:
"""
退商家 Amount×(1-ratio) 到可提现余额;店员额度单则释放额度。
幂等:PartialMerchantRefundLedger.order_id / biz_id 唯一。
"""
from orders.models import PartialMerchantRefundLedger
from users.business_models import User
from users.models import UserShangjia
from merchant_ops.services.wallet import is_staff_quota_order, release_dispatch_quota
from backend.utils import update_shangjia_daily
oid = str(getattr(order, 'OrderID', '') or '')
amount = _d(amount)
if amount <= 0:
return {'credited': False, 'amount': '0.00', 'idempotent': True}
biz_id = f'{oid}:merchant:partial_refund'
if PartialMerchantRefundLedger.query.filter(biz_id=biz_id).exists():
logger.warning('比例结算退商家幂等跳过 order=%s', oid)
return {'credited': False, 'amount': str(amount), 'idempotent': True}
try:
mid = order.shangjia_kuozhan.MerchantID
except Exception as e:
raise PartialSettleError('订单缺少商家信息,无法退还余额') from e
if not mid:
raise PartialSettleError('订单缺少商家ID,无法退还余额')
staff_quota = bool(is_staff_quota_order(oid))
reason_text = (reason or f'拒绝退款比例结算退还 ratio={ratio}').strip()[:200]
# 先落账本再入账,唯一约束防并发双退
try:
ledger = PartialMerchantRefundLedger(
order_id=oid,
merchant_uid=str(mid)[:32],
amount=amount,
order_amount=_d(order.Amount),
settle_ratio=ratio,
reason=reason_text,
source=(source or '')[:32],
operator=(operator or '')[:32],
biz_id=biz_id,
staff_quota=staff_quota,
)
ledger.save()
except Exception as e:
if PartialMerchantRefundLedger.query.filter(biz_id=biz_id).exists():
return {'credited': False, 'amount': str(amount), 'idempotent': True}
raise PartialSettleError(f'退商家账本写入失败: {e}') from e
if staff_quota:
try:
release_dispatch_quota(
oid, amount, operator or mid,
remark=f'比例结算退还额度 {reason_text}'[:80],
)
except Exception as e:
logger.error('比例结算释放店员额度失败 order=%s: %s', oid, e, exc_info=True)
raise PartialSettleError(f'退还店员额度失败: {e}') from e
else:
try:
sj_user = User.objects.select_for_update().get(UserUID=mid)
profile = sj_user.ShopProfile
UserShangjia.objects.filter(id=profile.id).update(yue=F('yue') + amount)
except (User.DoesNotExist, AttributeError) as e:
raise PartialSettleError('商家不存在,无法退还余额') from e
try:
update_shangjia_daily(
yonghuid=mid,
amount=amount,
action=3, # 退款
order_id=oid,
)
except Exception as e:
logger.error('比例结算退商家日统计失败: %s', e)
logger.info(
'比例结算退商家成功 order=%s merchant=%s amount=%s staff_quota=%s',
oid, mid, amount, staff_quota,
)
return {
'credited': True,
'amount': str(amount),
'idempotent': False,
'staff_quota': staff_quota,
'merchant_uid': mid,
}
@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'])
merchant_refund = _d(preview['merchant_refund_amount'])
dashou_id = locked.PlayerID
oid = locked.OrderID
reject_reason = (getattr(locked, 'RefundReason', None) or '').strip()
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',
)
merchant_refund_result = {'credited': False, 'amount': '0.00'}
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)
# 日统计:成交口径记 Amount×ratio;退还另记 action=3
try:
kept = _d(_d(locked.Amount) * ratio_d)
if kept > 0:
update_shangjia_daily(
yonghuid=mid,
amount=kept,
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='商家订单比例结算分红',
)
# 剩余 (1-ratio)×Amount 退回商家(幂等)
if merchant_refund > 0:
merchant_refund_result = _credit_merchant_partial_refund(
locked,
amount=merchant_refund,
ratio=ratio_d,
reason=reject_reason or f'比例结算退还{preview.get("refund_percent")}%',
source=source or '',
operator=operator or '',
)
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.PartialSettleMerchantRefund = merchant_refund
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',
'PartialSettleMerchantRefund',
'PartialSettledAt', 'PartialSettledBy',
'Status', 'AutoSettleEligible', 'AutoExpireAt', 'PendingDispatch', 'AutoTaskID',
'AssignedCS', 'UpdateTime',
])
logger.info(
'比例结算完成 order=%s ratio=%s player=%s manager=%s refund=%s source=%s team=%s',
oid, ratio_d, pay_player, pay_manager, merchant_refund, source, team_done,
)
return {
**preview,
'partial_settle_status': PARTIAL_STATUS_DONE,
'team_settled': team_done,
'source': source,
'merchant_refund': merchant_refund_result,
}
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('请填写拒绝理由')
# 默认留给商家结算(客服强制立即结需显式传 immediate)
timing = (settle_timing or 'merchant').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}