init: 数字员工平台初始代码

包含前端(Vue3 + VueFlow 画布)、后端(Go)、文档体系。
- 工作台画布:节点拖放、连线模式、右键菜单、AI 助手
- 后端:连接器 API、专员种子数据
- 导航:左侧导航、工坊、市场、控制台
This commit is contained in:
eaiadmin
2026-08-18 20:19:58 +08:00
commit 4e8817d768
239 changed files with 48631 additions and 0 deletions
@@ -0,0 +1,383 @@
# pj0231-eaisalestraining 完整分析报告(对比 + 差距 + 路线图 + 设计稿)
> 版本:V1.0 | 日期:2026-08-16
> 对比对象:pj006-zhilianyuan2(智练猿)
> 范围:知识库能力、考试能力、测试/自测能力、岗位与知识对应能力
> 原则:以**实际代码**为准(本项目 PRD 文档存在过时,见 §1.3)
>
> **注:** 本文基于 2026-08-16 代码快照(后端 Go + Gin + GORM,检索为 brute-force 余弦 + bge-m3);数据库与向量栈已决策迁移至 **MySQL 8.0 + FAISS**,见 `docs/db_schema.md` / `docs/deploy.md`。
---
## 目录
- Part A —— 现状对比
- Part B —— 差距汇总与反向差异
- Part C —— 改进路线图
- Part D —— 设计稿:岗位与知识对应能力(核心)
- Part E —— 设计要点:其余改进项
- Part F —— 待拍板决策点与下一步
---
# Part A —— 现状对比
## A1. 一句话结论
两个项目**不是同一量级**:
- **pj0231** 是一个**极简单租户、内部专属培训平台**——Go 后端约 4,900 行,11 张表,2 个角色,聚焦「看资料 → AI 答疑 → 考试验收」的窄闭环。
- **pj006** 是一个**多租户、多角色、带完整 AI 对练 / 岗位能力模型的通用销售培训平台**——Python 后端约 1.7 万行 + 独立 engine 库,23 版 schema、40+ 张表,4 个前端。
在四个对比维度上,pj006 全部**能力更全、更深**;但 pj0231 在个别点上有「更实」的实现(见 §B3)。
## A2. 系统定位与技术架构
| 维度 | pj0231 | pj006 |
|---|---|---|
| 定位 | 博昇内部培训(单企业) | 通用 AI 销售培训平台(多企业 SaaS) |
| 后端 | Go + Gin + GORM + SQLite | Python 3.10 + FastAPI + SQLite(per-org 分库) |
| 前端 | 1 个:Vue3 + Vite + Element Plus | 4 个:app / orgadmin / admin / platformadmin |
| 角色 | 2(employee / admin) | 多级(学员 / 组织管理员 oadmin / 平台管理员 padmin / 教练 trainer) |
| 租户 | 单租户 | 多租户(`orgs` 注册表 + 每 org 一个 `<org>.db`) |
| 代码量 | ~4,900 行 Go | ~17,200 行 Python + `zlylibs/engine` |
| 数据表 | 11 张 | 40+ 张(schema v23) |
| AI 底座 | 内网 OpenAI 兼容接口(Ollama LLM + bge-m3) | OpenAI 兼容路由(trainee/coach/eval 三 slot) |
## A3. 重要前提:pj0231 文档已过时
- `docs/PRD.md` 写「FastAPI + MySQL + 不做向量检索」;实际代码是 **Go + Gin + GORM + SQLite**,检索已实现**向量(brute-force 余弦 + bge-m3)**。
- 本项目权威信息以 `eaisalestrain_app/CLAUDE.md` 与**实际代码**为准。
---
## A4. 知识库能力对比
| 能力 | pj0231 | pj006 |
|---|---|---|
| 素材摄入 | ✅ 上传→审批→转换→切片(真实跑通) | ⚠️ 知识工厂骨架完备,Phase 1 解析抽取「不真跑、停 pending」 |
| 文档转换 | LibreOffice + pdftotext,切片入 `knowledge_chunk` | 设计 parse/structured/extraction_blocks 多阶段管线,未实跑 |
| 审批流 | ✅ `pending/approved/rejected`,审批前置 | ✅ 更细:`working_knowledge_points` 待复核 + `kb_publish_records` 发布链 |
| 知识组织 | 扁平 `knowledge_chunk` + `knowledge_source` | 多层:`knowledge_points` → `knowledge_meta` → `knowledge_units`(统一主干)→ `knowledge_chunks`(带 `embedding_status`) |
| 术语/规则 | ❌ 无 | ✅ `knowledge_terms`、`industry_rules` |
| 检索 | ✅ **混合检索**(向量余弦 + 关键词兜底) | ⚠️ 实跑 jieba 分词 + LIKE;向量仅文档宣称 |
| 自动出题 | ❌ 无 | ✅ 设计:`working_questions` 自动生成(待复核) |
| 知识-题目/场景关联 | 仅题目挂 `course_id` | ✅ `question_knowledge_units`、`scene_knowledge_units`、`knowledge_unit_links` |
## A5. 考试能力对比
| 能力 | pj0231 | pj006 |
|---|---|---|
| 题型 | 单选 / 多选 / 判断 | 单选 / 多选 / 判断 / **简答 essay** |
| 简答评分 | ❌ 无 | ✅ LLM 按 rubric 评分,得分率持久化 `exam_session_grades` |
| 组卷 | ✅ 按知识域 + 随机抽题 | ⚠️ 固定 `question_ids` 列表;蓝图组卷未接考试服务 |
| 考试次数 | 正式考只允许一次 | ✅ `max_attempts` + `deadline` |
| 自测 | ✅ 自测(即时对错、不存记录) | ✅ 练习走独立 `practice` 模块 |
| 判分 | 确定性比对 | 选择题确定性 + 简答 LLM |
| 成绩分析 | 仅存得分/正确率/明细 | ✅ 维度能力分析(weak/ok/strong)+ 薄弱题 + 改进建议 |
| 错题联动 | ❌ 无 | ✅ 交卷错题自动入 `mistake_records` |
## A6. 测试 / 自测 / 评估能力对比
### A6.1 面向学员(产品能力)
| 能力 | pj0231 | pj006 |
|---|---|---|
| 自测练习 | ✅ 自测(不存记录) | ✅ 每日一练 `practice`(游戏化:段位/星/连击/经验/积分) |
| 错题本 | ❌ 无 | ✅ `mistake_records`(练习+考试聚合) |
| 笔记/收藏 | ❌ 无 | ✅ `study_notes` + 收藏 |
| 学习档案/学情 | ❌ **PRD 明确禁止** | ✅ 学习档案 dashboard + 能力对标 |
| AI 对练复盘评分 | ❌ 无 | ✅ `dialogue_review_scores` + `ReviewScoreService` |
### A6.2 面向软件工程(自动化测试)
| 维度 | pj0231 | pj006 |
|---|---|---|
| 后端测试 | ❌ **0 个 `_test.go`** | ✅ 31 个 pytest 文件 |
| 引擎测试 | 无 engine 层 | ✅ `zlylibs/engine/tests` 约 20 个 |
| 测试策略文档 | ❌ 无 | ✅ `docs/12_Test_Strategy` |
| CI/依赖管理 | 无 | ✅ pyproject dev deps |
## A7. 岗位与知识对应能力(差距最大维度)
pj0231 是**结构性缺失**:用户只有 `role`,完全没有岗位概念,也没有岗位→知识→考试的能力链。
| 能力 | pj0231 | pj006 |
|---|---|---|
| 岗位表 | ❌ 无 | ✅ `positions` |
| 岗位↔用户绑定 | ❌ 无 | ✅ `user_positions` |
| 岗位能力模型(洋葱) | ❌ 无 | ✅ `competency_layers` + `competency_levels`(L1-L4)+ `competency_details` |
| 能力模型自动生成 | ❌ 无 | ✅ `generate_competency_model` |
| 岗位↔知识映射 | ❌ 无 | ✅ `position_knowledge_units` + `auto_match` |
| 岗位考试蓝图 | ❌ 无 | ✅ `position_exam_blueprints` |
| 标准岗位/职级代码 | ❌ 无 | ✅ `SD01_Standard_Job_Codes` |
| 岗位驱动学练考 | ❌ 无 | ✅ A11 学习 / A12 练习 / A13 蓝图组卷 |
---
# Part B —— 差距汇总与反向差异
## B1. 差距严重度总表
| 维度 | pj0231 现状 | 差距严重度 | 是否 pj0231 定位内该补 |
|---|---|---|---|
| 岗位与知识对应 | 完全缺失 | 🔴 高 | ✅ 是(本次核心诉求) |
| 错题本 / 学习闭环 | 完全缺失 | 🟠 中高 | ✅ 是(考试「考完即止」无闭环) |
| 简答题 + LLM 评分 | 缺失 | 🟠 中 | ✅ 是(LLM 底座已有,成本低) |
| 自动化测试 | 0 测试 | 🟠 中高(工程风险) | ✅ 是 |
| 多租户 / NPC 人格 / KPI | 缺失 | 🔵 低 | ❌ 否(与「极简内网单企业」冲突) |
| 向量检索 | ✅ 已实现 | — | —(反而领先 pj006) |
| 随机 + 按域组卷 | ✅ 已实现 | — | —(反而领先 pj006) |
## B2. 反向差异:pj0231 反而更「实」的点
1. **向量检索是真实现**:pj0231 混合检索端到端可用;pj006 实跑 jieba 关键词,向量仅字段预留。
2. **随机 + 按知识域自动抽题组卷**:pj0231 支持;pj006 考试为固定题目列表。
3. **文档→文本→检索链路真实跑通**:pj0231 LibreOffice/pdftotext 切片入库完整;pj006 知识工厂 Phase 1 未实跑。
4. **AI 算力点计费 + 审计日志**:pj0231 有 `ai_call_log`;pj006 未见同等计费。
---
# Part C —— 改进路线图
## C1. 优先级(按 ROI)
| 优先级 | 项 | 理由 |
|---|---|---|
| P0 | 岗位模型 + 岗位-知识映射 + 岗位驱动考试 | 最本质差距,本次核心诉求 |
| P1 | 学员「我的岗位应学清单」+ 成绩按岗位聚合 | P0 的自然延伸 |
| P2 | 错题本 + 轻量成绩档案 | 补考试闭环 |
| P2 | 简答题 + LLM 评分 | 复用 LLM 底座,成本低 |
| P2 | 后端自动化测试 | 消除 0 测试隐患 |
| 不做 | 多租户 / NPC 人格 / HEP 引擎 / KPI | 与极简定位冲突 |
## C2. 分阶段总览
```
P0(本次落地):岗位 + 映射 + 岗位驱动考试组卷
↓
P1:我的岗位应学清单 · 成绩按岗位聚合 · 岗位考试蓝图
↓
P2:错题本 · 简答题 LLM 评分 · 自动化测试
```
---
# Part D —— 设计稿:岗位与知识对应能力(核心)
> 完整设计见《设计稿_岗位与知识对应能力.md》,此处为完整版正文。
## D1. 目标与原则
**目标**:落地最小岗位驱动闭环 —— `岗位 → 用户绑定 → 岗位知识映射 → 岗位驱动考试组卷`。
**原则**:保持极简;复用现有 `AutoMigrate` / `RequireAdmin` / `pickQuestions`;级别词汇对齐 pj006 L1-L4 但**不建独立级别表**;单岗位优先。
## D2. 改动点清单
| 现有资产 | 位置 | 改动 |
|---|---|---|
| `User` | `internal/model/user.go` | 加列 `PositionID *uint`(可空) |
| `ExamPaper` | `internal/model/exam_paper.go` | 加列 `PositionID *uint`(可空) |
| 迁移 | `internal/store/db.go` | AutoMigrate 追加 2 张新表 |
| 路由 | `internal/api/router.go` | 新增岗位端点(admin 组) |
| 抽题 | `internal/api/exam.go` `pickQuestions` | 增加岗位知识映射过滤分支 |
| 种子 | `internal/store/seed.go` | 可选预置默认岗位 |
## D3. 数据模型
### 3.1 `position`(岗位表)
```go
type Position struct {
ID uint `gorm:"primaryKey" json:"id"`
Code string `gorm:"size:32;uniqueIndex;not null" json:"code"`
Name string `gorm:"size:64;not null" json:"name"`
Description string `gorm:"type:text" json:"description"`
Status string `gorm:"size:16;not null;default:active;index" json:"status"` // active / inactive
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Position) TableName() string { return "position" }
```
### 3.2 `position_knowledge`(岗位知识映射,核心)
```go
type PositionKnowledge struct {
ID uint `gorm:"primaryKey" json:"id"`
PositionID uint `gorm:"not null;index" json:"position_id"`
Domain string `gorm:"size:16;not null;index" json:"domain"` // company / product / sales
CourseID *uint `gorm:"index" json:"course_id"` // 可空
ProductID *uint `gorm:"index" json:"product_id"` // 可空
RequiredLevel string `gorm:"size:16;not null;default:L1" json:"required_level"` // L1/L2/L3/L4
Weight float64 `gorm:"not null;default:1" json:"weight"`
IsMandatory bool `gorm:"not null;default:true" json:"is_mandatory"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (PositionKnowledge) TableName() string { return "position_knowledge" }
```
### 3.3 级别常量
```go
const (
LevelL1 = "L1" // 入门级
LevelL2 = "L2" // 稳定执行级
LevelL3 = "L3" // 复杂处理级
LevelL4 = "L4" // 体系策略级
)
var validLevels = map[string]bool{LevelL1: true, LevelL2: true, LevelL3: true, LevelL4: true}
```
## D4. API 设计
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| GET | `/api/positions` | admin | 岗位列表 |
| POST | `/api/positions` | admin | 新建岗位 |
| PUT | `/api/positions/:id` | admin | 编辑岗位 |
| DELETE | `/api/positions/:id` | admin | 停用(软删) |
| GET | `/api/positions/:id/knowledge` | admin | 岗位知识映射列表 |
| PUT | `/api/positions/:id/knowledge` | admin | 保存映射(整表覆盖) |
| PUT | `/api/users/:id/position` | admin | 设置/清除用户岗位 |
| GET | `/api/my/position` | 员工 | 我的岗位 + 应学范围(P1) |
## D5. 岗位驱动考试组卷(核心算法改动)
修改 `pickQuestions`,在 `PositionID != nil` 时按岗位知识映射圈定题池,否则回退原 `domain` 抽题:
```go
func pickQuestions(p model.ExamPaper) ([]model.Question, error) {
pool := store.DB.Where("status = ?", "active")
if p.PositionID != nil {
var pks []model.PositionKnowledge
if err := store.DB.Where("position_id = ?", *p.PositionID).Find(&pks).Error; err != nil {
return nil, err
}
domains := make([]string, 0, len(pks))
courseIDs := make([]uint, 0, len(pks))
for _, pk := range pks {
domains = append(domains, pk.Domain)
if pk.CourseID != nil {
courseIDs = append(courseIDs, *pk.CourseID)
}
}
domains = dedupe(domains)
if len(domains) > 0 {
pool = pool.Where("domain IN ?", domains)
}
if len(courseIDs) > 0 {
pool = pool.Where("course_id IN ? OR course_id IS NULL", courseIDs)
}
} else {
domains := splitDomains(p.Domain)
if len(domains) > 0 {
pool = pool.Where("domain IN ?", domains)
}
}
var qs []model.Question
if err := pool.Order("id ASC").Find(&qs).Error; err != nil {
return nil, err
}
if len(qs) < p.QuestionCount {
return nil, fmt.Errorf("题库题目不足:需要 %d,可用 %d", p.QuestionCount, len(qs))
}
if p.Randomize {
rand.Shuffle(len(qs), func(i, j int) { qs[i], qs[j] = qs[j], qs[i] })
}
return qs[:p.QuestionCount], nil
}
```
配套:`CreatePaper` / `UpdatePaper` 追加校验——岗位考试必须已配置至少一条知识映射,否则拒绝。
## D6. 前端设计
- 导航:管理员「系统管理」下拉新增 **「岗位管理」**。
- 页面:岗位列表(CRUD)+ 岗位知识映射编辑面板(domain 下拉 + 课程/产品下拉 + L1-L4 + 权重 + 必学开关)。
- 改造:用户管理页加「岗位」下拉;考试配置表单加「关联岗位」下拉。
## D7. 分阶段
- **P0**:表 + API + 岗位 CRUD + 用户设岗 + 岗位驱动组卷 + 前端岗位管理页。
- **P1**:学员「我的岗位应学清单」+ 成绩按岗位聚合 + 岗位考试蓝图。
- **P2**:见 Part E。
## D8. 明确不做
❌ 不建洋葱三层模型;❌ 不引入多租户;❌ 不做 LLM 自动生成能力模型;❌ 不做多岗位绑定;❌ 不做学情分析。
---
# Part E —— 设计要点:其余改进项
## E1. 错题本 + 轻量成绩档案(P2)
> ⚠️ 与 PRD 冲突需先拍板:PRD 第 7 节「无学情分析」。**错题本**是学员自助工具(不做管理层分析),可不冲突;**学习档案/能力雷达**属「学情分析」范畴,需产品重新确认是否放开。
### 数据模型
```go
// MistakeRecord 错题本
type MistakeRecord struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"`
QuestionID uint `gorm:"not null;index" json:"question_id"`
Source string `gorm:"size:16;not null" json:"source"` // self_test / formal
UserAnswer string `gorm:"type:text" json:"user_answer"`
CorrectAnswer string `gorm:"type:text" json:"correct_answer"`
Resolved bool `gorm:"not null;default:false;index" json:"resolved"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (MistakeRecord) TableName() string { return "mistake_record" }
```
### 联动点
- 正式考 `ExamSubmit` 判分循环里,错题 `INSERT` 进 `mistake_record`(`source=formal`);自测可选不写。
- 学员端「我的错题本」页:列表 + 「已掌握」标记(`resolved=true`)。
## E2. 简答题 + LLM 评分(P2)
### 数据模型改动
- `question.type` 增加 `essay`;`answer` 存 rubric 评分要点文本;`options` 空。
- `exam_record.detail_json` 已存答题明细,可直接承载简答文本。
### 评分流程
- 复用 `internal/ai/llm.go`,新增「评分」能力路由(或在 `ai_call_log.capability` 加 `essay_grade`)。
- 交卷时对 `essay` 题调 LLM 按 rubric 打分(0~1 得分率),持久化到 `detail_json`。
- 选择题仍确定性判分;总分 = Σ得分率/题数×100(对齐 pj006 算法)。
## E3. 自动化测试(P2)
- **后端**:新增 `_test.go`,优先覆盖纯逻辑函数:`isCorrect`(判分)、`splitDomains`、`pickQuestions`(组卷)、`ai.Retrieve` 的 `splitTerms`/`cosine`。
- **命令**:`go test ./...`;接入 `start_dev` 或 CI 脚本。
- **前端**:可选,暂缓。
---
# Part F —— 待拍板决策点与下一步
## F1. 需产品/技术拍板
1. **岗位考试课程过滤语义**(D5):岗位绑定具体课程时,抽题用 `course_id IN ?`(严格)还是 `course_id IN ? OR course_id IS NULL`(宽松含域匹配)?
2. **错题本 vs 学情分析边界**(E1):PRD 禁止「学情分析」,错题本是否属于允许范围?
3. **岗位知识映射去重**(D3.2):是否加唯一约束防重复?
4. **简答题是否本期做**:P0 还是 P2?
## F2. 建议的下一步
1. 确认 F1 四个决策点;
2. 进入 P0 编码(改动文件已列于 D2,共 6 处);
3. P0 完成后补 `isCorrect`/`pickQuestions` 单测(借 P0 一并补测试债)。
---
> 本文件为整合版:Part A/B/C 来自《对比分析_pj0231_vs_pj006-zhilianyuan2.md》,Part D 整合自《设计稿_岗位与知识对应能力.md》,Part E 为新增其余改进项设计要点。