法宝AI法律助手全栈项目开发
一、项目概述
法宝是一个AI法律助手全栈项目,提供三大核心功能:AI法律咨询、法律文书生成、合同风险审查。
| 层 | 技术栈 | 端口 |
|---|---|---|
| 前端 | Vue 3 + Vite + Vue Router 4 + Element Plus + axios | 5173(dev) |
| 后端 | FastAPI + SQLAlchemy 2.0 + Pydantic V2 + JWT + python-docx | 8000 |
| AI引擎 | FastGPT(本地部署,3个应用:法律咨询/文书起草/合同审查) | 3000 |
| 数据库 | MySQL 8.0(utf8mb4,5张表) | 3306 |
设计语言:深蓝 #1a3a5c + 金色 #c9a96e,专业法律感。
二、前端开发
2.1 项目结构与路由
1 | src/ |
路由:/ 首页、/ai-consult 咨询、/doc-generate 文书、/contract-review 审查、/about 关于。
2.2 AI法律咨询页
核心交互流程:
1 | 用户输入 → POST /chat/conversations/{id}/messages |
关键实现:
- SSE流式解析:
fetch+reader.read()逐行解析data: {...},处理reasoning_content(思维链实时展示)和content(正文逐字流入) - 思维链面板:模型思考时推理过程逐字流式显示,标题实时计时
⏳ 深度思考中 · N 秒,正文开始后自动折叠为已深度思考 · 用时 N 秒 - 历史会话侧栏:登录后左侧240px面板,
onMounted加载getConversations,点击切换回填getMessages,hover 显示删除按钮(ElMessageBox.confirm),”新对话”按钮只重置聊天区不删数据 - 登录状态监听:
watch(isLoggedIn)—— 登录→重新加载列表,登出→清空+重置聊天区
2.3 文书生成页
三步流程(el-steps):选择类型 → 填写信息 → 生成预览
4种文书:民事起诉状、民事答辩状、律师函、授权委托书。
关键改进:
- 百分比进度条 → 阶段骨架提示:4阶段清单(理解信息→检索法规→构建框架→生成全文),每2-2.5秒推进一项,后端返回立即跳结果页
- 预览排版:服务端返回结构化JSON(
{title, sections}),前端按section type渲染:heading加粗、paragraph首行缩进、party角色标签+详情、signature右对齐、notice斜体灰色 - 下载:
POST /documents/download,传{doc_data: JSON字符串, doc_type},后端用专业排版生成docx返回blob - 历史卡片:第一步下方”我的文书历史”,查看(解析JSON进预览页)/ 下载(直接用记录内容生成docx)
2.4 合同审查页
流程:上传合同 → 阶段分析 → 风险报告
关键实现:
- 上传:
POST /contracts/upload,支持 .docx/.pdf/.txt/.doc/.wps,后端提取文本存original_text字段 - 5阶段骨架:解析文本→识别条款→比对法规→评估等级→生成报告
- 报告展示:评分圆环 + 高/中/低风险统计 + 风险条款卡片(原文摘录→风险分析→绿色修改建议→法律依据)+ 合规条款
- 下载:审查报告docx(
GET /contracts/report/{id})+ 原始合同文件(GET /contracts/file/{id}) - 历史卡片:每条记录显示文件名 + 评分tag(85+绿/70+黄/低分红)+ 时间,查看/下载报告/下载原文件
2.5 页面动态展示机制(Vue Router驱动)
核心问题:App.vue怎么做到”动态展示其他页面”?
本项目不用数组 + activeIndex + 动态组件的手动写法,而是用Vue Router驱动:
页面切换的6步链路
| 步骤 | 发生什么 | 在哪个文件 |
|---|---|---|
| 1 | 点击导航或按钮 | AppHeader.vue(router-link)/ HomePage.vue(router.push) |
| 2 | 地址栏URL变化 | 浏览器 |
| 3 | 路由表匹配:path→component | router/index.js |
| 4 | 懒加载:首次访问才下载页面组件 | router/index.js 的 () => import(...) |
| 5 | router-view拿到匹配的组件对象 | App.vue 的 v-slot |
| 6 | transition + <component :is> 渲染新页面 |
App.vue |
App.vue核心渲染结构
1 | <router-view v-slot="{ Component }"> |
逐层拆解:
| 代码 | 作用 |
|---|---|
<router-view> |
路由出口,自动拿到”当前URL匹配的组件” |
v-slot="{ Component }" |
作用域插槽,把匹配到的组件对象解构出来 |
<transition mode="out-in"> |
切换动画:旧组件完全消失后新组件再进来 |
<component :is="Component" /> |
动态组件,:is是哪个组件对象就渲染哪个 |
一句话:router-view负责”算出现在该显示谁”,component :is负责”把它画出来”。
URL切换底层机制
<router-link>渲染成<a href>,但点击时不是浏览器默认跳转,而是Vue Router接管:
1 | 点击 <a href="/ai-consult"> |
关键:pushState只改地址栏、不发网络请求——这是SPA”切换页面但浏览器不刷新”的全部秘密。
currentRoute:URL与页面之间的桥
Vue Router内部有一个currentRoute响应式对象,<router-view>依赖它:
URL变了 →
currentRoute.value更新 → 依赖它的计算属性失效 →router-view重渲染 →<component :is>实例化新组件
三条路径殊途同归:
| 路径 | URL是谁改的 | 结果 |
|---|---|---|
| 点router-link / router.push() | Vue Router用pushState主动改 | 地址栏变 + 组件切换,无刷新 |
| 浏览器前进/后退 | 浏览器改(触发popstate事件) | Router监听后更新currentRoute → 组件切换 |
| 地址栏手输/F5刷新 | 浏览器整页加载 | 应用重启 → 路由读URL匹配渲染 |
状态驱动 vs URL驱动
| 维度 | 手动写法(状态驱动) | 本项目(URL驱动) |
|---|---|---|
| 切换”开关” | 内存里activeIndex数字 | 地址栏URL字符串 |
| 浏览器前进/后退 | 不支持 | 原生支持 |
| 刷新页面 | 回到主页 | 停留在当前URL对应页面 |
| 组件加载 | 启动时全部import | 路由懒加载,用到才下载 |
| 扩展性 | 加页面改多处 | 加路由+组件即可,App.vue不动 |
2.6 认证与状态管理
1 | // useAuth.js — 全局单例 |
AppHeader.vue右侧:未登录显示”登录/注册”按钮,登录后显示用户名+”退出”http.js请求拦截器:从localStorage取token加Authorization: Bearer头http.js响应拦截器:blob类型直接返回,JSON自动解包{code, message, data}
三、后端开发
3.1 分层架构
1 | api/ 路由层(接收请求、校验参数、调用service、返回响应) |
3.2 数据库设计
| 表 | 用途 | 关键字段 |
|---|---|---|
users |
用户注册 | id, username, email, hashed_password |
conversations |
AI对话 | id, user_id(FK), title, updated_at |
messages |
对话消息 | id, conversation_id(FK), role, content |
documents |
文书记录 | id, user_id(FK), doc_type, form_data(JSON), generated_content(Text) |
contracts |
合同审查 | id, user_id(FK), file_name, file_path, original_text, review_result(JSON) |
统一响应格式:{code: 200, message: "success", data: {...}}
3.3 FastGPT集成与chatId隔离
核心调用:三个service统一调用 FastGPT 的 /v1/chat/completions 接口(OpenAI兼容格式),appId 放请求体内。
chatId命名规则(修复跨用户/跨对话记忆串台):
| 模块 | chatId格式 | 说明 |
|---|---|---|
| AI对话 | fabao-chat-u{uid}-c{conversation_id} |
每个对话独立记忆 |
| 文书生成 | fabao-doc-u{uid}-d{document_id} |
每次生成独立,不累积历史 |
| 合同审查 | fabao-review-u{uid}-r{contract_id} |
同一合同重新审查共用历史 |
前缀 chat/doc/review 解决三表主键可能撞号的问题。
文书生成特殊处理:原来先调FastGPT再存记录(调用时没有主键),改为”先建空记录拿主键→调FastGPT→回填content”。
3.4 文书docx排版
从简陋逐行写入改为专业排版服务 document_docx_service.py:
字体策略(关键:必须同时设置ascii+eastAsia):
- ascii:
Times New Roman - eastAsia:
Microsoft YaHei(否则Word打开中文变方块) - 全文纯黑
RGBColor(0,0,0)
结构化JSON → Word排版映射:
| section type | 含义 | Word样式 |
|---|---|---|
party |
当事人信息 | role加粗 + content正常,首行缩进24pt |
heading |
节标题 | 15pt加粗,段前14pt段后8pt |
numbered |
编号项 | number加粗 + content正常,首行缩进 |
paragraph |
正文段落 | 12pt,首行缩进2字符,1.6倍行距 |
center |
居中行 | 居中对齐,bold可选 |
signature |
落款 | 右对齐 |
blank |
空行 | spacing个空行 |
notice |
提示 | 11pt斜体 |
占位符长度按数据类型区分:人名6、身份证18、电话11、日期8、金额10、地址20、法院律所16个下划线。
3.5 合同审查报告docx
report_service.py 生成Word报告:标题→评分→结论→每条风险(原文/分析/建议/法律依据)→免责声明。
中文文件名处理:Content-Disposition 使用RFC 5987双filename格式,Access-Control-Expose-Headers: Content-Disposition 让前端能读到。
四、FastGPT调用链路
4.1 AI对话完整链路
1 | 前端 AiConsult.vue sendMessage() |
4.2 chatId的记忆语义
- 传了chatId:FastGPT自动从数据库取该会话历史 + 新消息一起喂模型 + 本轮落库
- 不传chatId:无记忆、不存储(适合一次性任务)
- 同一chatId重新审查:FastGPT带上上次历史(合同审查场景下合理关联)
五、部署与联调
5.1 前后端两个”服务”的本质区别
| 前端 | 后端 | |
|---|---|---|
| 技术 | Vue 3 + Vite + Element Plus | FastAPI + MySQL |
| 本质 | 一堆静态文件(HTML+JS+CSS) | 一个持续运行的程序,监听端口等请求 |
| 部署要求 | 任何能放静态文件的地方(CF Pages、Nginx、OSS) | 必须有一台一直开着机的服务器跑它 |
npm run build后前端就是dist/目录里的一堆文件,没有”进程”概念。能”调接口”纯粹是因为浏览器执行了其中的JS代码发起HTTP请求。”前端部署在哪”和”后端部署在哪”完全独立,中间只靠一个URL连接。
5.2 请求从哪发出:http.js统一封装
1 | // src/utils/http.js |
- 没设置
VITE_API_BASE_URL(本地dev)→baseURL = '/api/v1',相对路径,请求发给当前页面所在服务器 - 设置了(CF Pages)→
baseURL = 'https://954487onpr52.vicp.fun/api/v1',绝对地址,直接打到花生壳
import.meta.env是编译时替换,不是运行时读取!Vite打包时直接把代码替换成字符串常量写死进JS。改环境变量后必须重新部署(重新build)才生效。
5.3 本地开发:Vite代理消跨域
1 | // vite.config.js |
工作流程:
1 | 浏览器 localhost:5173 |
为什么没有跨域? 页面来自5173,请求也发给5173 → 同源 → 浏览器不干涉。Vite转给8000是服务器间通信,浏览器不知道。
⚠️
npm run dev有代理 ✅;npm run preview预览build产物不带proxy(那是另一套preview.proxy配置,本项目没配)。所以本地build完preview会连不上后端——proxy不进打包产物。
5.4 线上部署架构
1 | 浏览器 公网 家里 |
- 前端:CF Pages,自定义域名
https://fabao.blog-x.cc.cd - 后端:花生壳内网穿透
https://954487onpr52.vicp.fun - 前端生产构建:
VITE_API_BASE_URL环境变量拼完整后端地址(Vite proxy仅dev生效)
5.5 CF Pages 405错误:静态托管不认识POST
当前端没配VITE_API_BASE_URL时,baseURL是相对路径/api/v1,浏览器把请求发给CF Pages自己。CF Pages是静态文件托管,只会”找文件、发文件”,对/api/v1/auth/login这个路径发POST → 405 Method Not Allowed。
解法:CF Pages项目设置里加构建环境变量 VITE_API_BASE_URL = https://954487onpr52.vicp.fun,打包出的JS里baseURL变完整地址,请求直接发给花生壳。
格式三要求:https://开头、不带/api/v1(代码会自己拼)、末尾不带/。
5.6 花生壳:让家里电脑”被公网访问”
后端跑在家里127.0.0.1:8000,家里宽带通常没有公网IP。花生壳(内网穿透)解决:
1 | 公网用户 ──▶ 954487onpr52.vicp.fun(花生壳云端) |
- 隧道是客户端主动向外建立的长连接,不需要公网IP、不需要端口映射
- 前提:电脑开机 + 后端运行 + 花生壳客户端在线,三者缺一线上就挂
- 免费版限制:带宽小、域名随机、有流量限额——演示够用,生产不行
5.7 CORS跨域:线上真正的”元凶”
同源策略
浏览器同源策略:协议 + 域名 + 端口三者完全一致才算同源。
| 页面地址 | 请求地址 | 是否跨域 |
|---|---|---|
http://localhost:5173 |
http://localhost:5173/api/... |
否(走代理时) |
https://fabao.blog-x.cc.cd |
https://954487onpr52.vicp.fun/api/... |
是(域名不同) |
跨域被拦的真实机制
跨域不是”请求发不出去”:
- 登录请求带
Content-Type: application/json,属于非简单请求 - 浏览器先偷偷发OPTIONS预检请求(preflight),问后端:”我这个来源、方法和头,你允许吗?”
- 后端响应头必须带
Access-Control-Allow-Origin: https://fabao.blog-x.cc.cd才算”批准” - 预检通过 → 浏览器才发真正的POST;不通过 → 浏览器直接拦截,JS只收到无响应的错误
为什么本地开发从没遇到CORS?因为本地走Vite代理,请求对浏览器是同源,CORS机制根本没被触发。这是”本地没问题、一上线就出事”的根本原因。
后端CORS白名单
1 | # backend/app/main.py |
backend/.env:CORS_ORIGINS=["http://localhost:5173","http://localhost:5174","https://fabao.blog-x.cc.cd"]
修改后必须重启后端(--reload只监听.py文件,不监听.env)。
排查技巧:curl没有同源策略,可直接验证:
1 | curl.exe -s -i -X OPTIONS "https://954487onpr52.vicp.fun/api/v1/auth/login" -H "Origin: https://fabao.blog-x.cc.cd" -H "Access-Control-Request-Method: POST" |
5.8 多实例”假重启”坑
改了.env也”重启”了,线上依然报跨域。原因:电脑上残留6个python进程(两组后端实例,uvicorn –reload启动=父进程+工作进程),占8000端口的实例仍用旧配置。
为什么会残留:Windows上点X关窗口或Ctrl+C偶尔杀不干净,进程变后台孤儿继续跑。
加固措施:
app/config.py:读.env从相对路径改成绝对路径(避免从非backend目录启动读不到.env)app/main.py:启动日志打印CORS 允许来源:[...]——一眼看出配置有没有生效
5.9 配置速查表
| 需求 | 改哪里 | 改完必须做什么 |
|---|---|---|
| 后端换端口(本地) | vite.config.js的proxy.target |
重启npm run dev |
| 前端请求的后端地址(线上) | CF Pages环境变量VITE_API_BASE_URL |
重新部署(重新构建才生效) |
| 允许某域名跨域 | backend/.env的CORS_ORIGINS |
重启后端(–reload不监听.env) |
| 改后端代码 | backend/app/下任意.py |
不用管,–reload自动重启 |
| 清残留进程 | Get-Process python | Stop-Process -Force |
再启动后端 |
5.10 排错思路(前端连不上后端)
- 请求打到哪了? → F12 Network看失败请求的URL → CF Pages自己域名说明
VITE_API_BASE_URL没配/没重新部署 - 服务通不通? → 浏览器开
https://后端地址/docs或curl → 打不开=花生壳/后端没在跑 - 是不是跨域被拦? → F12 Console看
blocked by CORS policy,用OPTIONS curl验证 - 配置真的生效了吗? → 看后端启动日志
CORS 允许来源:[...],还是旧值=有残留实例
六、问题与解决
问题1:FastGPT chatId硬编码导致跨用户记忆串台
- 现象:zhang聊过”张三欠5000”,新对话和lisi账号都能”记得”并复述
- 原因:
ai_service.py的chatId: "fabao-backend"是写死常量,所有用户共用一条FastGPT会话 - 解决:chatId改为
fabao-chat-u{user_id}-c{conversation_id},三模块统一前缀+用户+主键
问题2:文书下载丢失AI生成内容
- 现象:下载的docx变成前端输入的表单值模板拼接
- 原因:第一轮改造把
DownloadRequest改成只收form_data,AI内容被丢弃 - 解决:保留AI内容走智能排版(
format_ai_content_docx),form_data仅作降级
问题3:python-docx中文变方块
- 原因:只设了ascii字体族,未设eastAsia
- 解决:每个run同时设置
font.name = "Times New Roman"+font._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
问题4:历史列表登录后显示”无历史”
- 原因:
onMounted时未登录→loadSessions直接return;登录后无重新触发 - 解决:
watch(isLoggedIn),登录→重新加载,登出→清空列表
问题5:CORS 400 Disallowed CORS origin
- 原因:改.env后有6个残留python进程,占用8000的实例仍是旧配置
- 解决:杀干净全部残留进程 +
.env补线上域名 + 单实例重启
七、未解决的问题
- FastGPT旧记录:硬编码chatId产生的旧记录(含zhang隐私内容)需手动在FastGPT后台删除
- 对话删除不同步FastGPT:前端删对话只删MySQL,FastGPT侧chat记录残留(chatId唯一化后不再读写,无害但不干净)
- 合同审查记忆策略:同一合同重新审查共用chatId带历史,如需每次无记忆需加时间戳
- FastGPT使用者识别:后台”使用者”列全显示Owner(API Key属主),OpenAPI方式无法区分真实用户
今日总结
| 模块 | 核心要点 |
|---|---|
| 前端框架 | Vue3+Vite+Element Plus,4页面+历史UI,SSE流式解析+思维链面板 |
| 页面动态展示 | Vue Router驱动:router-view算组件、component :is渲染、pushState无刷新切换、懒加载、URL驱动vs状态驱动 |
| 后端架构 | FastAPI四层分层,5张MySQL表,JWT认证,依赖注入 |
| FastGPT集成 | 3个应用,chatId按”模块+用户+主键”隔离,appId放请求体 |
| 文书生成 | 结构化JSON→确定性Word排版,8种section type,中文字体双族设置 |
| 合同审查 | FastGPT返回JSON→前端渲染+Word报告下载,RFC 5987中文文件名 |
| 前后端联调 | Vite代理消跨域、CF Pages+花生壳架构、CORS预检机制、import.meta.env编译时替换、405/400排错四步法 |
| 关键修复 | chatId串台、双气泡、CORS残留进程、历史列表watch、文书下载丢AI内容 |