day35 法宝AI法律助手全栈项目开发

法宝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
2
3
4
5
6
7
8
9
10
11
src/
├── api/ # 后端接口封装(auth/chat/document/contract)
├── composables/ # 认证状态管理(useAuth)
├── utils/http.js # axios实例(自动token、统一解包、blob短路)
├── views/ # 四个页面
│ ├── HomePage.vue # 首页(Hero + 三模块卡片)
│ ├── AiConsult.vue # AI法律咨询(对话 + 历史侧栏)
│ ├── DocGenerate.vue # 文书生成(三步流程 + 历史卡片)
│ └── ContractReview.vue # 合同审查(上传→审查→报告 + 历史卡片)
├── components/ # AppHeader(导航+登录弹窗)/ AppFooter / FeatureCard
└── router/index.js # 懒加载 + 页面标题守卫

路由:/ 首页、/ai-consult 咨询、/doc-generate 文书、/contract-review 审查、/about 关于。

2.2 AI法律咨询页

核心交互流程:

1
2
3
4
用户输入 → POST /chat/conversations/{id}/messages
→ 后端存用户消息 → 组装历史上下文(最近10条)
→ 调FastGPT → 存AI回复 → 返回两条消息
→ 前端逐字渲染到气泡

关键实现:

  • 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
2
3
4
5
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>

逐层拆解:

代码 作用
<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
2
3
4
5
6
7
8
点击 <a href="/ai-consult">
├─ 浏览器默认行为:整页跳转(已被 e.preventDefault() 阻止)
└─ Vue Router 接管后的5步:
① e.preventDefault()
② 内部调用 router.push('/ai-consult')
③ history.pushState(...) 地址栏变(不发请求!)
④ currentRoute.value = 新路由对象 更新响应式状态
⑤ router-view 依赖 currentRoute 重新渲染 → 换组件

关键: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
2
3
// useAuth.js — 全局单例
const user = ref(JSON.parse(localStorage.getItem('fabao_user') || 'null'))
const isLoggedIn = computed(() => !!user.value)
  • AppHeader.vue 右侧:未登录显示”登录/注册”按钮,登录后显示用户名+”退出”
  • http.js 请求拦截器:从localStorage取token加 Authorization: Bearer 头
  • http.js 响应拦截器:blob类型直接返回,JSON自动解包 {code, message, data}

三、后端开发

3.1 分层架构

1
2
3
4
5
6
api/        路由层(接收请求、校验参数、调用service、返回响应)
services/ 业务逻辑层(AI调用、文书模板、合同审查、docx生成)
models/ ORM层(5张表,SQLAlchemy 2.0 Mapped写法)
schemas/ 校验层(Pydantic V2 请求/响应模型)
core/ 安全层(JWT签发验证、密码哈希、依赖注入)
utils/ 工具层(docx/pdf/txt文本提取、统一响应封装)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
前端 AiConsult.vue  sendMessage()
│ POST /api/v1/chat/conversations/{id}/messages
▼
后端 chat.py send_message()
│ 1. 存用户消息到 messages 表
│ 2. 查该 conversation 最近 10 条(MAX_HISTORY=10)
│ 3. 拼 chatId = fabao-chat-u{uid}-c{conv_id}
│ 4. 调 ai_service.generate_reply(history, content, chat_id)
▼
ai_service.py
│ payload = {chatId, stream:false, messages:[历史+本次], appId}
│ POST http://localhost:3000/api/v1/chat/completions
▼
FastGPT
├─ 按 chatId 找/建 chat 记录 → 自动取该 chatId 的历史
├─ 与传入 messages 合并 → 交给模型
└─ 本轮问答写回该 chat → 返回 AI 回复

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
2
3
4
5
6
// src/utils/http.js
const baseURL = import.meta.env.VITE_API_BASE_URL
? `${import.meta.env.VITE_API_BASE_URL}/api/v1`
: '/api/v1'

const http = axios.create({ baseURL, timeout: 120000 })
  • 没设置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
2
3
4
5
// vite.config.js
server: {
port: 5173,
proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } }
}

工作流程:

1
2
3
4
5
6
7
浏览器 localhost:5173
│ 请求 /api/v1/auth/login(相对路径)
▼
Vite 开发服务器 (5173)
│ 检测到 /api 开头 → 悄悄转发给 8000
▼
FastAPI 后端 (8000)

为什么没有跨域? 页面来自5173,请求也发给5173 → 同源 → 浏览器不干涉。Vite转给8000是服务器间通信,浏览器不知道。

