diff --git a/jituan/services/order_deal.py b/jituan/services/order_deal.py index 89e92ec..1703cde 100644 --- a/jituan/services/order_deal.py +++ b/jituan/services/order_deal.py @@ -70,6 +70,22 @@ def _pick_display_unit(minutes: int) -> tuple[str, int]: return 'm', m +def _as_bool(value, default: bool = False) -> bool: + """兼容 JSON bool / 0|1 / "true"|"false",避免开关保存后读回仍是关。""" + if value is None: + return default + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return value != 0 + s = str(value).strip().lower() + if s in ('1', 'true', 'yes', 'on', 'y'): + return True + if s in ('0', 'false', 'no', 'off', 'n', ''): + return False + return default + + def _parse_timeout_to_minutes( data: dict, *, @@ -88,7 +104,8 @@ def _parse_timeout_to_minutes( """ if not isinstance(data, dict): return None - unit = (data.get(unit_key) or '').strip().lower() + raw_unit = data.get(unit_key) + unit = str(raw_unit or '').strip().lower() if value_key in data and data.get(value_key) is not None and unit in ('h', 'm', 'hour', 'hours', 'min', 'minute', 'minutes'): try: val = int(data.get(value_key)) @@ -622,11 +639,11 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict: row.dispatch_block_minutes = m_block row.dispatch_block_hours = max(1, (m_block + 59) // 60) - if 'dispatch_block_enabled' in data and data['dispatch_block_enabled'] is not None: - row.dispatch_block_enabled = bool(data['dispatch_block_enabled']) + if 'dispatch_block_enabled' in data and data.get('dispatch_block_enabled') is not None: + row.dispatch_block_enabled = _as_bool(data.get('dispatch_block_enabled'), False) - if 'auto_settle_enabled' in data and data['auto_settle_enabled'] is not None: - enabled = bool(data['auto_settle_enabled']) + if 'auto_settle_enabled' in data and data.get('auto_settle_enabled') is not None: + enabled = _as_bool(data.get('auto_settle_enabled'), False) row.auto_settle_enabled = enabled if enabled and not was_auto: row.auto_settle_enabled_at = now @@ -634,8 +651,8 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict: row.auto_settle_enabled_at = None _clear_club_auto_settle_eligible(row.club_id) - if 'stats_enabled' in data and data['stats_enabled'] is not None: - enabled = bool(data['stats_enabled']) + if 'stats_enabled' in data and data.get('stats_enabled') is not None: + enabled = _as_bool(data.get('stats_enabled'), False) row.stats_enabled = enabled if enabled and not was_stats: # 开启时默认从较早起点计,便于旧单刷新也能出成交率 @@ -643,23 +660,24 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict: if not enabled: # 停止写入;保留 since 供只读理解历史起点;再次开启会刷新 since pass - if enabled and was_stats and data.get('reset_stats_since'): + if enabled and was_stats and _as_bool(data.get('reset_stats_since'), False): 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 + # 未传或空值时跳过,避免前端误传 undefined/null 把库里的比例冲掉或直接 400 + if 'default_partial_settle_ratio' in data: + raw = data.get('default_partial_settle_ratio') + if raw is not None and str(raw).strip() != '' and str(raw).strip().lower() != 'undefined': + from decimal import Decimal, InvalidOperation + 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 空 if row.stats_enabled and not row.stats_since: @@ -670,7 +688,7 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict: row.stats_since = _db_datetime(row.stats_since) row.updated_by = (updated_by or '')[:32] - row.save() + _persist_deal_config_row(row) # 保存后触发一次历史回填,保证抢单池马上能刷到成交率/罚款率 try: from jituan.models import DealStatEvent @@ -682,6 +700,36 @@ def save_deal_config(club_id: str, data: dict, updated_by: str = '') -> dict: return config_to_dict(row) +def _persist_deal_config_row(row): + """ + 落库。若线上尚未跑 minutes 迁移,回落到只写 hours/开关,避免整次保存 500 导致前端像「保存了又被清空」。 + """ + from jituan.models import ClubOrderDealConfig + + try: + row.save() + return + except Exception as e: + err = str(e).lower() + if not any(k in err for k in ('minutes_platform', 'minutes_merchant', 'dispatch_block_minutes', 'unknown column')): + raise + logger.warning('deal config minutes 列不可用,回落 hours 写入 club=%s: %s', row.club_id, e) + + ClubOrderDealConfig.objects.filter(pk=row.pk).update( + auto_settle_enabled=bool(row.auto_settle_enabled), + auto_settle_enabled_at=row.auto_settle_enabled_at, + hours_platform=int(row.hours_platform or 48), + hours_merchant=int(row.hours_merchant or 48), + dispatch_block_enabled=bool(getattr(row, 'dispatch_block_enabled', False)), + dispatch_block_hours=int(getattr(row, 'dispatch_block_hours', None) or 24), + stats_enabled=bool(row.stats_enabled), + stats_since=row.stats_since, + default_partial_settle_ratio=getattr(row, 'default_partial_settle_ratio', None), + updated_by=(row.updated_by or '')[:32], + ) + row.refresh_from_db() + + def _clear_club_auto_settle_eligible(club_id: str): from orders.models import Order Order.objects.filter(ClubID=club_id, AutoSettleEligible=True).update(