diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc index 1ce64da..4af207e 100644 Binary files a/backend/app/__pycache__/main.cpython-313.pyc and b/backend/app/__pycache__/main.cpython-313.pyc differ diff --git a/backend/app/main.py b/backend/app/main.py index 4fe60ad..22221b3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1439,6 +1439,8 @@ def _ask_assignment_data( "task_id": assignment.task.task_no, "public_token": assignment.task.public_token or assignment.task.task_no, "shop_name": assignment.task.shop_name, + "ecommerce_platform": assignment.task.ecommerce_platform or "", + "product_id": assignment.task.product_id or "", "product_name": assignment.task.product_name, "product_link": assignment.task.product_link, "product_qr": _product_qr_data(assignment.task), @@ -2036,6 +2038,8 @@ def _ask_review_board_data( "task_id": assignment.task.task_no, "public_token": assignment.task.public_token or assignment.task.task_no, "shop_name": assignment.task.shop_name, + "ecommerce_platform": assignment.task.ecommerce_platform or "", + "product_id": assignment.task.product_id or "", "product_name": assignment.task.product_name, "product_link": assignment.task.product_link, "product_qr": _product_qr_data(assignment.task), @@ -2066,6 +2070,8 @@ def _ask_review_board_data( "public_token": assignment.task.public_token or assignment.task.task_no, "status_label": "已完成" if refund_completed else "待返款", "shop_name": assignment.task.shop_name, + "ecommerce_platform": assignment.task.ecommerce_platform or "", + "product_id": assignment.task.product_id or "", "product_name": assignment.task.product_name, "qa_count": selected_count, "claimed_count": selected_count, @@ -2474,6 +2480,13 @@ def ask_completion_reviews(user: AdminUser = Depends(current_user), db: Session @app.get("/api/admin/review-pending-count") def review_pending_count(user: AdminUser = Depends(current_user), db: Session = Depends(get_db)): """Return lightweight sidebar counts without loading review rows or images.""" + answer_draft_count = db.scalar( + select(func.count(AnswerSubmission.id)).where( + AnswerSubmission.status.in_(["待筛选", "待运营派发"]), + AnswerSubmission.questions.any(), + _answer_owner_clause(user), + ) + ) or 0 answer_count = db.scalar( select(func.count(AnswerSubmission.id)).where( AnswerSubmission.status == "待审核", @@ -2498,6 +2511,7 @@ def review_pending_count(user: AdminUser = Depends(current_user), db: Session = "ask": ask_count, "answer": answer_count, "total": ask_count + answer_count, + "answer_drafts": answer_draft_count, }, } diff --git a/backend/tests/__pycache__/test_auth.cpython-313-pytest-9.1.1.pyc b/backend/tests/__pycache__/test_auth.cpython-313-pytest-9.1.1.pyc index 0c58ecd..a6cb8b4 100644 Binary files a/backend/tests/__pycache__/test_auth.cpython-313-pytest-9.1.1.pyc and b/backend/tests/__pycache__/test_auth.cpython-313-pytest-9.1.1.pyc differ diff --git a/backend/tests/__pycache__/test_refund_records.cpython-313-pytest-9.1.1.pyc b/backend/tests/__pycache__/test_refund_records.cpython-313-pytest-9.1.1.pyc index b03e456..0729d7a 100644 Binary files a/backend/tests/__pycache__/test_refund_records.cpython-313-pytest-9.1.1.pyc and b/backend/tests/__pycache__/test_refund_records.cpython-313-pytest-9.1.1.pyc differ diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 9635d5a..5f26435 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -437,6 +437,7 @@ def test_task_board_is_available_to_operator(): assert counts.status_code == 200 count_data = counts.json()["data"] assert count_data["total"] == count_data["ask"] + count_data["answer"] + assert "answer_drafts" in count_data def test_answer_task_expires_after_24_hours_and_can_be_reactivated(): diff --git a/backend/tests/test_refund_records.py b/backend/tests/test_refund_records.py index dd8b5d3..47c9aa8 100644 --- a/backend/tests/test_refund_records.py +++ b/backend/tests/test_refund_records.py @@ -52,7 +52,11 @@ def test_refund_snapshot_is_idempotent_when_callback_wins_race(setup): @pytest.mark.parametrize("batched", [True, False]) def test_ask_confirmation_creates_record(setup, batched): db, client, owner, person, _ = setup - task = AskTask(task_no="TASK-refund", product_link="https://example.com", product_name="商品", shop_name="提问店铺", owner_id=owner.id, status="已发布") + task = AskTask( + task_no="TASK-refund", product_link="https://example.com", product_name="商品", + shop_name="提问店铺", ecommerce_platform="京东", product_id="687154817423861300", + owner_id=owner.id, status="已发布", + ) db.add(task) db.flush() assignment = AskAssignment(task_id=task.id, user_id=person.id, status="已通过") @@ -70,6 +74,10 @@ def test_ask_confirmation_creates_record(setup, batched): kind, route = "ask_assignment", "ask-assignments" db.add(RefundRecipient(target_kind=kind, target_id=record.id, account="ask@example.com", payee_name="提问人")) db.commit() + detail = client.get(f"/api/admin/task-board/{route}/{record.id}") + assert detail.status_code == 200, detail.text + assert detail.json()["data"]["ecommerce_platform"] == "京东" + assert detail.json()["data"]["product_id"] == "687154817423861300" url = f"/api/admin/task-board/{route}/{record.id}/confirm-refund" payer_id = db.scalar(select(RefundPaymentAccount.id)) owner = db.get(AdminUser, task.owner_id) @@ -84,6 +92,8 @@ def test_ask_confirmation_creates_record(setup, batched): row = client.get("/api/admin/refund-records").json()["data"]["items"][0] assert row["amount_fen"] == 50 assert row["payee_name"] == "提问人" and row["order_no"] == "TASK-refund" + assert row["ecommerce_platform"] == "京东" + assert row["product_id"] == "687154817423861300" def test_available_accounts_and_disabled_account_rejection(setup): diff --git a/frontend/src/CreateAnswerTasksPage.jsx b/frontend/src/CreateAnswerTasksPage.jsx index 7d528cf..efb7f2b 100644 --- a/frontend/src/CreateAnswerTasksPage.jsx +++ b/frontend/src/CreateAnswerTasksPage.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' -export default function CreateAnswerTasksPageV2({ api, user }) { +export default function CreateAnswerTasksPageV2({ api, user, answerDraftCount = 0 }) { const [records, setRecords] = useState([]) const [expanded, setExpanded] = useState({}) const [busy, setBusy] = useState('') @@ -219,6 +219,7 @@ export default function CreateAnswerTasksPageV2({ api, user }) { setPublishing(null) setPublishForm({ ecommerce_platform: ecommercePlatform, shop_name: shopName, product_id: '' }) await load() + window.dispatchEvent(new CustomEvent('answer-draft-count-changed')) } catch (err) { setError(err.message) } finally { @@ -234,6 +235,7 @@ export default function CreateAnswerTasksPageV2({ api, user }) { await api(`/api/admin/answer-task-drafts/${record.id}/reject`, { method: 'POST' }) setRecords(current => current.filter(item => item.id !== record.id)) setRejecting(null) + window.dispatchEvent(new CustomEvent('answer-draft-count-changed')) } catch (err) { setError(err.message) } finally { @@ -277,7 +279,7 @@ export default function CreateAnswerTasksPageV2({ api, user }) { return <>

创建任务

通过顶部切换创建「提问任务」或「回答任务」。

-
+
ⓘ 回答截图审核用户提交截图后会立即进入此审核队列。按「用户ID」分组核对识别的问题,勾选需要派发的问题,回答可重新生成或直接编辑,确认后生成任务。
{answerLink &&
我的回答任务专属链接长期固定,用户通过此链接提交的截图只会进入您的审核队列
event.target.select()} />
} diff --git a/frontend/src/CreateAskTasksPage.jsx b/frontend/src/CreateAskTasksPage.jsx index 4f4de89..5ca6678 100644 --- a/frontend/src/CreateAskTasksPage.jsx +++ b/frontend/src/CreateAskTasksPage.jsx @@ -65,7 +65,7 @@ function withoutTransientProductFields(value = {}) { return (await file.text()).slice(0, 20000); } -export default function CreateAskTasksPage({ api, librarySelection = [], onLibrarySelectionConsumed }) { +export default function CreateAskTasksPage({ api, librarySelection = [], onLibrarySelectionConsumed, answerDraftCount = 0 }) { const cached = readPageCache("create-ask-task", {}); const [form, setForm] = useState(() => { // Product identifiers and QR/token data are one-time publish inputs. Never @@ -662,6 +662,9 @@ export default function CreateAskTasksPage({ api, librarySelection = [], onLibra } > 创建回答任务 + {answerDraftCount > 0 && ( + {answerDraftCount > 99 ? "99+" : answerDraftCount} + )} diff --git a/frontend/src/TaskBoardPage.jsx b/frontend/src/TaskBoardPage.jsx index 0834888..f8dbe17 100644 --- a/frontend/src/TaskBoardPage.jsx +++ b/frontend/src/TaskBoardPage.jsx @@ -48,13 +48,21 @@ function TaskDetailLoading() { ); } -export function TaskDetail({ task, close, onConfirmRefund, onSaveRefundAmount, onReactivate, onCancel, actionBusy, api, hasNextRefund, onNextRefund, nextBusy, readOnly = false }) { +export function TaskDetail({ task, close, onConfirmRefund, onSaveRefundAmount, onReactivate, onCancel, actionBusy, api, user, hasNextRefund, onNextRefund, nextBusy, readOnly = false }) { const [amount, setAmount] = useState(""); - const [payerAccount, setPayerAccount] = useState(""); + const payerAccountStorageKey = `task_board_last_payer_account_${user?.id || user?.username || "current"}`; + const rememberedPayerAccount = () => { + try { + return window.localStorage.getItem(payerAccountStorageKey) || ""; + } catch { + return ""; + } + }; + const [payerAccount, setPayerAccount] = useState(rememberedPayerAccount); const [paymentAccounts, setPaymentAccounts] = useState([]); const [accountsBusy, setAccountsBusy] = useState(false); const [refundError, setRefundError] = useState(""); - useEffect(() => { setPayerAccount(""); setRefundError(""); }, [task.board_key]); + useEffect(() => { setPayerAccount(rememberedPayerAccount()); setRefundError(""); }, [task.board_key, payerAccountStorageKey]); const [amountBusy, setAmountBusy] = useState(false); const [amountError, setAmountError] = useState(""); useEffect(() => { @@ -99,11 +107,23 @@ export function TaskDetail({ task, close, onConfirmRefund, onSaveRefundAmount, o setRefundError(""); setPaymentAccounts([]); api("/api/admin/refund-payment-accounts/available").then((result) => { - if (!cancelled) setPaymentAccounts(result.data); + if (cancelled) return; + const accounts = result.data || []; + const remembered = rememberedPayerAccount(); + const isAvailable = accounts.some((account) => String(account.id) === remembered); + setPaymentAccounts(accounts); + setPayerAccount(isAvailable ? remembered : ""); + if (remembered && !isAvailable) { + try { window.localStorage.removeItem(payerAccountStorageKey); } catch { /* localStorage may be unavailable */ } + } }).catch((error) => { if (!cancelled) setRefundError(error.message); }) .finally(() => { if (!cancelled) setAccountsBusy(false); }); return () => { cancelled = true; }; - }, [refundOpen, api]); + }, [refundOpen, api, payerAccountStorageKey]); + function selectPayerAccount(value) { + setPayerAccount(value); + setRefundError(""); + } async function confirmRefund() { if (!validAmount) return setRefundError("请输入大于0且最多两位小数的金额"); if (!payerAccount) return setRefundError("请选择付款账户"); @@ -111,6 +131,7 @@ export function TaskDetail({ task, close, onConfirmRefund, onSaveRefundAmount, o setRefundError(""); try { await onConfirmRefund(task, amount, payerAccount); + try { window.localStorage.setItem(payerAccountStorageKey, payerAccount); } catch { /* storage is optional */ } setRefundOpen(false); } catch (error) { setRefundError(error.message); @@ -366,7 +387,7 @@ export function TaskDetail({ task, close, onConfirmRefund, onSaveRefundAmount, o 默认按每条问答 0.50 元计算;支付宝单笔转账最低 0.10 元。