feat(douyu): 支持管理员取消作废协作单
This commit is contained in:
@@ -317,6 +317,122 @@ class TestDouyuCollabOrders:
|
||||
assert result["order"].status == collab.STATUS_DONE
|
||||
assert result["order"].done_at is not None
|
||||
|
||||
def test_admin_can_restore_voided_order_to_another_status(self):
|
||||
order = collab.create_order(
|
||||
self.session,
|
||||
self.admin,
|
||||
order_no="DY-RESTORE-1",
|
||||
shop="店铺A",
|
||||
account_username="douyu-account-a",
|
||||
gift_name="礼包A",
|
||||
required_points=100,
|
||||
home_images=["collab/home-restore-1.png"],
|
||||
client=FakeClient(),
|
||||
)
|
||||
collab.set_status(
|
||||
self.session,
|
||||
self.admin,
|
||||
order,
|
||||
status=collab.STATUS_VOID,
|
||||
reason="误作废",
|
||||
)
|
||||
|
||||
restored = collab.set_status(
|
||||
self.session,
|
||||
self.admin,
|
||||
order,
|
||||
status=collab.STATUS_SCRIPTED,
|
||||
reason="管理员确认恢复",
|
||||
)
|
||||
|
||||
assert restored.status == collab.STATUS_SCRIPTED
|
||||
assert restored.void_reason == ""
|
||||
assert restored.recharged_at is not None
|
||||
assert restored.scripted_at is not None
|
||||
payload = collab.serialize_one(self.session, restored)
|
||||
serialized = DouyuCollabOrderOut.model_validate(payload).model_dump()
|
||||
assert serialized["status"] == collab.STATUS_SCRIPTED
|
||||
assert serialized["void_reason"] == ""
|
||||
|
||||
audit = (
|
||||
self.session.query(AuditLog)
|
||||
.filter(AuditLog.action == "douyu:collab:void_restore")
|
||||
.order_by(AuditLog.id.desc())
|
||||
.first()
|
||||
)
|
||||
assert audit is not None
|
||||
assert audit.username == self.admin.username
|
||||
assert '"from":"void"' in audit.detail
|
||||
assert '"to":"scripted"' in audit.detail
|
||||
assert '"previous_void_reason":"误作废"' in audit.detail
|
||||
|
||||
def test_non_admin_cannot_restore_voided_order(self):
|
||||
operator = User(
|
||||
username="douyu-collab-restore-operator",
|
||||
password_hash="hash",
|
||||
role="operation",
|
||||
is_active=True,
|
||||
)
|
||||
self.session.add(operator)
|
||||
self.session.commit()
|
||||
order = collab.create_order(
|
||||
self.session,
|
||||
self.admin,
|
||||
order_no="DY-RESTORE-2",
|
||||
account_username="douyu-account-a",
|
||||
draft=True,
|
||||
)
|
||||
collab.set_status(
|
||||
self.session,
|
||||
self.admin,
|
||||
order,
|
||||
status=collab.STATUS_VOID,
|
||||
reason="测试权限",
|
||||
)
|
||||
|
||||
with pytest.raises(collab.CollabError) as excinfo:
|
||||
collab.set_status(
|
||||
self.session,
|
||||
operator,
|
||||
order,
|
||||
status=collab.STATUS_INCOMPLETE,
|
||||
reason="越权恢复",
|
||||
)
|
||||
|
||||
assert excinfo.value.status_code == 403
|
||||
assert excinfo.value.code == "admin_required"
|
||||
assert order.status == collab.STATUS_VOID
|
||||
assert order.void_reason == "测试权限"
|
||||
|
||||
def test_void_restore_still_enforces_recharge_fields(self):
|
||||
order = collab.create_order(
|
||||
self.session,
|
||||
self.admin,
|
||||
order_no="DY-RESTORE-3",
|
||||
account_username="douyu-account-a",
|
||||
draft=True,
|
||||
)
|
||||
collab.set_status(
|
||||
self.session,
|
||||
self.admin,
|
||||
order,
|
||||
status=collab.STATUS_VOID,
|
||||
reason="资料不全",
|
||||
)
|
||||
|
||||
with pytest.raises(collab.CollabError) as excinfo:
|
||||
collab.set_status(
|
||||
self.session,
|
||||
self.admin,
|
||||
order,
|
||||
status=collab.STATUS_RECHARGED,
|
||||
reason="直接恢复",
|
||||
)
|
||||
|
||||
assert excinfo.value.code == "recharge_precondition"
|
||||
assert order.status == collab.STATUS_VOID
|
||||
assert order.void_reason == "资料不全"
|
||||
|
||||
def test_points_shortage_enters_points_error(self):
|
||||
order = collab.create_order(
|
||||
self.session,
|
||||
|
||||
@@ -908,6 +908,60 @@ def _assert_ready_for_recharge(order: DouyuExternalOrder) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _restore_void_status(
|
||||
db: Session,
|
||||
current: User,
|
||||
order: DouyuExternalOrder,
|
||||
*,
|
||||
target: str,
|
||||
reason: str,
|
||||
commit: bool,
|
||||
) -> DouyuExternalOrder:
|
||||
"""超级管理员取消作废,并把订单转为指定状态。"""
|
||||
if current.role != "super_admin":
|
||||
raise CollabError(
|
||||
"仅超级管理员可取消作废",
|
||||
status_code=403,
|
||||
code="admin_required",
|
||||
)
|
||||
if order.archived_at is not None:
|
||||
raise CollabError("已归档的订单不能改状态", code="order_archived")
|
||||
|
||||
target_index = STAGE_INDEX.get(target)
|
||||
if target_index is not None and target_index >= STAGE_INDEX[STATUS_TO_RECHARGE]:
|
||||
_assert_ready_for_recharge(order)
|
||||
|
||||
old_void_reason = order.void_reason or ""
|
||||
# 取消作废等于重新进入状态机:先清空旧流程时间,再按目标状态补当前时间。
|
||||
for field in STATUS_TIMESTAMP_FIELDS.values():
|
||||
setattr(order, field, None)
|
||||
if target_index is not None:
|
||||
for name, field in STATUS_TIMESTAMP_FIELDS.items():
|
||||
if STAGE_INDEX[name] <= target_index:
|
||||
setattr(order, field, _now())
|
||||
|
||||
order.status = target
|
||||
order.void_reason = ""
|
||||
order.updated_by = current.id
|
||||
order.version = int(order.version or 0) + 1
|
||||
record_audit(
|
||||
db,
|
||||
current,
|
||||
action="douyu:collab:void_restore",
|
||||
target=_status_audit_target(order),
|
||||
detail={
|
||||
"from": STATUS_VOID,
|
||||
"to": target,
|
||||
"previous_void_reason": old_void_reason,
|
||||
"reason": _text(reason, max_length=256),
|
||||
},
|
||||
)
|
||||
if commit:
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
return order
|
||||
|
||||
|
||||
def set_status(
|
||||
db: Session,
|
||||
current: User,
|
||||
@@ -926,7 +980,16 @@ def set_status(
|
||||
if order.archived_at is not None:
|
||||
raise CollabError("已归档的订单不能改状态", code="order_archived")
|
||||
if order.status == STATUS_VOID:
|
||||
raise CollabError("已作废的订单不能改状态", code="order_void")
|
||||
if target == STATUS_VOID:
|
||||
raise CollabError("已作废的订单不能改状态", code="order_void")
|
||||
return _restore_void_status(
|
||||
db,
|
||||
current,
|
||||
order,
|
||||
target=target,
|
||||
reason=reason,
|
||||
commit=commit,
|
||||
)
|
||||
|
||||
old = order.status
|
||||
now = _now()
|
||||
|
||||
@@ -748,6 +748,14 @@ export default function DouyuCollabPage() {
|
||||
setBatchStatusRollback(false);
|
||||
};
|
||||
|
||||
const openRestoreVoid = (order: DouyuCollabOrder) => {
|
||||
setStatusTarget(order);
|
||||
setBatchStatusOpen(false);
|
||||
setStatusValue('incomplete');
|
||||
setStatusReason('');
|
||||
setBatchStatusRollback(false);
|
||||
};
|
||||
|
||||
const openBatchStatus = () => {
|
||||
if (selectedIds.length === 0) {
|
||||
message.warning('请先勾选协作单');
|
||||
@@ -800,13 +808,14 @@ export default function DouyuCollabPage() {
|
||||
} else if (statusTarget) {
|
||||
const currentIndex = FLOW_ORDER.indexOf(statusTarget.status);
|
||||
const targetIndex = FLOW_ORDER.indexOf(statusValue);
|
||||
const restoringVoid = statusTarget.status === 'void';
|
||||
await douyuCollabApi.setStatus(statusTarget.id, {
|
||||
status: statusValue,
|
||||
reason: statusReason,
|
||||
rollback: targetIndex >= 0 && currentIndex >= 0 && targetIndex < currentIndex,
|
||||
expected_version: statusTarget.version,
|
||||
});
|
||||
message.success('状态已更新');
|
||||
message.success(restoringVoid ? '已取消作废并恢复状态' : '状态已更新');
|
||||
setSelectedRowKeys((keys) => keys.filter((key) => Number(key) !== statusTarget.id));
|
||||
setStatusTarget(null);
|
||||
}
|
||||
@@ -1350,6 +1359,17 @@ export default function DouyuCollabPage() {
|
||||
</Dropdown>
|
||||
</>
|
||||
)}
|
||||
{canEdit && !row.is_archived && row.status === 'void' && isAdmin && (
|
||||
<Tooltip title="仅超级管理员可将已作废订单恢复为其它状态">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<UndoOutlined />}
|
||||
onClick={() => openRestoreVoid(row)}
|
||||
>
|
||||
取消作废
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{row.is_archived && canEdit && (
|
||||
<Button size="small" icon={<UndoOutlined />} onClick={() => void unarchiveOrders(row)}>
|
||||
取消归档
|
||||
@@ -1960,7 +1980,13 @@ export default function DouyuCollabPage() {
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={batchStatusOpen ? `批量改状态(${selectedCount} 单)` : `改状态:${statusTarget?.order_no || ''}`}
|
||||
title={
|
||||
batchStatusOpen
|
||||
? `批量改状态(${selectedCount} 单)`
|
||||
: statusTarget?.status === 'void'
|
||||
? `取消作废:${statusTarget.order_no}`
|
||||
: `改状态:${statusTarget?.order_no || ''}`
|
||||
}
|
||||
open={batchStatusOpen || Boolean(statusTarget)}
|
||||
onCancel={() => {
|
||||
setBatchStatusOpen(false);
|
||||
@@ -1968,7 +1994,13 @@ export default function DouyuCollabPage() {
|
||||
}}
|
||||
onOk={() => void submitStatus()}
|
||||
confirmLoading={statusBusy}
|
||||
okText={batchStatusOpen ? `改成「${STATUS_META[statusValue].label}」` : '确定'}
|
||||
okText={
|
||||
batchStatusOpen
|
||||
? `改成「${STATUS_META[statusValue].label}」`
|
||||
: statusTarget?.status === 'void'
|
||||
? `恢复为「${STATUS_META[statusValue].label}」`
|
||||
: '确定'
|
||||
}
|
||||
okButtonProps={{
|
||||
disabled: batchStatusOpen && batchRollbackRequired && !batchStatusRollback,
|
||||
}}
|
||||
@@ -1978,13 +2010,26 @@ export default function DouyuCollabPage() {
|
||||
style={{ width: '100%' }}
|
||||
value={statusValue}
|
||||
onChange={(value: DouyuCollabStatus) => setStatusValue(value)}
|
||||
options={STATUS_ORDER.map((item) => ({
|
||||
options={STATUS_ORDER.filter(
|
||||
(item) => !(statusTarget?.status === 'void' && item === 'void'),
|
||||
).map((item) => ({
|
||||
value: item,
|
||||
label: STATUS_META[item].label,
|
||||
}))}
|
||||
/>
|
||||
{statusValue === 'void' && (
|
||||
<Alert type="warning" showIcon message="作废后不可恢复;请在备注里写作废原因。" />
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="作废后普通用户不能恢复;如需恢复,请由管理员取消作废。"
|
||||
/>
|
||||
)}
|
||||
{statusTarget?.status === 'void' && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="管理员操作:订单会恢复为所选状态,原作废原因会保留在审计记录中。"
|
||||
/>
|
||||
)}
|
||||
{batchStatusOpen && (
|
||||
<>
|
||||
@@ -2009,7 +2054,9 @@ export default function DouyuCollabPage() {
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
value={statusReason}
|
||||
placeholder="原因 / 备注(可选)"
|
||||
placeholder={
|
||||
statusTarget?.status === 'void' ? '取消作废原因(可选)' : '原因 / 备注(可选)'
|
||||
}
|
||||
onChange={(event) => setStatusReason(event.target.value)}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
Reference in New Issue
Block a user