diff --git a/front/app/boss/analysis/AnalysisContent.tsx b/front/app/boss/analysis/AnalysisContent.tsx
index 36af561..c43e851 100644
--- a/front/app/boss/analysis/AnalysisContent.tsx
+++ b/front/app/boss/analysis/AnalysisContent.tsx
@@ -102,6 +102,8 @@ export default function AnalysisContent({
handleConfirmBatch,
handleConfirmAiRecommendedBatch,
handleConfirmManualBatch,
+ handleReconcileJob,
+ handleRetryJob,
handleSkipJob,
clearAnalysisData,
} = useBossDeliveryActions({
@@ -287,6 +289,8 @@ export default function AnalysisContent({
selectedManualJobIds={selectedManualJobIds}
onOpenText={openTextDialog}
onConfirmJob={handleConfirmJob}
+ onReconcileJob={handleReconcileJob}
+ onRetryJob={handleRetryJob}
onSkipJob={handleSkipJob}
onLoadList={loadList}
onInputPageChange={setInputPage}
diff --git a/front/app/boss/analysis/components/BossJobTable.tsx b/front/app/boss/analysis/components/BossJobTable.tsx
index 3034f05..1aabd23 100644
--- a/front/app/boss/analysis/components/BossJobTable.tsx
+++ b/front/app/boss/analysis/components/BossJobTable.tsx
@@ -23,6 +23,8 @@ export function BossJobTable({
selectedManualJobIds,
onOpenText,
onConfirmJob,
+ onReconcileJob,
+ onRetryJob,
onSkipJob,
onLoadList,
onInputPageChange,
@@ -44,6 +46,8 @@ export function BossJobTable({
selectedManualJobIds: ReadonlySet
onOpenText: (title: string, content?: string) => void
onConfirmJob: (job: BossJob) => void
+ onReconcileJob: (job: BossJob) => void
+ onRetryJob: (job: BossJob) => void
onSkipJob: (job: BossJob) => void
onLoadList: (page: number, size: number) => void
onInputPageChange: (value: number | string) => void
@@ -190,15 +194,30 @@ export function BossJobTable({
{(page - 1) * size + idx + 1}
- {job.deliveryStatus === "待确认" ? (
+ {job.deliveryStatus === "待确认" || job.deliveryStatus === "投递确认中" ? (
-
+ ) : job.deliveryStatus === "投递结果待确认" ? (
+
+ onReconcileJob(job)} className="h-7 w-full rounded px-2 text-xs leading-none">
+ 对账
+
+ onRetryJob(job)} className="h-7 w-full rounded px-2 text-xs leading-none">
+ 重试
+ ) : job.deliveryStatus === "投递失败" ? (
+ onRetryJob(job)} className="h-7 w-full rounded px-2 text-xs leading-none">
+ 重试
+
) : (job.deliveryStatus || "").includes("已投递") ? (
diff --git a/front/app/boss/analysis/hooks/useBossDeliveryActions.ts b/front/app/boss/analysis/hooks/useBossDeliveryActions.ts
index a695fea..6442b00 100644
--- a/front/app/boss/analysis/hooks/useBossDeliveryActions.ts
+++ b/front/app/boss/analysis/hooks/useBossDeliveryActions.ts
@@ -6,6 +6,72 @@ import { API_BASE } from "@/lib/api"
import { sendChromeBridgeMessage } from "@/lib/chromeBridge"
import type { BossJob, FilterState } from "../types"
+type ReservedTask = { id?: number; requestKey?: string }
+
+async function postJsonWithRetry(url: string, body?: unknown) {
+ let lastError: unknown
+ for (let attempt = 0; attempt < 2; attempt += 1) {
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: body === undefined ? undefined : { "Content-Type": "application/json" },
+ body: body === undefined ? undefined : JSON.stringify(body),
+ })
+ return await response.json()
+ } catch (error) {
+ lastError = error
+ }
+ }
+ throw lastError instanceof Error ? lastError : new Error("投递请求未收到响应")
+}
+
+function unresolvedReservations(tasks: ReservedTask[], result: Record) {
+ const rows = Array.isArray(result.results) ? result.results : []
+ if (rows.length === 0) return tasks
+ const persistedKeys = new Set(rows.map((row) => {
+ if (!row || typeof row !== "object") return ""
+ const item = row as { requestKey?: unknown; persisted?: unknown }
+ return item.persisted === true ? String(item.requestKey || "") : ""
+ }))
+ return tasks.filter((task) => !task.requestKey || !persistedKeys.has(task.requestKey))
+}
+
+function formatBatchDeliveryResult(result: Record) {
+ const summary = String(result.message || "批量投递任务已结束。")
+ const rows = Array.isArray(result.results) ? result.results : []
+ if (rows.length === 0) return summary
+ const details = rows.slice(0, 50).map((row, index) => {
+ const item = row && typeof row === "object"
+ ? row as { id?: unknown; requestKey?: unknown; outcome?: unknown; evidence?: unknown; persisted?: unknown; message?: unknown }
+ : {}
+ const persisted = item.persisted === true ? "已落库" : "待补偿"
+ return `${index + 1}. 岗位 ${String(item.id || "-")} · ${String(item.outcome || "UNKNOWN")} · ${persisted} · ${String(item.evidence || "-")}\n${String(item.message || "")}`
+ })
+ return `${summary}\n\n逐条结果:\n${details.join("\n")}`
+}
+
+async function markUnknownReservations(tasks: ReservedTask[], reason: string) {
+ const results = await Promise.allSettled(tasks.map(async (task) => {
+ if (!task.id || !task.requestKey) return
+ const response = await fetch(`${API_BASE}/api/boss/jobs/${task.id}/delivery-result`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ requestKey: task.requestKey,
+ outcome: "UNKNOWN",
+ evidence: "NO_CONFIRMATION",
+ message: reason,
+ }),
+ })
+ const data = await response.json().catch(() => ({}))
+ if (!response.ok || data.success === false) {
+ throw new Error(data.message || "Boss UNKNOWN 状态回写失败")
+ }
+ }))
+ const failed = results.filter((result) => result.status === "rejected")
+ if (failed.length > 0) console.error("Boss UNKNOWN 状态回写失败", failed)
+}
+
export function useBossDeliveryActions({
filters,
activeScanRunId,
@@ -57,31 +123,88 @@ export function useBossDeliveryActions({
}, [openTextDialog])
const handleConfirmJob = useCallback(async (job: BossJob) => {
+ let reservedTasks: ReservedTask[] = []
try {
setActingJobId(job.id)
- const res = await fetch(`${API_BASE}/api/boss/jobs/${job.id}/confirm`, { method: "POST" })
- const data = await res.json()
+ const ok = window.confirm(`将通过 Chrome 真实联系 Boss HR:${job.companyName || ""} / ${job.jobName || ""}。确认继续?`)
+ if (!ok) return
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/${job.id}/confirm`)
if (!data.success) {
openTextDialog("确认投递", data.message || "该岗位暂不能投递。")
return
}
- const ok = window.confirm(`将通过 Chrome 真实联系 Boss HR:${job.companyName || ""} / ${job.jobName || ""}。确认继续?`)
- if (!ok) return
+ reservedTasks = [data.task]
const result = await sendChromeBridgeMessage({
type: "BOSS_DELIVER_ONE",
platform: "boss",
task: data.task,
}, 120000)
+ if (result.persisted !== true) {
+ await markUnknownReservations(reservedTasks, result.message || "Chrome Bridge 未返回岗位结果")
+ }
openTextDialog("确认投递", result.message || (result.success ? "已发送投递请求。" : "Chrome投递失败。"))
await loadList(page, size)
await refreshStats()
} catch {
+ await markUnknownReservations(reservedTasks, "前端未收到 Chrome 投递执行结果")
openTextDialog("待确认发送", "确认失败:网络或服务异常。")
} finally {
setActingJobId(null)
}
}, [loadList, openTextDialog, page, refreshStats, size])
+ const handleReconcileJob = useCallback(async (job: BossJob) => {
+ const answer = window.prompt(
+ "请先在 Boss 平台核对该岗位。输入“已投递”确认成功,输入“未投递”确认失败;其他内容不会修改状态。",
+ )?.trim()
+ if (answer !== "已投递" && answer !== "未投递") return
+ try {
+ setActingJobId(job.id)
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/${job.id}/delivery-reconcile`, {
+ outcome: answer === "已投递" ? "CONFIRMED" : "FAILED",
+ message: `用户在 Boss 平台人工核对:${answer}`,
+ })
+ openTextDialog("人工对账", data.message || (data.success ? "人工对账已保存。" : "人工对账失败。"))
+ await loadList(page, size)
+ await refreshStats()
+ } catch {
+ openTextDialog("人工对账", "人工对账失败:网络或服务异常。")
+ } finally {
+ setActingJobId(null)
+ }
+ }, [loadList, openTextDialog, page, refreshStats, size])
+
+ const handleRetryJob = useCallback(async (job: BossJob) => {
+ let reservedTasks: ReservedTask[] = []
+ const ok = window.confirm("这会创建新的投递 attempt,并可能再次联系该 Boss HR。确认显式重试?")
+ if (!ok) return
+ try {
+ setActingJobId(job.id)
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/${job.id}/delivery-retry`)
+ if (!data.success || !data.task) {
+ openTextDialog("重试投递", data.message || "当前岗位不能重试。")
+ return
+ }
+ reservedTasks = [data.task]
+ const result = await sendChromeBridgeMessage({
+ type: "BOSS_DELIVER_ONE",
+ platform: "boss",
+ task: data.task,
+ }, 120000)
+ if (result.persisted !== true) {
+ await markUnknownReservations(reservedTasks, result.message || "Chrome 重试结果未确认写入")
+ }
+ openTextDialog("重试投递", result.message || "重试任务已结束。")
+ await loadList(page, size)
+ await refreshStats()
+ } catch {
+ await markUnknownReservations(reservedTasks, "前端未收到 Chrome 重试执行结果")
+ openTextDialog("重试投递", "重试失败:网络或服务异常,已保守标记待对账。")
+ } finally {
+ setActingJobId(null)
+ }
+ }, [loadList, openTextDialog, page, refreshStats, size])
+
const currentBatchFilters = useCallback(() => ({
location: filters.location || undefined,
experience: filters.experience || undefined,
@@ -95,30 +218,32 @@ export function useBossDeliveryActions({
}), [activeScanRunId, filters])
const handleConfirmBatch = useCallback(async () => {
+ let reservedTasks: ReservedTask[] = []
try {
setActingBatch(true)
- const res = await fetch(`${API_BASE}/api/boss/jobs/confirm-batch`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(currentBatchFilters()),
- })
- const data = await res.json()
+ const ok = window.confirm("将通过 Chrome 真实联系当前筛选范围内的 Boss 待确认岗位。确认继续?")
+ if (!ok) return
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/confirm-batch`, currentBatchFilters())
const tasks = data.tasks || []
+ reservedTasks = tasks
if (!data.success || tasks.length === 0) {
openTextDialog("批量投递", data.message || "当前筛选条件下没有待确认岗位。")
return
}
- const ok = window.confirm(`将通过 Chrome 真实联系 ${tasks.length} 个 Boss 待确认岗位。确认继续?`)
- if (!ok) return
const result = await sendChromeBridgeMessage({
type: "BOSS_DELIVER_BATCH",
platform: "boss",
tasks,
}, Math.max(120000, tasks.length * 30000))
- openTextDialog("批量投递", result.message || "批量投递任务已结束。")
+ const unresolved = unresolvedReservations(reservedTasks, result)
+ if (unresolved.length > 0) {
+ await markUnknownReservations(unresolved, result.message || "Chrome Bridge 未确认写入完整批量结果")
+ }
+ openTextDialog("批量投递", formatBatchDeliveryResult(result))
await loadList(page, size)
await refreshStats()
} catch {
+ await markUnknownReservations(reservedTasks, "前端未收到 Chrome 批量投递执行结果")
openTextDialog("批量投递", "批量投递失败:网络或服务异常。")
} finally {
setActingBatch(false)
@@ -126,30 +251,35 @@ export function useBossDeliveryActions({
}, [currentBatchFilters, loadList, openTextDialog, page, refreshStats, size])
const handleConfirmAiRecommendedBatch = useCallback(async () => {
+ let reservedTasks: ReservedTask[] = []
try {
setActingAiBatch(true)
- const res = await fetch(`${API_BASE}/api/boss/jobs/confirm-batch`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ aiRecommendedOnly: true, scanRunId: activeScanRunId || undefined }),
+ const ok = window.confirm("将通过 Chrome 真实联系当前批次中 AI 推荐的 Boss 待确认岗位。确认继续?")
+ if (!ok) return
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/confirm-batch`, {
+ aiRecommendedOnly: true,
+ scanRunId: activeScanRunId || undefined,
})
- const data = await res.json()
const tasks = data.tasks || []
+ reservedTasks = tasks
if (!data.success || tasks.length === 0) {
openTextDialog("AI推荐一键投递", data.message || "当前没有 AI 推荐的待确认岗位。")
return
}
- const ok = window.confirm(`将通过 Chrome 真实联系 ${tasks.length} 个 Boss AI推荐待确认岗位。确认继续?`)
- if (!ok) return
const result = await sendChromeBridgeMessage({
type: "BOSS_DELIVER_BATCH",
platform: "boss",
tasks,
}, Math.max(120000, tasks.length * 30000))
- openTextDialog("AI推荐一键投递", result.message || "AI推荐批量投递任务已结束。")
+ const unresolved = unresolvedReservations(reservedTasks, result)
+ if (unresolved.length > 0) {
+ await markUnknownReservations(unresolved, result.message || "Chrome Bridge 未确认写入完整批量结果")
+ }
+ openTextDialog("AI推荐一键投递", formatBatchDeliveryResult(result))
await loadList(page, size)
await refreshStats()
} catch {
+ await markUnknownReservations(reservedTasks, "前端未收到 Chrome AI 推荐批量投递结果")
openTextDialog("AI推荐一键投递", "AI推荐批量投递失败:网络或服务异常。")
} finally {
setActingAiBatch(false)
@@ -163,39 +293,40 @@ export function useBossDeliveryActions({
return false
}
+ let reservedTasks: ReservedTask[] = []
try {
setActingManualBatch(true)
- const res = await fetch(`${API_BASE}/api/boss/jobs/confirm-batch`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- ids: uniqueIds,
- manualOverrideAiNotMatch: true,
- }),
+ const ok = window.confirm(
+ `AI 已将这些岗位判定为不匹配。你正在按人工判断强制投递 ${uniqueIds.length} 个岗位,`
+ + "将通过 Chrome 真实联系 Boss HR。确认继续?",
+ )
+ if (!ok) return false
+ const data = await postJsonWithRetry(`${API_BASE}/api/boss/jobs/confirm-batch`, {
+ ids: uniqueIds,
+ manualOverrideAiNotMatch: true,
})
- const data = await res.json()
const tasks = data.tasks || []
+ reservedTasks = tasks
if (!data.success || tasks.length === 0) {
openTextDialog("人工投递", data.message || "所选岗位中没有可人工投递的AI不匹配岗位。")
return false
}
- const ok = window.confirm(
- `AI 已将这些岗位判定为不匹配。你正在按人工判断强制投递 ${tasks.length} 个岗位,`
- + `将通过 Chrome 真实联系 Boss HR。确认继续?${data.message ? `\n\n${data.message}` : ""}`,
- )
- if (!ok) return false
-
const result = await sendChromeBridgeMessage({
type: "BOSS_DELIVER_BATCH",
platform: "boss",
tasks,
}, Math.max(120000, tasks.length * 30000))
- openTextDialog("人工投递", result.message || "人工批量投递任务已结束。")
+ const unresolved = unresolvedReservations(reservedTasks, result)
+ if (unresolved.length > 0) {
+ await markUnknownReservations(unresolved, result.message || "Chrome Bridge 未确认写入完整批量结果")
+ }
+ openTextDialog("人工投递", formatBatchDeliveryResult(result))
await loadList(page, size)
await refreshStats()
return true
} catch {
+ await markUnknownReservations(reservedTasks, "前端未收到 Chrome 人工批量投递结果")
openTextDialog("人工投递", "人工批量投递失败:网络或服务异常。")
return false
} finally {
@@ -254,6 +385,8 @@ export function useBossDeliveryActions({
handleConfirmBatch,
handleConfirmAiRecommendedBatch,
handleConfirmManualBatch,
+ handleReconcileJob,
+ handleRetryJob,
handleSkipJob,
clearAnalysisData,
}
diff --git a/front/app/boss/analysis/types.ts b/front/app/boss/analysis/types.ts
index 96b067f..c444f2d 100644
--- a/front/app/boss/analysis/types.ts
+++ b/front/app/boss/analysis/types.ts
@@ -93,7 +93,7 @@ export type FilterState = {
filterHeadhunter: boolean
}
-export const DELIVERY_STATUS_OPTIONS = ["待确认", "LIST_COLLECTED", "AI分析中", "已投递", "未投递", "AI不匹配", "AI分析失败", "采集信息不足", "已过滤", "已跳过", "投递失败"]
+export const DELIVERY_STATUS_OPTIONS = ["待确认", "投递确认中", "投递结果待确认", "LIST_COLLECTED", "AI分析中", "已投递", "未投递", "AI不匹配", "AI分析失败", "采集信息不足", "已过滤", "已跳过", "投递失败"]
export const EXPERIENCE_OPTIONS = ["在校/应届", "1年以内", "1-3年", "3-5年", "5-10年", "10年以上"]
export const DEGREE_OPTIONS = ["不限", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
diff --git a/front/app/boss/analysis/utils.ts b/front/app/boss/analysis/utils.ts
index d39c20e..6ced181 100644
--- a/front/app/boss/analysis/utils.ts
+++ b/front/app/boss/analysis/utils.ts
@@ -59,6 +59,8 @@ export function badgeClass(kind: "delivery" | "hr" | "recruitment", value?: stri
if (kind === "delivery") {
if (v.includes("已投递")) return `${base} bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300`
if (v.includes("待确认")) return `${base} bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300`
+ if (v.includes("投递确认中")) return `${base} bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300`
+ if (v.includes("投递结果待确认")) return `${base} bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300`
if (v === "LIST_COLLECTED") return `${base} bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300`
if (v.includes("AI分析中")) return `${base} bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300`
if (v.includes("采集信息不足")) return `${base} bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300`
diff --git a/front/app/liepin/analysis/AnalysisContent.tsx b/front/app/liepin/analysis/AnalysisContent.tsx
index 9e2dbcb..9255b89 100644
--- a/front/app/liepin/analysis/AnalysisContent.tsx
+++ b/front/app/liepin/analysis/AnalysisContent.tsx
@@ -19,6 +19,8 @@ type StatsResponse = {
total: number
delivered: number
pending: number
+ requested: number
+ unknown: number
filtered: number
failed: number
avgMonthlyK?: number | null
@@ -51,9 +53,16 @@ type LiepinJob = {
hrName?: string
hrTitle?: string
delivered?: number
+ deliveryStatus?: string
createTime?: string
}
+function deliveryStatusOf(job: LiepinJob) {
+ const status = job.deliveryStatus?.trim()
+ if (status) return status
+ return job.delivered === 1 ? "已投递" : "未投递"
+}
+
type PagedResult = {
items: LiepinJob[]
total: number
@@ -227,7 +236,7 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
const [detailJob, setDetailJob] = useState(null)
const [computedSalaryBuckets, setComputedSalaryBuckets] = useState([])
- const statusOptions = ["未投递", "已投递"]
+ const statusOptions = ["未投递", "投递确认中", "投递结果待确认", "已投递", "投递失败"]
useEffect(() => {
loadStats()
@@ -349,7 +358,7 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
it.jobExpReq || "",
it.jobEduReq || "",
it.hrName || "",
- (it.delivered === 1 ? "已投递" : "未投递"),
+ deliveryStatusOf(it),
it.jobLink || "",
it.createTime || "",
])
@@ -466,6 +475,8 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
return [
{ title: "总岗位数", value: k?.total ?? 0 },
{ title: "已投递", value: k?.delivered ?? 0 },
+ { title: "投递确认中", value: k?.requested ?? 0 },
+ { title: "结果待确认", value: k?.unknown ?? 0 },
{ title: "未投递", value: k?.pending ?? 0 },
{ title: "平均月薪(K)", value: (k?.avgMonthlyK ?? avgMonthlyKFromItems ?? 0) },
]
@@ -771,7 +782,7 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
{it.jobEduReq || ""} |
{it.hrName || ""} |
- {it.delivered === 1 ? "已投递" : "未投递"}
+ {deliveryStatusOf(it)}
|
{it.jobLink ? (
@@ -804,7 +815,7 @@ export default function AnalysisContent({ showHeader = false }: { showHeader?: b
经验:{detailJob.jobExpReq || ""}
学历:{detailJob.jobEduReq || ""}
HR:{detailJob.hrName || ""}
- 状态:{detailJob.delivered === 1 ? "已投递" : "未投递"}
+ 状态:{deliveryStatusOf(detailJob)}
创建时间:{formatDateOnly(detailJob.createTime)}
diff --git a/front/app/zhilian/analysis/AnalysisContent.tsx b/front/app/zhilian/analysis/AnalysisContent.tsx
index 03a19a5..49e342b 100644
--- a/front/app/zhilian/analysis/AnalysisContent.tsx
+++ b/front/app/zhilian/analysis/AnalysisContent.tsx
@@ -93,6 +93,76 @@ type PagedResult = {
size: number
}
+async function markZhilianUnknownReservations(
+ tasks: Array<{ id?: number; requestKey?: string }>,
+ reason: string,
+) {
+ const results = await Promise.allSettled(tasks.map(async (task) => {
+ if (!task.id || !task.requestKey) return
+ const response = await fetch(`${API_BASE}/api/zhilian/jobs/${task.id}/delivery-result`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ requestKey: task.requestKey,
+ outcome: "UNKNOWN",
+ evidence: "NO_CONFIRMATION",
+ message: reason,
+ }),
+ })
+ const data = await response.json().catch(() => ({}))
+ if (!response.ok || data.success === false) {
+ throw new Error(data.message || "智联 UNKNOWN 状态回写失败")
+ }
+ }))
+ const failed = results.filter((result) => result.status === "rejected")
+ if (failed.length > 0) console.error("智联 UNKNOWN 状态回写失败", failed)
+}
+
+async function postZhilianJsonWithRetry(url: string, body?: unknown) {
+ let lastError: unknown
+ for (let attempt = 0; attempt < 2; attempt += 1) {
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: body === undefined ? undefined : { "Content-Type": "application/json" },
+ body: body === undefined ? undefined : JSON.stringify(body),
+ })
+ return await response.json()
+ } catch (error) {
+ lastError = error
+ }
+ }
+ throw lastError instanceof Error ? lastError : new Error("智联投递请求未收到响应")
+}
+
+function unresolvedZhilianReservations(
+ tasks: Array<{ id?: number; requestKey?: string }>,
+ result: Record ,
+) {
+ const rows = Array.isArray(result.results) ? result.results : []
+ if (rows.length === 0) return tasks
+ const persistedKeys = new Set(rows.map((row) => {
+ if (!row || typeof row !== "object") return ""
+ const item = row as { requestKey?: unknown; persisted?: unknown }
+ return item.persisted === true ? String(item.requestKey || "") : ""
+ }))
+ return tasks.filter((task) => !task.requestKey || !persistedKeys.has(task.requestKey))
+}
+
+function formatZhilianBatchResult(result: Record) {
+ const summary = String(result.message || "批量投递任务已结束。")
+ const rows = Array.isArray(result.results) ? result.results : []
+ if (rows.length === 0) return summary
+ const details = rows.slice(0, 50).map((row, index) => {
+ const item = row && typeof row === "object"
+ ? row as { id?: unknown; outcome?: unknown; evidence?: unknown; persisted?: unknown; message?: unknown }
+ : {}
+ const persisted = item.persisted === true ? "已落库" : "待补偿"
+ return `${index + 1}. 岗位 ${String(item.id || "-")} · ${String(item.outcome || "UNKNOWN")} · ${persisted} · ${String(item.evidence || "-")}\n${String(item.message || "")}`
+ })
+ return `${summary}\n\n逐条结果:\n${details.join("\n")}`
+}
+
type ChartRef = { destroy: () => void }
const CATEGORY_COLORS = [
@@ -528,7 +598,7 @@ export default function AnalysisContent({ showHeader = false, refreshSignal = 0
const [pendingCardsExpanded, setPendingCardsExpanded] = useState(false)
const activeScanRunId = ""
- const statusOptions = ["待确认", "AI分析中", "未投递", "已投递", "已过滤", "投递失败", "AI不匹配", "AI分析失败"]
+ const statusOptions = ["待确认", "投递确认中", "投递结果待确认", "AI分析中", "未投递", "已投递", "已过滤", "投递失败", "AI不匹配", "AI分析失败"]
const loadList = async (toPage = page, toSize = size) => {
try {
@@ -785,58 +855,121 @@ export default function AnalysisContent({ showHeader = false, refreshSignal = 0
alert("该智联岗位缺少内部ID,无法确认投递。")
return
}
+ let reservedTasks: Array<{ id?: number; requestKey?: string }> = []
try {
setActingJobId(job.id)
- const res = await fetch(`${API_BASE}/api/zhilian/jobs/${job.id}/confirm`, { method: "POST" })
- const data = await res.json()
+ const ok = window.confirm(`将通过 Chrome 真实申请智联岗位:${job.companyName || ""} / ${job.jobTitle || ""}。确认继续?`)
+ if (!ok) return
+ const data = await postZhilianJsonWithRetry(`${API_BASE}/api/zhilian/jobs/${job.id}/confirm`)
if (!data.success) {
alert(data.message || "该智联岗位暂不能投递。")
return
}
- const ok = window.confirm(`将通过 Chrome 真实申请智联岗位:${job.companyName || ""} / ${job.jobTitle || ""}。确认继续?`)
- if (!ok) return
+ reservedTasks = [data.task]
const result = await sendChromeBridgeMessage({
type: "ZHILIAN_DELIVER_ONE",
platform: "zhilian",
task: data.task,
}, 120000)
+ if (result.persisted !== true) {
+ await markZhilianUnknownReservations(reservedTasks, result.message || "Chrome Bridge 未返回岗位结果")
+ }
alert(result.message || (result.success ? "已发送投递请求。" : "Chrome投递失败。"))
await loadList(page, size)
await loadStats()
await loadDashboardStats()
} catch {
+ await markZhilianUnknownReservations(reservedTasks, "前端未收到 Chrome 投递执行结果")
alert("确认投递失败:网络或服务异常。")
} finally {
setActingJobId(null)
}
}
+ const handleReconcileJob = async (job: ZhilianJob) => {
+ if (!job.id) return
+ const answer = window.prompt(
+ "请先在智联平台核对该岗位。输入“已投递”确认成功,输入“未投递”确认失败;其他内容不会修改状态。",
+ )?.trim()
+ if (answer !== "已投递" && answer !== "未投递") return
+ try {
+ setActingJobId(job.id)
+ const data = await postZhilianJsonWithRetry(`${API_BASE}/api/zhilian/jobs/${job.id}/delivery-reconcile`, {
+ outcome: answer === "已投递" ? "CONFIRMED" : "FAILED",
+ message: `用户在智联平台人工核对:${answer}`,
+ })
+ alert(data.message || (data.success ? "人工对账已保存。" : "人工对账失败。"))
+ await loadList(page, size)
+ await loadStats()
+ await loadDashboardStats()
+ } catch {
+ alert("人工对账失败:网络或服务异常。")
+ } finally {
+ setActingJobId(null)
+ }
+ }
+
+ const handleRetryJob = async (job: ZhilianJob) => {
+ if (!job.id) return
+ const ok = window.confirm("这会创建新的投递 attempt,并可能再次申请该智联岗位。确认显式重试?")
+ if (!ok) return
+ let reservedTasks: Array<{ id?: number; requestKey?: string }> = []
+ try {
+ setActingJobId(job.id)
+ const data = await postZhilianJsonWithRetry(`${API_BASE}/api/zhilian/jobs/${job.id}/delivery-retry`)
+ if (!data.success || !data.task) {
+ alert(data.message || "当前岗位不能重试。")
+ return
+ }
+ reservedTasks = [data.task]
+ const result = await sendChromeBridgeMessage({
+ type: "ZHILIAN_DELIVER_ONE",
+ platform: "zhilian",
+ task: data.task,
+ }, 120000)
+ if (result.persisted !== true) {
+ await markZhilianUnknownReservations(reservedTasks, result.message || "Chrome 重试结果未确认写入")
+ }
+ alert(result.message || "重试任务已结束。")
+ await loadList(page, size)
+ await loadStats()
+ await loadDashboardStats()
+ } catch {
+ await markZhilianUnknownReservations(reservedTasks, "前端未收到 Chrome 重试执行结果")
+ alert("重试失败:网络或服务异常,已保守标记待对账。")
+ } finally {
+ setActingJobId(null)
+ }
+ }
+
const handleConfirmBatch = async () => {
+ let reservedTasks: Array<{ id?: number; requestKey?: string }> = []
try {
setActingBatch(true)
- const res = await fetch(`${API_BASE}/api/zhilian/jobs/confirm-batch`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(currentBatchFilters()),
- })
- const data = await res.json()
+ const ok = window.confirm("将通过 Chrome 真实申请当前筛选范围内的智联待确认岗位。确认继续?")
+ if (!ok) return
+ const data = await postZhilianJsonWithRetry(`${API_BASE}/api/zhilian/jobs/confirm-batch`, currentBatchFilters())
const tasks = data.tasks || []
+ reservedTasks = tasks
if (!data.success || tasks.length === 0) {
alert(data.message || "当前筛选条件下没有智联待确认岗位。")
return
}
- const ok = window.confirm(`将通过 Chrome 真实申请 ${tasks.length} 个智联待确认岗位。确认继续?`)
- if (!ok) return
const result = await sendChromeBridgeMessage({
type: "ZHILIAN_DELIVER_BATCH",
platform: "zhilian",
tasks,
}, Math.max(120000, tasks.length * 30000))
- alert(result.message || "批量投递任务已结束。")
+ const unresolved = unresolvedZhilianReservations(reservedTasks, result)
+ if (unresolved.length > 0) {
+ await markZhilianUnknownReservations(unresolved, result.message || "Chrome Bridge 未确认写入完整批量结果")
+ }
+ alert(formatZhilianBatchResult(result))
await loadList(page, size)
await loadStats()
await loadDashboardStats()
} catch {
+ await markZhilianUnknownReservations(reservedTasks, "前端未收到 Chrome 批量投递执行结果")
alert("批量投递失败:网络或服务异常。")
} finally {
setActingBatch(false)
@@ -1180,14 +1313,27 @@ export default function AnalysisContent({ showHeader = false, refreshSignal = 0
}`}
>
- {it.deliveryStatus === "待确认" ? (
+ {it.deliveryStatus === "待确认" || it.deliveryStatus === "投递确认中" ? (
handleConfirmJob(it)}
className="h-7 rounded-lg px-3 text-xs"
>
- Chrome投递
+ {it.deliveryStatus === "投递确认中" ? "恢复投递" : "Chrome投递"}
+
+ ) : it.deliveryStatus === "投递结果待确认" ? (
+
+ handleReconcileJob(it)} className="h-7 rounded-lg px-3 text-xs">
+ 对账
+
+ handleRetryJob(it)} className="h-7 rounded-lg px-3 text-xs">
+ 重试
+
+
+ ) : it.deliveryStatus === "投递失败" ? (
+ handleRetryJob(it)} className="h-7 rounded-lg px-3 text-xs">
+ 重试
) : (it.deliveryStatus || "").trim() === "已投递" ? (
diff --git a/src/main/java/com/getjobs/application/controller/BossAnalyticsController.java b/src/main/java/com/getjobs/application/controller/BossAnalyticsController.java
index 52fffc0..c8a6e58 100644
--- a/src/main/java/com/getjobs/application/controller/BossAnalyticsController.java
+++ b/src/main/java/com/getjobs/application/controller/BossAnalyticsController.java
@@ -5,6 +5,7 @@
import com.getjobs.application.dto.DeliveryResultRequest;
import com.getjobs.application.entity.BossConfigEntity;
import com.getjobs.application.service.DeliveryStatus;
+import com.getjobs.application.service.DeliveryAttemptService;
import com.getjobs.application.service.BossService;
import com.getjobs.application.service.BossStatsService;
import org.springframework.web.bind.annotation.*;
@@ -23,10 +24,14 @@ public class BossAnalyticsController {
private final BossService bossService;
private final BossStatsService bossStatsService;
+ private final DeliveryAttemptService deliveryAttemptService;
- public BossAnalyticsController(BossService bossService, BossStatsService bossStatsService) {
+ public BossAnalyticsController(BossService bossService,
+ BossStatsService bossStatsService,
+ DeliveryAttemptService deliveryAttemptService) {
this.bossService = bossService;
this.bossStatsService = bossStatsService;
+ this.deliveryAttemptService = deliveryAttemptService;
}
/**
@@ -128,10 +133,16 @@ public Map confirmPendingJob(@PathVariable("id") Long id) {
BossJobDataEntity job = bossService.getBossJobById(id);
Map error = validateDeliverable(job);
if (error != null) return error;
+ DeliveryAttemptService.RequestResult attempt = deliveryAttemptService.requestBoss(
+ job.getId(), job.getProfileId(), firstNonBlank(job.getEncryptId(), String.valueOf(job.getId())), false);
+ if (!attempt.accepted()) {
+ return Map.of("success", false, "message", attempt.message(), "status", Objects.toString(job.getDeliveryStatus(), ""));
+ }
return Map.of(
"success", true,
- "message", "请在 Chrome 中确认投递该岗位",
- "task", toDeliveryTask(job)
+ "resumed", !attempt.created(),
+ "message", attempt.created() ? "投递请求已创建,请在 Chrome 中等待平台确认" : "已恢复原投递请求,请勿重复创建",
+ "task", toDeliveryTask(job, attempt.requestKey())
);
}
@@ -170,7 +181,7 @@ public Map confirmBatch(@RequestBody ConfirmBatchRequest request
}
} else if (aiRecommendedOnly) {
BossService.PagedResult page = bossService.listBossJobs(
- List.of(DeliveryStatus.WAITING_CONFIRM),
+ List.of(DeliveryStatus.WAITING_CONFIRM, DeliveryStatus.DELIVERY_REQUESTED),
null,
null,
null,
@@ -190,7 +201,7 @@ public Map confirmBatch(@RequestBody ConfirmBatchRequest request
}
} else {
BossService.PagedResult page = bossService.listBossJobs(
- List.of(DeliveryStatus.WAITING_CONFIRM),
+ List.of(DeliveryStatus.WAITING_CONFIRM, DeliveryStatus.DELIVERY_REQUESTED),
request == null ? null : request.getLocation(),
request == null ? null : request.getExperience(),
request == null ? null : request.getDegree(),
@@ -206,14 +217,27 @@ public Map confirmBatch(@RequestBody ConfirmBatchRequest request
if (page != null && page.items != null) candidates.addAll(page.items);
}
- List | | |