修复支付是出现500报错的BUG,添加认领人ID,添加使用统计最近上线时间
This commit is contained in:
parent
858fc132ef
commit
00239b0431
@ -1280,6 +1280,7 @@ def _ask_assignment_data(
|
|||||||
"used_count": len(used_ids),
|
"used_count": len(used_ids),
|
||||||
"remaining_count": max(len(items) - len(used_ids), 0),
|
"remaining_count": max(len(items) - len(used_ids), 0),
|
||||||
"task_id": assignment.task.task_no,
|
"task_id": assignment.task.task_no,
|
||||||
|
"public_token": assignment.task.public_token or assignment.task.task_no,
|
||||||
"shop_name": assignment.task.shop_name,
|
"shop_name": assignment.task.shop_name,
|
||||||
"product_name": assignment.task.product_name,
|
"product_name": assignment.task.product_name,
|
||||||
"product_link": assignment.task.product_link,
|
"product_link": assignment.task.product_link,
|
||||||
@ -1517,7 +1518,15 @@ def _task_board_data(record: AnswerSubmission, include_images: bool = True, imag
|
|||||||
"delete_endpoint": f"/api/admin/task-board/{record.id}",
|
"delete_endpoint": f"/api/admin/task-board/{record.id}",
|
||||||
"refund_endpoint": f"/api/admin/task-board/{record.id}/confirm-refund" if record.status == "已通过" else None,
|
"refund_endpoint": f"/api/admin/task-board/{record.id}/confirm-refund" if record.status == "已通过" else None,
|
||||||
"assigned_users": [{"user_id": _public_user_id(record.user), "phone": _public_user_account(record.user), "taobao_id": record.user.taobao_id, "status": status_labels.get(record.status, record.status)}],
|
"assigned_users": [{"user_id": _public_user_id(record.user), "phone": _public_user_account(record.user), "taobao_id": record.user.taobao_id, "status": status_labels.get(record.status, record.status)}],
|
||||||
"selected_questions": ([{"id": q.id, "sequence_no": q.sequence_no, "question": q.question_text, "answer": q.answer_text, "product_title": q.product_title} for q in selected] if include_images else []),
|
"selected_questions": ([{
|
||||||
|
"id": q.id,
|
||||||
|
"sequence_no": q.sequence_no,
|
||||||
|
"question": q.question_text,
|
||||||
|
"answer": q.answer_text,
|
||||||
|
"product_title": q.product_title,
|
||||||
|
"user_id": _public_user_id(record.user),
|
||||||
|
"phone": _public_user_account(record.user),
|
||||||
|
} for q in selected] if include_images else []),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1762,7 +1771,7 @@ def _ask_task_data(task: AskTask, include_questions: bool = True) -> dict:
|
|||||||
return {
|
return {
|
||||||
"id": task.id, "board_key": f"ask-task-{task.id}", "record_kind": "ask_task",
|
"id": task.id, "board_key": f"ask-task-{task.id}", "record_kind": "ask_task",
|
||||||
"task_id": task.task_no, "task_type": "ask", "task_type_label": "提问任务",
|
"task_id": task.task_no, "task_type": "ask", "task_type_label": "提问任务",
|
||||||
"public_token": task.public_token,
|
"public_token": task.public_token or task.task_no,
|
||||||
"shop_name": task.shop_name,
|
"shop_name": task.shop_name,
|
||||||
"product_name": task.product_name, "product_link": task.product_link,
|
"product_name": task.product_name, "product_link": task.product_link,
|
||||||
# Keep the persisted task state alongside the display label. The
|
# Keep the persisted task state alongside the display label. The
|
||||||
@ -1855,6 +1864,7 @@ def _ask_review_board_data(
|
|||||||
"assignment_id": record.id,
|
"assignment_id": record.id,
|
||||||
"status": "已通过",
|
"status": "已通过",
|
||||||
"task_id": assignment.task.task_no,
|
"task_id": assignment.task.task_no,
|
||||||
|
"public_token": assignment.task.public_token or assignment.task.task_no,
|
||||||
"shop_name": assignment.task.shop_name,
|
"shop_name": assignment.task.shop_name,
|
||||||
"product_name": assignment.task.product_name,
|
"product_name": assignment.task.product_name,
|
||||||
"product_link": assignment.task.product_link,
|
"product_link": assignment.task.product_link,
|
||||||
@ -1883,6 +1893,7 @@ def _ask_review_board_data(
|
|||||||
"record_kind": "ask_batch" if is_batch else "ask_assignment",
|
"record_kind": "ask_batch" if is_batch else "ask_assignment",
|
||||||
"task_type": "ask",
|
"task_type": "ask",
|
||||||
"task_type_label": "提问任务",
|
"task_type_label": "提问任务",
|
||||||
|
"public_token": assignment.task.public_token or assignment.task.task_no,
|
||||||
"status_label": "已完成" if refund_completed else "待返款",
|
"status_label": "已完成" if refund_completed else "待返款",
|
||||||
"shop_name": assignment.task.shop_name,
|
"shop_name": assignment.task.shop_name,
|
||||||
"product_name": assignment.task.product_name,
|
"product_name": assignment.task.product_name,
|
||||||
@ -1915,6 +1926,8 @@ def _ask_review_board_data(
|
|||||||
"question": question["question"],
|
"question": question["question"],
|
||||||
"answer": "",
|
"answer": "",
|
||||||
"product_title": assignment.task.product_name,
|
"product_title": assignment.task.product_name,
|
||||||
|
"user_id": _public_user_id(assignment.user),
|
||||||
|
"phone": _public_user_account(assignment.user),
|
||||||
} for question in selected] if include_images else []),
|
} for question in selected] if include_images else []),
|
||||||
"detail_endpoint": (
|
"detail_endpoint": (
|
||||||
f"/api/admin/task-board/ask-batches/{record.id}"
|
f"/api/admin/task-board/ask-batches/{record.id}"
|
||||||
@ -3400,16 +3413,29 @@ def _save_refund_recipient(db, kind, target_id, account, name):
|
|||||||
|
|
||||||
|
|
||||||
def _snapshot_refund(db, record, kind, user, payer_account="", alipay_trade_no=""):
|
def _snapshot_refund(db, record, kind, user, payer_account="", alipay_trade_no=""):
|
||||||
|
existing = db.scalar(select(RefundRecord).where(
|
||||||
|
RefundRecord.target_kind == kind,
|
||||||
|
RefundRecord.target_id == record.id,
|
||||||
|
))
|
||||||
|
if existing:
|
||||||
|
# A verified Alipay callback may finish the same payment before the
|
||||||
|
# originating HTTP request resumes. Keep the immutable snapshot and
|
||||||
|
# only fill the provider trade number when the older row lacks it.
|
||||||
|
if alipay_trade_no and not existing.alipay_trade_no:
|
||||||
|
existing.alipay_trade_no = alipay_trade_no.strip()
|
||||||
|
return existing
|
||||||
identity = _refund_recipient_data(record, kind)
|
identity = _refund_recipient_data(record, kind)
|
||||||
task = record.assignment.task if kind == "ask_batch" else record.task if kind == "ask_assignment" else record
|
task = record.assignment.task if kind == "ask_batch" else record.task if kind == "ask_assignment" else record
|
||||||
db.add(RefundRecord(
|
snapshot = RefundRecord(
|
||||||
target_kind=kind, target_id=record.id, owner_id=task.owner_id,
|
target_kind=kind, target_id=record.id, owner_id=task.owner_id,
|
||||||
shop_name=task.shop_name or "",
|
shop_name=task.shop_name or "",
|
||||||
order_no=record.submission_no if kind == "answer_submission" else task.task_no,
|
order_no=record.submission_no if kind == "answer_submission" else task.task_no,
|
||||||
payee_name=identity["payee_name"], payee_account=identity["alipay_account"],
|
payee_name=identity["payee_name"], payee_account=identity["alipay_account"],
|
||||||
amount_fen=identity["refund_amount_fen"], operator_name=user.display_name,
|
amount_fen=identity["refund_amount_fen"], operator_name=user.display_name,
|
||||||
payer_account=payer_account.strip(), alipay_trade_no=alipay_trade_no.strip(),
|
payer_account=payer_account.strip(), alipay_trade_no=alipay_trade_no.strip(),
|
||||||
))
|
)
|
||||||
|
db.add(snapshot)
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
def _complete_alipay_refund_order(db: Session, order: AlipayRefundOrder, trade_no: str) -> bool:
|
def _complete_alipay_refund_order(db: Session, order: AlipayRefundOrder, trade_no: str) -> bool:
|
||||||
@ -3462,6 +3488,47 @@ def _complete_alipay_refund_order(db: Session, order: AlipayRefundOrder, trade_n
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _finalize_refund_confirmation(
|
||||||
|
db: Session, record, kind: str, user: AdminUser, payer: RefundPaymentAccount, trade_no: str
|
||||||
|
) -> None:
|
||||||
|
"""Finish one refund exactly once after the provider call returns.
|
||||||
|
|
||||||
|
Alipay can deliver its success callback before the original confirmation
|
||||||
|
request resumes. Locking the provider order makes the callback, automatic
|
||||||
|
reconciliation, and the browser request share one idempotent completion
|
||||||
|
path instead of racing to insert the same refund record.
|
||||||
|
"""
|
||||||
|
if settings.alipay_enabled:
|
||||||
|
order = db.scalar(
|
||||||
|
select(AlipayRefundOrder)
|
||||||
|
.where(
|
||||||
|
AlipayRefundOrder.target_kind == kind,
|
||||||
|
AlipayRefundOrder.target_id == record.id,
|
||||||
|
)
|
||||||
|
.order_by(AlipayRefundOrder.id.desc())
|
||||||
|
.with_for_update()
|
||||||
|
.execution_options(populate_existing=True)
|
||||||
|
)
|
||||||
|
if not order or not _complete_alipay_refund_order(db, order, trade_no):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="支付宝已返回成功,但本地返款状态暂未完成同步,请稍后刷新,勿重复付款",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_snapshot_refund(db, record, kind, user, payer.name, trade_no)
|
||||||
|
if kind == "ask_batch":
|
||||||
|
record.status = "paid"
|
||||||
|
record.updated_at = datetime.utcnow()
|
||||||
|
_sync_ask_task_status(record.assignment.task)
|
||||||
|
elif kind == "ask_assignment":
|
||||||
|
record.status = "已完成"
|
||||||
|
record.updated_at = datetime.utcnow()
|
||||||
|
_sync_ask_task_status(record.task)
|
||||||
|
else:
|
||||||
|
record.status = "已完成"
|
||||||
|
|
||||||
|
|
||||||
def _reconcile_pending_alipay_orders_once() -> int:
|
def _reconcile_pending_alipay_orders_once() -> int:
|
||||||
"""Query pending/legacy uncertain transfers and finalize verified results."""
|
"""Query pending/legacy uncertain transfers and finalize verified results."""
|
||||||
if not settings.alipay_enabled:
|
if not settings.alipay_enabled:
|
||||||
@ -3812,10 +3879,7 @@ async def confirm_ask_batch_refund(batch_id: int, refund_amount: str | None = Fo
|
|||||||
payer = _authorized_refund_payer(db, user, payer_account_id)
|
payer = _authorized_refund_payer(db, user, payer_account_id)
|
||||||
_save_refund_amount(db, batch, "ask_batch", refund_amount, snapshot=True)
|
_save_refund_amount(db, batch, "ask_batch", refund_amount, snapshot=True)
|
||||||
alipay_trade_no = _perform_alipay_refund(db, batch, "ask_batch", user, payer)
|
alipay_trade_no = _perform_alipay_refund(db, batch, "ask_batch", user, payer)
|
||||||
_snapshot_refund(db, batch, "ask_batch", user, payer.name, alipay_trade_no)
|
_finalize_refund_confirmation(db, batch, "ask_batch", user, payer, alipay_trade_no)
|
||||||
batch.status = "paid"
|
|
||||||
batch.updated_at = datetime.utcnow()
|
|
||||||
_sync_ask_task_status(batch.assignment.task)
|
|
||||||
db.commit(); db.refresh(batch)
|
db.commit(); db.refresh(batch)
|
||||||
return {"success": True, "data": _ask_review_board_data(batch, include_images=True, image_endpoints=True)}
|
return {"success": True, "data": _ask_review_board_data(batch, include_images=True, image_endpoints=True)}
|
||||||
|
|
||||||
@ -3832,10 +3896,7 @@ async def confirm_ask_assignment_refund(assignment_id: int, refund_amount: str |
|
|||||||
payer = _authorized_refund_payer(db, user, payer_account_id)
|
payer = _authorized_refund_payer(db, user, payer_account_id)
|
||||||
_save_refund_amount(db, assignment, "ask_assignment", refund_amount, snapshot=True)
|
_save_refund_amount(db, assignment, "ask_assignment", refund_amount, snapshot=True)
|
||||||
alipay_trade_no = _perform_alipay_refund(db, assignment, "ask_assignment", user, payer)
|
alipay_trade_no = _perform_alipay_refund(db, assignment, "ask_assignment", user, payer)
|
||||||
_snapshot_refund(db, assignment, "ask_assignment", user, payer.name, alipay_trade_no)
|
_finalize_refund_confirmation(db, assignment, "ask_assignment", user, payer, alipay_trade_no)
|
||||||
assignment.status = "已完成"
|
|
||||||
assignment.updated_at = datetime.utcnow()
|
|
||||||
_sync_ask_task_status(assignment.task)
|
|
||||||
db.commit(); db.refresh(assignment)
|
db.commit(); db.refresh(assignment)
|
||||||
return {"success": True, "data": _ask_review_board_data(assignment, include_images=True, image_endpoints=True)}
|
return {"success": True, "data": _ask_review_board_data(assignment, include_images=True, image_endpoints=True)}
|
||||||
|
|
||||||
@ -3880,8 +3941,7 @@ async def confirm_answer_refund(submission_id: int, refund_amount: str | None =
|
|||||||
payer = _authorized_refund_payer(db, user, payer_account_id)
|
payer = _authorized_refund_payer(db, user, payer_account_id)
|
||||||
_save_refund_amount(db, record, "answer_submission", refund_amount, snapshot=True)
|
_save_refund_amount(db, record, "answer_submission", refund_amount, snapshot=True)
|
||||||
alipay_trade_no = _perform_alipay_refund(db, record, "answer_submission", user, payer)
|
alipay_trade_no = _perform_alipay_refund(db, record, "answer_submission", user, payer)
|
||||||
_snapshot_refund(db, record, "answer_submission", user, payer.name, alipay_trade_no)
|
_finalize_refund_confirmation(db, record, "answer_submission", user, payer, alipay_trade_no)
|
||||||
record.status = "已完成"
|
|
||||||
db.commit(); db.refresh(record)
|
db.commit(); db.refresh(record)
|
||||||
return {"success": True, "data": _task_board_data(record, include_images=True, image_endpoints=True)}
|
return {"success": True, "data": _task_board_data(record, include_images=True, image_endpoints=True)}
|
||||||
|
|
||||||
@ -4929,14 +4989,21 @@ def admin_usage_stats(_: AdminUser = Depends(require_super_admin), db: Session =
|
|||||||
if day in trend:
|
if day in trend:
|
||||||
trend[day]["count"] += 1
|
trend[day]["count"] += 1
|
||||||
key = (entry.user_id, entry.username, entry.display_name, entry.department_name)
|
key = (entry.user_id, entry.username, entry.display_name, entry.department_name)
|
||||||
user_totals[key] = user_totals.get(key, 0) + 1
|
user_totals.setdefault(key, {"count": 0, "latest": entry.entered_at})
|
||||||
|
user_totals[key]["count"] += 1
|
||||||
|
if entry.entered_at > user_totals[key]["latest"]:
|
||||||
|
user_totals[key]["latest"] = entry.entered_at
|
||||||
department = entry.department_name or "未分配部门"
|
department = entry.department_name or "未分配部门"
|
||||||
department_totals.setdefault(department, {"count": 0, "latest": entry.entered_at, "users": {}})
|
department_totals.setdefault(department, {"count": 0, "latest": entry.entered_at, "users": {}})
|
||||||
department_totals[department]["count"] += 1
|
department_totals[department]["count"] += 1
|
||||||
if entry.entered_at > department_totals[department]["latest"]:
|
if entry.entered_at > department_totals[department]["latest"]:
|
||||||
department_totals[department]["latest"] = entry.entered_at
|
department_totals[department]["latest"] = entry.entered_at
|
||||||
member_key = (entry.user_id, entry.username, entry.display_name)
|
member_key = (entry.user_id, entry.username, entry.display_name)
|
||||||
department_totals[department]["users"][member_key] = department_totals[department]["users"].get(member_key, 0) + 1
|
members = department_totals[department]["users"]
|
||||||
|
members.setdefault(member_key, {"count": 0, "latest": entry.entered_at})
|
||||||
|
members[member_key]["count"] += 1
|
||||||
|
if entry.entered_at > members[member_key]["latest"]:
|
||||||
|
members[member_key]["latest"] = entry.entered_at
|
||||||
for day in trend:
|
for day in trend:
|
||||||
trend[day]["users"] = len({entry.user_id for entry in entries if entry.entered_at.date().isoformat() == day})
|
trend[day]["users"] = len({entry.user_id for entry in entries if entry.entered_at.date().isoformat() == day})
|
||||||
users = [
|
users = [
|
||||||
@ -4945,10 +5012,11 @@ def admin_usage_stats(_: AdminUser = Depends(require_super_admin), db: Session =
|
|||||||
"username": username,
|
"username": username,
|
||||||
"display_name": display_name,
|
"display_name": display_name,
|
||||||
"department_name": department_name,
|
"department_name": department_name,
|
||||||
"count": count,
|
"count": values["count"],
|
||||||
|
"latest_at": values["latest"].isoformat(),
|
||||||
}
|
}
|
||||||
for (user_id, username, display_name, department_name), count in sorted(
|
for (user_id, username, display_name, department_name), values in sorted(
|
||||||
user_totals.items(), key=lambda item: (-item[1], item[0][2])
|
user_totals.items(), key=lambda item: (-item[1]["count"], item[0][2])
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
departments = [
|
departments = [
|
||||||
@ -4962,10 +5030,11 @@ def admin_usage_stats(_: AdminUser = Depends(require_super_admin), db: Session =
|
|||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"username": username,
|
"username": username,
|
||||||
"display_name": display_name,
|
"display_name": display_name,
|
||||||
"count": count,
|
"count": member_values["count"],
|
||||||
|
"latest_at": member_values["latest"].isoformat(),
|
||||||
}
|
}
|
||||||
for (user_id, username, display_name), count in sorted(
|
for (user_id, username, display_name), member_values in sorted(
|
||||||
values["users"].items(), key=lambda item: (-item[1], item[0][2])
|
values["users"].items(), key=lambda item: (-item[1]["count"], item[0][2])
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,9 +82,11 @@ def test_admin_usage_entry_is_deduplicated_and_visible_in_stats():
|
|||||||
assert stats.status_code == 200
|
assert stats.status_code == 200
|
||||||
operator = next(item for item in stats.json()["data"]["users"] if item["username"] == "operator")
|
operator = next(item for item in stats.json()["data"]["users"] if item["username"] == "operator")
|
||||||
assert operator["count"] >= 1
|
assert operator["count"] >= 1
|
||||||
|
assert operator["latest_at"]
|
||||||
department = next(item for item in stats.json()["data"]["departments"] if operator["department_name"] == item["name"])
|
department = next(item for item in stats.json()["data"]["departments"] if operator["department_name"] == item["name"])
|
||||||
assert department["user_count"] >= 1
|
assert department["user_count"] >= 1
|
||||||
assert any(member["username"] == "operator" for member in department["members"])
|
member = next(member for member in department["members"] if member["username"] == "operator")
|
||||||
|
assert member["latest_at"] == operator["latest_at"]
|
||||||
|
|
||||||
|
|
||||||
def test_operator_cannot_access_super_admin_apis():
|
def test_operator_cannot_access_super_admin_apis():
|
||||||
@ -1289,7 +1291,8 @@ def test_approved_answer_appears_as_pending_refund_with_detail(monkeypatch):
|
|||||||
with TestClient(app) as client:
|
with TestClient(app) as client:
|
||||||
owner_token = answer_owner_token(client)
|
owner_token = answer_owner_token(client)
|
||||||
login = client.post("/api/answer/auth/taobao-login", json={"taobao_id": taobao_id, "owner_token": owner_token})
|
login = client.post("/api/answer/auth/taobao-login", json={"taobao_id": taobao_id, "owner_token": owner_token})
|
||||||
token = login.json()["data"]["token"]
|
answer_session = login.json()["data"]
|
||||||
|
token = answer_session["token"]
|
||||||
created = client.post(
|
created = client.post(
|
||||||
"/api/answer/me/submissions",
|
"/api/answer/me/submissions",
|
||||||
data={"token": token, "owner_token": owner_token},
|
data={"token": token, "owner_token": owner_token},
|
||||||
@ -1334,6 +1337,10 @@ def test_approved_answer_appears_as_pending_refund_with_detail(monkeypatch):
|
|||||||
assert detail.status_code == 200
|
assert detail.status_code == 200
|
||||||
assert len(detail.json()["data"]["completion_screenshots"]) == 1
|
assert len(detail.json()["data"]["completion_screenshots"]) == 1
|
||||||
assert len(detail.json()["data"]["payment_screenshots"]) == 1
|
assert len(detail.json()["data"]["payment_screenshots"]) == 1
|
||||||
|
assert {
|
||||||
|
question["user_id"]
|
||||||
|
for question in detail.json()["data"]["selected_questions"]
|
||||||
|
} == {answer_session["user_id"]}
|
||||||
refunded = client.post(
|
refunded = client.post(
|
||||||
payout["refund_endpoint"],
|
payout["refund_endpoint"],
|
||||||
data={"payer_account_id": payer_id},
|
data={"payer_account_id": payer_id},
|
||||||
@ -1590,6 +1597,13 @@ def test_ask_batches_survive_reservation_timeout_and_parent_completes_only_after
|
|||||||
assert completed_parent["status_label"] == "已完成"
|
assert completed_parent["status_label"] == "已完成"
|
||||||
assert completed_parent["completed_count"] == 2
|
assert completed_parent["completed_count"] == 2
|
||||||
assert len(completed_parent["claimed_tasks"]) == 2
|
assert len(completed_parent["claimed_tasks"]) == 2
|
||||||
|
child_detail = client.get(completed_parent["claimed_tasks"][0]["detail_endpoint"])
|
||||||
|
assert child_detail.status_code == 200
|
||||||
|
assert child_detail.json()["data"]["public_token"] == completed_parent["public_token"]
|
||||||
|
assert {
|
||||||
|
question["user_id"]
|
||||||
|
for question in child_detail.json()["data"]["selected_questions"]
|
||||||
|
} == {session["user_id"]}
|
||||||
parent_detail = client.get(completed_parent["detail_endpoint"])
|
parent_detail = client.get(completed_parent["detail_endpoint"])
|
||||||
assert parent_detail.status_code == 200
|
assert parent_detail.status_code == 200
|
||||||
assert {
|
assert {
|
||||||
|
|||||||
@ -32,6 +32,23 @@ def test_answer_confirmation_snapshot_survives_changes_and_deletion(setup):
|
|||||||
assert client.get("/api/admin/refund-records").json()["data"]["items"][0] == row
|
assert client.get("/api/admin/refund-records").json()["data"]["items"][0] == row
|
||||||
|
|
||||||
|
|
||||||
|
def test_refund_snapshot_is_idempotent_when_callback_wins_race(setup):
|
||||||
|
db, _, owner, _, task = setup
|
||||||
|
payer = db.scalar(select(RefundPaymentAccount))
|
||||||
|
|
||||||
|
first = main._snapshot_refund(
|
||||||
|
db, task, "answer_submission", owner, payer.name, "ALI-RACE-001"
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
second = main._snapshot_refund(
|
||||||
|
db, task, "answer_submission", owner, payer.name, "ALI-RACE-001"
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
assert second.id == first.id
|
||||||
|
assert db.scalars(select(RefundRecord)).all() == [first]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("batched", [True, False])
|
@pytest.mark.parametrize("batched", [True, False])
|
||||||
def test_ask_confirmation_creates_record(setup, batched):
|
def test_ask_confirmation_creates_record(setup, batched):
|
||||||
db, client, owner, person, _ = setup
|
db, client, owner, person, _ = setup
|
||||||
|
|||||||
@ -79,8 +79,9 @@ export function TaskDetail({ task, close, onConfirmRefund, onSaveRefundAmount, a
|
|||||||
const [linkCopied, setLinkCopied] = useState(false);
|
const [linkCopied, setLinkCopied] = useState(false);
|
||||||
const isAskTask =
|
const isAskTask =
|
||||||
task.task_type === "ask" || task.task_type_label === "提问任务";
|
task.task_type === "ask" || task.task_type_label === "提问任务";
|
||||||
const taskLink = isAskTask
|
const askTaskToken = task.public_token || task.task_id;
|
||||||
? `${window.location.origin}/ask/${task.public_token}`
|
const taskLink = isAskTask && askTaskToken
|
||||||
|
? `${window.location.origin}/ask/${encodeURIComponent(askTaskToken)}`
|
||||||
: "";
|
: "";
|
||||||
const proofScreenshots = [
|
const proofScreenshots = [
|
||||||
...(task.question_screenshots || []),
|
...(task.question_screenshots || []),
|
||||||
|
|||||||
@ -790,16 +790,21 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.usage-member-heading {
|
.usage-member-heading {
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: space-between;
|
grid-template-columns: minmax(0, 1fr) 180px 80px;
|
||||||
|
gap: 16px;
|
||||||
padding: 10px 0 4px;
|
padding: 10px 0 4px;
|
||||||
color: #8b9db3;
|
color: #8b9db3;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.usage-member-heading span:nth-child(2),
|
||||||
|
.usage-member-heading span:nth-child(3) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
.usage-user-row {
|
.usage-user-row {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 180px 80px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 11px 0;
|
padding: 11px 0;
|
||||||
border-top: 1px solid #edf2f7;
|
border-top: 1px solid #edf2f7;
|
||||||
@ -815,6 +820,13 @@
|
|||||||
.usage-user-row b {
|
.usage-user-row b {
|
||||||
color: #1764e8;
|
color: #1764e8;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.usage-user-row time {
|
||||||
|
color: #7187a3;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.usage-summary-grid {
|
.usage-summary-grid {
|
||||||
@ -833,6 +845,18 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
.usage-member-heading {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.usage-user-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 8px 12px;
|
||||||
|
}
|
||||||
|
.usage-user-row time {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 2;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
.usage-chart {
|
.usage-chart {
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2928,10 +2928,11 @@ function UsagePanel({ data }) {
|
|||||||
</button>
|
</button>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="usage-department-members">
|
<div className="usage-department-members">
|
||||||
<div className="usage-member-heading"><span>成员</span><span>使用次数</span></div>
|
<div className="usage-member-heading"><span>成员</span><span>最近使用</span><span>使用次数</span></div>
|
||||||
{department.members.map((member) => (
|
{department.members.map((member) => (
|
||||||
<div className="usage-user-row" key={`${department.name}-${member.user_id}-${member.username}`}>
|
<div className="usage-user-row" key={`${department.name}-${member.user_id}-${member.username}`}>
|
||||||
<div><strong>{member.display_name}</strong><span>{member.username}</span></div>
|
<div><strong>{member.display_name}</strong><span>{member.username}</span></div>
|
||||||
|
<time>{formatDateTime(member.latest_at)}</time>
|
||||||
<b>{member.count} 次</b>
|
<b>{member.count} 次</b>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user