fix(huya): 作废订单后失效旧积分快照
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""清理虎牙外部单协作中作废订单遗留的积分核账快照。
|
||||
|
||||
作废只代表订单不再参与后续核账,但历史版本没有清掉作废前写入的组积分合计,
|
||||
导致作废行以及尚未重算的同组订单继续显示旧的需求/结余。这里做一次幂等修复:
|
||||
|
||||
1. 所有 ``void`` 行清空积分快照;
|
||||
2. 同组仍在核账状态、且快照早于最近一次作废动作的订单,也清空快照;
|
||||
3. 资料完整且 CK 仍有效的 ``recharged / points_error`` 行退回 ``to_recharge``,
|
||||
提示重新核账。
|
||||
|
||||
实时状态切换已在服务层同步失效快照;本迁移只修复升级前已经产生的历史数据。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20261112_0078"
|
||||
down_revision: str | None = "20261112_0077"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
TABLE = "huya_external_orders"
|
||||
STALE_MESSAGE = "订单组成员已作废或恢复,原积分统计已失效,请重新核账"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
if not inspector.has_table(TABLE) or not inspector.has_table("audit_logs"):
|
||||
return
|
||||
|
||||
# 作废行不再参与任何后续核账,旧快照只保留在审计历史里。
|
||||
bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
UPDATE {TABLE}
|
||||
SET points_checked_at = NULL,
|
||||
points_balance = NULL,
|
||||
points_required_total = NULL,
|
||||
points_surplus = NULL,
|
||||
points_threshold = NULL,
|
||||
points_check_reason = 'stale_group',
|
||||
points_check_message = :message,
|
||||
points_check_detail = NULL
|
||||
WHERE status = 'void'
|
||||
AND (
|
||||
points_checked_at IS NOT NULL
|
||||
OR points_balance IS NOT NULL
|
||||
OR points_required_total IS NOT NULL
|
||||
OR points_surplus IS NOT NULL
|
||||
OR points_threshold IS NOT NULL
|
||||
OR points_check_detail IS NOT NULL
|
||||
)
|
||||
"""
|
||||
),
|
||||
{"message": STALE_MESSAGE},
|
||||
)
|
||||
|
||||
# 只处理“快照早于最近一次作废”的同组行;作废后又重新核账过的结果必须保留。
|
||||
bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
UPDATE {TABLE} AS orders
|
||||
JOIN (
|
||||
SELECT void_order.bind_game_account,
|
||||
void_order.bind_game_role,
|
||||
MAX(audit.created_at) AS voided_at
|
||||
FROM {TABLE} AS void_order
|
||||
JOIN audit_logs AS audit
|
||||
ON audit.target = CONCAT('huya_external_order:', void_order.id)
|
||||
AND audit.action = 'huya:collab:status'
|
||||
AND JSON_UNQUOTE(JSON_EXTRACT(audit.detail, '$.to')) = 'void'
|
||||
WHERE void_order.status = 'void'
|
||||
AND void_order.bind_game_account <> ''
|
||||
AND void_order.bind_game_role <> ''
|
||||
GROUP BY void_order.bind_game_account, void_order.bind_game_role
|
||||
) AS void_groups
|
||||
ON orders.bind_game_account = void_groups.bind_game_account
|
||||
AND orders.bind_game_role = void_groups.bind_game_role
|
||||
SET orders.status = CASE
|
||||
WHEN orders.status IN ('recharged', 'points_error')
|
||||
AND orders.ck_status = 'valid'
|
||||
AND orders.cookie IS NOT NULL
|
||||
AND orders.cookie <> ''
|
||||
AND orders.gift_name <> ''
|
||||
AND COALESCE(JSON_LENGTH(orders.home_images), 0) > 0
|
||||
THEN 'to_recharge'
|
||||
ELSE orders.status
|
||||
END,
|
||||
orders.recharged_at = NULL,
|
||||
orders.scripted_at = NULL,
|
||||
orders.done_at = NULL,
|
||||
orders.points_checked_at = NULL,
|
||||
orders.points_balance = NULL,
|
||||
orders.points_required_total = NULL,
|
||||
orders.points_surplus = NULL,
|
||||
orders.points_threshold = NULL,
|
||||
orders.points_check_reason = 'stale_group',
|
||||
orders.points_check_message = :message,
|
||||
orders.points_check_detail = NULL
|
||||
WHERE orders.status IN ('to_recharge', 'recharged', 'points_error')
|
||||
AND orders.points_checked_at IS NOT NULL
|
||||
AND orders.points_checked_at <= void_groups.voided_at
|
||||
"""
|
||||
),
|
||||
{"message": STALE_MESSAGE},
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""数据修复不可逆;旧快照仍保留在审计日志中,无法可靠恢复当前展示字段。"""
|
||||
@@ -730,6 +730,14 @@ def set_status(
|
||||
"allow_recharge": bool(allow_recharge),
|
||||
},
|
||||
)
|
||||
if target == STATUS_VOID or from_status == STATUS_VOID:
|
||||
_invalidate_recharge_group_after_void(
|
||||
db,
|
||||
current,
|
||||
order,
|
||||
changed_from=from_status,
|
||||
changed_to=target,
|
||||
)
|
||||
if commit:
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
@@ -827,6 +835,17 @@ RECHARGE_GROUP_STATUSES: tuple[str, ...] = (
|
||||
STATUS_RECHARGED,
|
||||
STATUS_POINTS_ERROR,
|
||||
)
|
||||
_POINTS_SNAPSHOT_FIELDS: tuple[str, ...] = (
|
||||
"points_checked_at",
|
||||
"points_balance",
|
||||
"points_required_total",
|
||||
"points_surplus",
|
||||
"points_threshold",
|
||||
"points_check_reason",
|
||||
"points_check_message",
|
||||
"points_check_detail",
|
||||
)
|
||||
_STALE_RECHARGE_MESSAGE = "订单组成员已作废或恢复,原积分统计已失效,请重新核账"
|
||||
|
||||
# 探针读到的 bind_game_account 往往是「微信用户 / QQ用户」这种通用昵称,
|
||||
# 只按它分组会把不同买家并成一组(2026-10-19 生产 8 单被合成 48000 分)。
|
||||
@@ -857,6 +876,93 @@ def _recharge_group_orders(
|
||||
)
|
||||
|
||||
|
||||
def _clear_points_snapshot(order: HuyaExternalOrder) -> None:
|
||||
order.points_checked_at = None
|
||||
order.points_balance = None
|
||||
order.points_required_total = None
|
||||
order.points_surplus = None
|
||||
order.points_threshold = None
|
||||
order.points_check_reason = ""
|
||||
order.points_check_message = ""
|
||||
order.points_check_detail = None
|
||||
|
||||
|
||||
def _invalidate_recharge_group_after_void(
|
||||
db: Session,
|
||||
current: User | None,
|
||||
changed_order: HuyaExternalOrder,
|
||||
*,
|
||||
changed_from: str,
|
||||
changed_to: str,
|
||||
) -> None:
|
||||
"""订单作废/恢复后,让同组旧核账结果失效,避免继续展示作废前的合计。"""
|
||||
|
||||
group_key = buyer_key_of(changed_order)
|
||||
if group_key is None:
|
||||
return
|
||||
rows = (
|
||||
db.query(HuyaExternalOrder)
|
||||
.filter(
|
||||
HuyaExternalOrder.bind_game_account == group_key[0],
|
||||
HuyaExternalOrder.bind_game_role == group_key[1],
|
||||
or_(
|
||||
HuyaExternalOrder.id == changed_order.id,
|
||||
HuyaExternalOrder.status.in_(RECHARGE_GROUP_STATUSES),
|
||||
),
|
||||
)
|
||||
.order_by(HuyaExternalOrder.id.asc())
|
||||
.all()
|
||||
)
|
||||
now = _now()
|
||||
for row in rows:
|
||||
previous_status = row.status
|
||||
previous_snapshot = {
|
||||
field: getattr(row, field)
|
||||
for field in (
|
||||
"points_checked_at",
|
||||
"points_balance",
|
||||
"points_required_total",
|
||||
"points_surplus",
|
||||
)
|
||||
}
|
||||
had_snapshot = any(
|
||||
getattr(row, field) not in (None, "") for field in _POINTS_SNAPSHOT_FIELDS
|
||||
)
|
||||
next_status = previous_status
|
||||
if (
|
||||
row.id != changed_order.id
|
||||
and previous_status in (STATUS_RECHARGED, STATUS_POINTS_ERROR)
|
||||
and had_snapshot
|
||||
and _is_ready_for_recharge(row)
|
||||
):
|
||||
next_status = STATUS_TO_RECHARGE
|
||||
row.status = next_status
|
||||
if row.bound_at is None:
|
||||
row.bound_at = now
|
||||
_sync_stage_timestamps(row, next_status, now)
|
||||
if not had_snapshot and next_status == previous_status:
|
||||
continue
|
||||
|
||||
_clear_points_snapshot(row)
|
||||
row.points_check_reason = "stale_group"
|
||||
row.points_check_message = _STALE_RECHARGE_MESSAGE
|
||||
_touch(row, current)
|
||||
record_audit(
|
||||
db,
|
||||
current,
|
||||
action="huya:collab:recharge_invalidate",
|
||||
target=_audit_target(row),
|
||||
detail={
|
||||
"source_order_no": changed_order.order_no,
|
||||
"source_change": f"{changed_from}->{changed_to}",
|
||||
"group_key": _recharge_group_label(group_key),
|
||||
"from_status": previous_status,
|
||||
"to_status": next_status,
|
||||
"previous": previous_snapshot,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _resolve_group_required_points(
|
||||
db: Session,
|
||||
current: User | None,
|
||||
|
||||
@@ -154,6 +154,7 @@ const ACTION_LABELS: Record<string, string> = {
|
||||
'huya:collab:ck': 'CK 变更',
|
||||
'huya:collab:ck_status': 'CK 验证',
|
||||
'huya:collab:probe_snapshot': '绑定快照刷新',
|
||||
'huya:collab:recharge_invalidate': '核账快照失效',
|
||||
'huya:collab:image': '图片变更',
|
||||
'huya:collab:delete': '删除',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user