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 && }
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 元。