⚠️ npm run dev有代理 ✅;npm run preview预览build产物不带proxy(那是另一套preview.proxy配置,本项目没配)。所以本地build完preview会连不上后端——proxy不进打包产物。

5.4 线上部署架构

1
2
3
4
5
6
7
8
浏览器                              公网                         家里
┌───────────────────┐ ①加载页面 ┌──────────────┐ ┌──────────────┐
│ fabao.blog-x.cc.cd│ ◀────────▶ │ Cloudflare │ 静态文件 │ 你的电脑 │
│ │ │ Pages CDN │ │ │
│ │ ②请求API └──────────────┘ │ ②'花生壳客户端│
│ │ ──────────▶ 954487onpr52.vicp.fun │ ↓ │
└───────────────────┘ (跨域!) │ FastAPI:8000 │
└──────────────┘
  • 前端: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
2
3
4
5
6
7
公网用户 ──▶ 954487onpr52.vicp.fun(花生壳云端)
│ 云端沿"隧道"下发
▼
家里电脑花生壳客户端(主动和云端保持长连接)
│ 转发到本机
▼
127.0.0.1:8000(FastAPI后端)
  • 隧道是客户端主动向外建立的长连接,不需要公网IP、不需要端口映射
  • 前提:电脑开机 + 后端运行 + 花生壳客户端在线,三者缺一线上就挂
  • 免费版限制:带宽小、域名随机、有流量限额——演示够用,生产不行

5.7 CORS跨域:线上真正的”元凶”

同源策略

浏览器同源策略:协议 + 域名 + 端口三者完全一致才算同源。

页面地址 请求地址 是否跨域
http://localhost:5173 http://localhost:5173/api/... 否(走代理时)
https://fabao.blog-x.cc.cd https://954487onpr52.vicp.fun/api/... 是(域名不同)

跨域被拦的真实机制

跨域不是”请求发不出去”:

  1. 登录请求带Content-Type: application/json,属于非简单请求
  2. 浏览器先偷偷发OPTIONS预检请求(preflight),问后端:”我这个来源、方法和头,你允许吗?”
  3. 后端响应头必须带Access-Control-Allow-Origin: https://fabao.blog-x.cc.cd才算”批准”
  4. 预检通过 → 浏览器才发真正的POST;不通过 → 浏览器直接拦截,JS只收到无响应的错误

为什么本地开发从没遇到CORS?因为本地走Vite代理,请求对浏览器是同源,CORS机制根本没被触发。这是”本地没问题、一上线就出事”的根本原因。

后端CORS白名单

1
2
# backend/app/main.py
app.add_middleware(CORSMiddleware, allow_origins=settings.CORS_ORIGINS, ...)

backend/.env:CORS_ORIGINS=["http://localhost:5173","http://localhost:5174","https://fabao.blog-x.cc.cd"]

修改后必须重启后端(--reload只监听.py文件,不监听.env)。

排查技巧:curl没有同源策略,可直接验证:

1
2
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"
# 返回200 + access-control-allow-origin头 = 放行;返回400 = 白名单没配对

5.8 多实例”假重启”坑

改了.env也”重启”了,线上依然报跨域。原因:电脑上残留6个python进程(两组后端实例,uvicorn –reload启动=父进程+工作进程),占8000端口的实例仍用旧配置。

为什么会残留:Windows上点X关窗口或Ctrl+C偶尔杀不干净,进程变后台孤儿继续跑。

加固措施:

  1. app/config.py:读.env从相对路径改成绝对路径(避免从非backend目录启动读不到.env)
  2. 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 排错思路(前端连不上后端)

  1. 请求打到哪了? → F12 Network看失败请求的URL → CF Pages自己域名说明VITE_API_BASE_URL没配/没重新部署
  2. 服务通不通? → 浏览器开https://后端地址/docs或curl → 打不开=花生壳/后端没在跑
  3. 是不是跨域被拦? → F12 Console看blocked by CORS policy,用OPTIONS curl验证
  4. 配置真的生效了吗? → 看后端启动日志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 补线上域名 + 单实例重启

七、未解决的问题

  1. FastGPT旧记录:硬编码chatId产生的旧记录(含zhang隐私内容)需手动在FastGPT后台删除
  2. 对话删除不同步FastGPT:前端删对话只删MySQL,FastGPT侧chat记录残留(chatId唯一化后不再读写,无害但不干净)
  3. 合同审查记忆策略:同一合同重新审查共用chatId带历史,如需每次无记忆需加时间戳
  4. 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内容