Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
R
recruit-sys
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
李文光
recruit-sys
Commits
d392f9db
Commit
d392f9db
authored
Aug 31, 2026
by
李文光
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 业务数据仅存数据库,移除浏览器本地兜底与 localStorage 主事实源
parent
61e871cb
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
23 additions
and
37 deletions
+23
-37
README.md
README.md
+1
-1
project_handoff.md
docs/project_handoff.md
+3
-3
recruitment.js
vue-app/src/api/recruitment.js
+0
-5
AppShell.vue
vue-app/src/layouts/AppShell.vue
+2
-0
recruitment.js
vue-app/src/stores/recruitment.js
+17
-28
No files found.
README.md
View file @
d392f9db
...
...
@@ -139,7 +139,7 @@ backend\.venv\Scripts\python.exe backend/scripts/start_server.py --with-frontend
-
**
QwenPaw 接入(可选)
**
:JD 生成 / 简历分析可走独立的 QwenPaw 智能体服务(SSE 流式),失败自动回退。
-
**
对外 JD 同步
**
:接收企微 QwenPaw 产出的 JD 并创建 / 更新岗位(
`/
api/jd/sync-external
`)
。
-
**
录用入职管理
**
:Offer 草稿生成、审批材料与待入职跟进。
-
**
数据持久化与导入导出
**
:
状态存 SQLite,支持
`G
ET/PUT /api/state
`、
localStorage 导入
、JSON 备份导出。
-
**
数据持久化与导入导出
**
:
业务数据以 SQLite 为唯一事实源(前端不使用浏览器本地缓存兜底),支持
`G
ET/PUT /api/state
`
整包读写
、JSON 备份导出。
-
**
简历采集接入
**
:浏览器扩展或外部系统通过
`/
api/ingest
`
推送简历,支持幂等队列、PDF 下载与解析。
## API 概览
...
...
docs/project_handoff.md
View file @
d392f9db
...
...
@@ -134,7 +134,7 @@ recruit-sys/
### 4.3 前端架构
-
Vue 3 Composition API + Pinia + Vue Router + Element Plus;无 TypeScript,无组件测试。
-
**状态同步策略**
:
`stores/recruitment.js`
以
localStorage(key 按工作区分:
`recruitment-system-<owner>-v1`
,开放模式
`local`
沿用旧 key
`recruitment-system-mvp-v1`
,见
[
constants.js
](
vue-app/src/utils/constants.js
)
)为主事实源,向后端
`/api/state`
全量保存;启动时尝试读取后端状态,两者有冲突时以后端为准(
`meta.persisted`
为 true 时用后端)
。
-
**状态同步策略**
:
`stores/recruitment.js`
以
数据库为唯一事实源——启动时从
`GET /api/state`
整包加载,保存时
`PUT /api/state`
全量写入;
**不再使用浏览器 localStorage 缓存业务数据**
(localStorage 仅存登录令牌
`recruitment-system-auth-v1`
)。数据库不可用时显示空工作区并提示「数据库连接失败」,不回退本地缓存
。
-
**登录态**
:
`stores/user.js`
存 Bearer 令牌与用户信息(key
`recruitment-system-auth-v1`
);axios 请求拦截器自动注入
`Authorization: Bearer`
,401 时清空凭证并跳
`/login`
;路由守卫
`router/index.js`
的
`beforeEach`
做未登录/已登录/角色(admin)三层跳转。
-
**工作区切换(admin)**
:AppShell 顶栏工作区下拉选择目标用户,
`recruitment`
store 的
`switchWorkspace()`
先保存当前区、再整包加载目标区数据。
-
大量业务逻辑在
`src/utils/`
(22 个纯函数模块):匹配打分、字段推断、JD 质量、任务流程、Offer 模板等。规则与后端
[
resume_parser_core.py
](
backend/app/services/resume_parser_core.py
)
共享同一份学校/专业/技能词典(
`shared/resume-dictionaries.json`
),改一处即可。
...
...
@@ -276,7 +276,7 @@ backend\.venv\Scripts\python.exe -m pytest -c backend/pyproject.toml
| 简历文件 |
`./uploads/resumes/`
(
`FILES_DIR`
可改) | hash 命名,
`GET /api/resumes/{storedName}`
下载 |
| 状态快照 |
`app_state_meta.raw_snapshot`
| 整包 JSON 备份(按工作区一行),空库时回读 |
| 导出备份 |
`GET /api/export/state`
| 下载
`recruitment-backup-YYYY-MM-DD.json`
,可经
`/api/import/local-state`
导回(当前/指定工作区) |
|
前端 localStorage | key
`recruitment-system-<owner>-v1`
(开放模式
`local`
用旧 key
`recruitment-system-mvp-v1`
) | 前端主事实源,向后端全量同步
|
|
业务数据 | 数据库(SQLite) | 唯一事实源;前端不使用浏览器本地缓存兜底
|
| 前端登录态 | key
`recruitment-system-auth-v1`
| Bearer 令牌 + 用户信息 |
---
...
...
@@ -292,7 +292,7 @@ backend\.venv\Scripts\python.exe -m pytest -c backend/pyproject.toml
-
**状态同步是「整包覆盖」而非增量**
:
`PUT /api/state`
会按当前工作区全表删后重建(
[
state_repository.py 的 replace_state
](
backend/app/repositories/state_repository.py
)
)。
**同一工作区内**
多端同时编辑会互相覆盖(last-write-wins),建议同一工作区单用户或串行使用;不同用户之间已按
`owner_id`
隔离。
-
**令牌无状态、不主动吊销**
:登录令牌是 HMAC 签名的无状态 Bearer,重置密码不会使已签发令牌立即失效(禁用用户会因
`is_active`
检查立即失效);如需要严格吊销需引入 sessions 表(后续可做)。
-
**
`localStorage` 与后端状态的一致性**
:前端先写 localStorage 再同步后端;若 localStorage 有旧数据且后端空库,会以 localStorage 为准导入(
`/api/import/local-state`
逻辑存在)
。
**清浏览器数据前请先导出备份**
(
`GET /api/export/state`
)。
-
**
业务数据以数据库为唯一事实源**
:前端启动时从
`GET /api/state`
整包加载,保存时
`PUT /api/state`
全量写入;不使用浏览器本地缓存兜底,数据库不可用时提示「数据库连接失败」并显示空工作区
。
**清浏览器数据前请先导出备份**
(
`GET /api/export/state`
)。
-
**简历解析是纯规则、无 OCR**
:扫描件/图片型 PDF 提取不到文本,
`parse_status`
会标记
`needs_manual`
,需人工补录(见
[
ingest_repository.py
](
backend/app/repositories/ingest_repository.py
)
的
`parse_status`
判断)。解析器面向中文文本简历优化,英文简历字段提取能力弱。
-
**`reference/` 是外部参考副本**
:
`browser-extension/reference/`
下的 boss-sync-plugin 是别家方案的复制品,仅作参考,不参与运行,也不应被当作本项目代码维护(已进 .gitignore)。
...
...
vue-app/src/api/recruitment.js
View file @
d392f9db
...
...
@@ -10,11 +10,6 @@ export async function saveState(state, owner = '') {
return
data
}
export
async
function
importLocalState
(
state
,
owner
=
''
)
{
const
{
data
}
=
await
http
.
post
(
'/api/import/local-state'
,
state
,
{
params
:
owner
?
{
owner
}
:
{}
})
return
data
}
export
async
function
parseResume
(
file
)
{
const
formData
=
new
FormData
()
formData
.
append
(
'resume'
,
file
)
...
...
vue-app/src/layouts/AppShell.vue
View file @
d392f9db
...
...
@@ -160,6 +160,8 @@ async function loadWorkspaceOptions() {
}
onMounted
(()
=>
{
// 先校正工作区归属(登录/换号后以当前用户为准),再加载数据库数据
store
.
syncOwner
(
userStore
.
workspaceOwner
)
store
.
boot
()
loadWorkspaceOptions
()
})
...
...
vue-app/src/stores/recruitment.js
View file @
d392f9db
import
{
defineStore
}
from
'pinia'
import
{
AUTH_STORAGE_KEY
,
workspaceStorageKey
,
seedState
}
from
'@/utils/constants'
import
{
AUTH_STORAGE_KEY
,
seedState
}
from
'@/utils/constants'
import
*
as
api
from
'@/api/recruitment'
import
{
enrichCandidateFields
,
...
...
@@ -26,6 +26,9 @@ import { isAiEvaluationReport } from '@/utils/scoring'
import
{
matchedJobForCandidate
,
candidateJobTitle
}
from
'@/utils/links'
import
{
createOfferRecord
}
from
'@/utils/normalize'
// 业务数据一律以数据库为准,不使用浏览器本地缓存;空工作区用这个骨架初始化
const
EMPTY_STATE
=
{
jobs
:
[],
candidates
:
[],
offers
:
[],
tasks
:
[],
eventLog
:
[]
}
function
buildRecruitmentEvent
(
type
,
candidate
=
{},
extra
=
{})
{
const
at
=
extra
.
at
||
new
Date
().
toISOString
()
return
{
...
...
@@ -212,18 +215,9 @@ function initialWorkspaceOwner() {
export
const
useRecruitmentStore
=
defineStore
(
'recruitment'
,
{
state
:
()
=>
{
const
owner
=
initialWorkspaceOwner
()
const
storageKey
=
workspaceStorageKey
(
owner
)
let
initial
=
seedState
try
{
const
raw
=
localStorage
.
getItem
(
storageKey
)
if
(
raw
)
initial
=
JSON
.
parse
(
raw
)
}
catch
{
/* ignore */
}
return
{
state
:
normalizeState
(
initial
),
state
:
normalizeState
(
EMPTY_STATE
),
workspaceOwner
:
owner
,
storageKey
,
activePage
:
'todo'
,
saveStateText
:
'本地已保存'
,
loading
:
false
,
...
...
@@ -276,28 +270,26 @@ export const useRecruitmentStore = defineStore('recruitment', {
actions
:
{
async
boot
()
{
this
.
loading
=
true
const
localRaw
=
localStorage
.
getItem
(
this
.
storageKey
)
try
{
const
payload
=
await
api
.
fetchState
(
this
.
workspaceOwner
)
if
(
payload
.
meta
?.
empty
&&
localRaw
)
{
await
api
.
importLocalState
(
this
.
state
,
this
.
workspaceOwner
)
this
.
saveStateText
=
'后端数据库已连接'
return
}
this
.
applyLoadedState
(
payload
)
this
.
saveStateText
=
'后端数据库已连接'
}
catch
{
try
{
this
.
applyLoadedState
(
localRaw
?
JSON
.
parse
(
localRaw
)
:
seedState
)
}
catch
{
this
.
applyLoadedState
(
seedState
)
}
this
.
saveStateText
=
'使用浏览器本地兜底数据'
// 数据一律以数据库为准:数据库不可用时显示空工作区,不再回退浏览器本地缓存
this
.
applyLoadedState
(
EMPTY_STATE
)
this
.
saveStateText
=
'数据库连接失败'
}
finally
{
this
.
loading
=
false
}
},
// 登录/挂载后校正工作区归属:以当前登录用户为准,避免退出后换号登录残留旧工作区
syncOwner
(
owner
=
''
)
{
if
(
!
owner
||
owner
===
this
.
workspaceOwner
)
return
this
.
workspaceOwner
=
owner
this
.
applyLoadedState
(
EMPTY_STATE
)
},
// 切换工作区(admin 查看/操作其他用户数据):先落当前区,再整包加载目标区
async
switchWorkspace
(
owner
=
'local'
)
{
if
(
!
owner
||
owner
===
this
.
workspaceOwner
)
return
...
...
@@ -307,9 +299,7 @@ export const useRecruitmentStore = defineStore('recruitment', {
/* 保存失败不阻断切换 */
}
this
.
workspaceOwner
=
owner
this
.
storageKey
=
workspaceStorageKey
(
owner
)
const
localRaw
=
localStorage
.
getItem
(
this
.
storageKey
)
this
.
applyLoadedState
(
localRaw
?
JSON
.
parse
(
localRaw
)
:
seedState
)
this
.
applyLoadedState
(
EMPTY_STATE
)
await
this
.
boot
()
},
...
...
@@ -323,7 +313,6 @@ export const useRecruitmentStore = defineStore('recruitment', {
persist
(
message
=
'已保存'
)
{
this
.
state
.
updatedAt
=
new
Date
().
toISOString
()
localStorage
.
setItem
(
this
.
storageKey
,
JSON
.
stringify
(
this
.
state
))
this
.
saveStateText
=
message
this
.
saveStateToServer
()
},
...
...
@@ -334,7 +323,7 @@ export const useRecruitmentStore = defineStore('recruitment', {
if
(
payload
.
state
?.
updatedAt
)
this
.
state
.
updatedAt
=
payload
.
state
.
updatedAt
this
.
saveStateText
=
'已保存'
}
catch
{
this
.
saveStateText
=
'
本地暂存,后端未保存
'
this
.
saveStateText
=
'
保存失败,请检查后端连接
'
}
},
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment