110
社区成员
发帖
与我相关
我的任务
分享| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 202501福大-软件工程实践-W班 |
| 这个作业要求在哪里 | 软工实践——CodeArts团队实战总结 |
| 这个作业的目标 | CodeArts团队实战总结 |
我们开发了一个基于大语言模型(LLM)的购车意向咨询系统。系统能根据用户的个性化需求(如预算、偏好、使用场景等),智能生成购车建议和车型分析。本项目采用前后端分离架构,前端使用 Vue.js,后端使用 Spring Boot,并通过 Docker Compose 进行容器化编排部署。
我们对系统的参与者(Actor)和用例(Use Case)进行了分析,主要参与者包括:普通用户、管理员 以及 外部LLM服务。

系统的后端核心模型围绕 User、Consultation 和 ConsultationRequest 三个实体展开。
模型 (Model):
User : 存储用户信息、BCrypt加密的密码、角色(user/admin)和积分。Consultation : 咨询的主记录,包含状态(PROCESSING, COMPLETED等)、LLM的JSON响应、摘要和反馈分数(feedbackScore)。ConsultationRequest : 存储用户提交的原始表单数据(如 budgetMin, carTypes),carTypes 等字段以 JSON 字符串形式存储在数据库中。User 可以发起多个 Consultation (一对多);一个 Consultation 对应一个 ConsultationRequest (一对一)。控制器 (Controller):
AuthController : 处理 /api/auth 路径,负责登录和注册。UserController : 处理 /api/user,负责用户信息的获取和更新。ConsultController : 处理 /api/consult,负责用户发起咨询、获取结果、删除和反馈。AdminController : 处理 /api/admin,负责所有管理员对用户和咨询的管理操作。AnalyticsController : 处理 /api/admin/analytics,提供分析报告接口。服务 (Service):
UserServiceImpl : 实现用户和认证的核心业务逻辑。ConsultServiceImpl : 实现核心的异步咨询、积分计算、反馈处理和删除逻辑。AnalyticsServiceImpl : 实现聚合ConsultationRequest [cite: backend/src/main/java/model/ConsultationRequest.java]数据,生成热门话题报告的逻辑。LLMClientImpl : 封装对智普AI和阿里百炼的双重API调用。
本项目采用前后端分离的现代化架构,通过 Docker Compose 容器化编排,整个系统由三个核心容器组成:
node-dev (前端): 一个 node:20-alpine 容器,运行 Vite 开发服务器。它负责渲染 Vue.js 应用,并作为用户访问入口(http://120.79.169.214:5173)。java-dev (后端): 一个 maven:3.9.11-eclipse-temurin-21 容器,负责运行 Spring Boot API 服务(http://120.79.169.214:8080)。mysql-dev (数据库): 一个 mysql:8.0 容器,为后端提供持久化存储。数据流:
用户浏览器访问 node-dev -> node-dev (Vite) 的 /api 代理将请求转发到 java-dev (Spring Boot) -> java-dev 调用 mysql-dev 或外部 LLM API -> java-dev 返回 JSON -> node-dev 渲染 VUE 页面。
我们遵循 API_CONTRACT.md 文档(并结合后期迭代)实现了以下 RESTful API:
| 方法 | 路径 | 描述 |
|---|---|---|
POST | /api/auth/register | 用户注册 |
POST | /api/auth/login | 用户登录 (返回Token) |
GET | /api/user/info | (需认证) 获取用户信息 |
PUT | /api/user/info | (需认证) 更新用户信息 |
POST | /api/consult/start | (需认证) 发起新咨询 (异步) |
GET | /api/consult/history | (需认证) 获取历史咨询列表 |
GET | /api/consult/result/{id} | (需认证) 获取单次咨询结果 |
DELETE | /api/consult/{id} | (需认证) 用户删除自己的咨询 |
POST | /api/consult/{id}/feedback | (需认证) 提交咨询反馈 (赞/踩) |
GET | /api/admin/users | (管理员) 分页获取用户列表 |
POST | /api/admin/users/{id}/ban | (管理员) 封禁用户 |
POST | /api/admin/users/{id}/unban | (管理员) 解封用户 |
POST | /api/admin/users/{id}/points | (管理员) 调整用户积分 |
GET | /api/admin/consults | (管理员) 获取所有咨询记录 |
DELETE | /api/admin/consults/{id} | (管理员) 删除任意咨询 |
GET | /api/admin/analytics/hot-topics | (管理员) 获取热门话题分析 |
本项目的核心瓶颈在于 LLM API 的高延迟。我们通过"异步+线程池"的经典模式解决了这个问题。
ConsultController): POST /api/consult/start 接口仅负责调用 ConsultServiceImpl,该服务立即创建 PROCESSING 状态的记录并返回 consultId。AsyncConfig): 我们定义了一个名为 llmExecutor 的 ThreadPoolTaskExecutor。在 persistRequest 成功提交数据库事务后,@Transactional(onCommit=...) (通过 TransactionSynchronizationManager.registerSynchronization 实现) 会将 processLLM 任务提交到此线程池中执行,使API调用与HTTP响应解耦。LLMClientImpl):CompletableFuture.supplyAsync(..., llmExecutor) 来并行执行 invokeZhipu 和 invokeBailian 两个方法。zai-sdk 官方SDK进行调用。RestTemplate 调用其兼容 OpenAI 的 REST 接口。llm.mock.enabled 属性。当其为 true 时,sendConsultRequest 会跳过真实API调用,返回模拟数据,用于压力测试。persistSuccess):CompletableFuture 都完成后,buildConsultResult 会判断结果(全成功 COMPLETED、部分成功 PARTIAL_SUCCESS 或全失败 FAILED)。persistSuccess 将结果(包含两个模型的回复)序列化为 JSON 存入 assistantResponse 字段,并更新状态。persistSuccess 会在此时调用 userRepository 为用户增加咨询积分(+10分)。POST /api/consult/{id}/feedback 接口。ConsultServiceImpl 中的 giveFeedback 方法会:1. 检查用户是否已反馈(feedbackScore != null);2. 保存反馈(赞/踩);3. 为用户增加反馈积分(+5分)。GET /api/admin/analytics/hot-topics 接口。AnalyticsServiceImpl 会 findAll 所有咨询请求,然后实时遍历列表。它使用 ObjectMapper 将 carTypes, scenes 等字段中存储的 JSON 数组(如 ["SUV", "轿车"])反序列化为 Java List<String>,并使用 Map<String, Long> 动态聚合词频,最后返回统计结果。我们关注了几个关键的用户体验点:
Navbar 和 RecordsView)都使用了媒体查询 (@media),在小屏幕上会自动折叠或堆叠,确保移动端可用性。select[multiple] 控件,在 ConsultationView.vue 中重构为**"标签式复选框"**(Pill-style Checkboxes)。这在桌面和移动端上都提供了更直观、更易于点击的选择体验。marked 库将其转换为 HTML,并使用 DOMPurify 库对 HTML 进行清理(防止XSS攻击),最后通过 v-html 将格式化的表格、列表和标题渲染给用户,极大提高了可读性。:disabled="loading"),并使用 alert() 提供清晰的操作结果反馈。LoginModal 和 RegisterModal 中,我们使用 JavaScript(phoneRegex)在前端对手机号格式进行即时验证,阻止了无效的API提交。示例截图:
Commit 次数统计:
- 总提交次数: 71次
- Person A (102300316蒋嘉会): 7 次
- Person B (102300203郭昀琪): 33 次
- Person C (102300134曾威): 9 次
- Person D (102300433袁昊): 16 次
为确保助教可以100%成功运行本项目,我们采用了 Docker Compose“一键式”部署。您不需要在本地安装 Java, Node.js 或 MySQL。
克隆仓库:
git@codehub.devcloud.cn-north-4.huaweicloud.com:a8c044a1ac024628b56e770bcb22f8a1/buy_car.git
cd buy_car
放置本地依赖:
zai-sdk 是本地 systemPath 依赖,请在 backend/ 目录下创建一个 lib 文件夹。zai-sdk-0.1.0.jar 文件复制到 backend/lib/ 目录中。配置环境变量:
deployment/ 目录。.env。.env 文件中,并填入您的 LLM API 密钥:# deployment/.env 文件
# 填入您的密钥
LLM_ZHIPU_KEY=sk-your-zhipu-key
LLM_BAILIAN_KEY=sk-your-bailian-key
# 保持默认即可
JWT_SECRET=super-secret-key-for-dev
DB_NAME=myapp
DB_USER=devuser
DB_PASS=devpass
LLM_MOCK_ENABLED=false
启动项目:
deployment/ 目录下。docker compose (V2) 或 docker-compose (V1):docker compose up --build
# 或
# docker-compose up --build
访问系统:
java-dev 和 node-dev 均启动成功。http://localhost:5173 即可访问。本项目的核心是 POST /api/consult/start 咨询接口。为避免 LLM API 长时间响应(5-30秒)导致前端超时,我们设计了异步处理流程:
ConsultServiceImpl) 立即在数据库中创建一条 status 为 PROCESSING 的记录,并立刻返回 consultId。llmExecutor 线程池 中取出一个新线程,该线程并行调用智普AI和阿里百炼两个 API。status 更新为 COMPLETED, PARTIAL_SUCCESS 或 FAILED。consultId 定期轮询 GET /api/consult/result/{id} 接口(在 RecordsView.vue 中实现),直到 status 不再是 PROCESSING,然后获取并显示最终结果。JwtUtil 负责令牌的生成和验证。AdminController 和 AnalyticsController 会额外检查 Token 中的 role 字段是否为 "admin"。api.real.js 中使用 axios 拦截器,自动从 localStorage 读取 Token 并附加到每个请求的 Authorization 头中。router/index.ts 中的 beforeEach 守卫会检查 localStorage 中的 currentUser,以保护需要登录的页面。我们实现了“附加功能1” 所需的完整积分闭环:
ConsultServiceImpl 的 persistSuccess 方法中,当咨询状态被更新为 COMPLETED 或 PARTIAL_SUCCESS 时,系统会自动抓取该咨询对应的 User 对象,并为其增加 POINTS_PER_CONSULTATION(10分)。POST /api/consult/{id}/feedback 接口。当用户在 RecordsView.vue 点击“赞”或“踩”时,后端 giveFeedback 方法 会在记录反馈的同时,为用户增加 POINTS_PER_FEEDBACK(5分)。ProfileView.vue 会显示用户当前积分,并提供一个(模拟的)兑换礼品界面。我们实现了管理员所需的核心功能:
AdminView.vue 通过 GET /api/admin/users 实现了用户的分页和搜索。同时实现了“封禁”(banUser)和“解封”(unbanUser)功能。AdminView.vue 通过 GET /api/admin/consults 获取所有咨询,并通过 DELETE /api/admin/consults/{id} 实现删除。AnalyticsController。当 AdminView.vue 访问时,后端 AnalyticsServiceImpl 会查询所有 ConsultationRequest 记录,实时解析存储在 car_types, scenes 等字段中的 JSON 数组,动态聚合出热门话题(如 “SUV: 50次”, “通勤: 45次”)并返回给前端展示。我们使用 JMeter 对部署在 120.79.169.214 上的云服务器进行了压力测试。
POST /api/consult/start 接口。LLM_MOCK_ENABLED=true 开启了“压测模拟模式”。在此模式下,LLMClientImpl 会跳过真实的 API 调用,转而模拟一个 200-500ms 的延迟并返回模拟数据。

java-dev 容器的 CPU 达到 98.74%。聚合报告 显示 Start Consult 接口的吞吐量(Throughput) 达到了 10 QPS (每秒10个请求)。llmExecutor 的最大线程数和队列容量,并对数据库连接池进行调优。1. 首页 (HomeView.vue)
2. 登录与注册 (LoginModal.vue / RegisterModal.vue)
3. 购车咨询表单 (ConsultationView.vue)
4. 咨询结果详情 (RecordsView.vue)
5. 个人中心 (ProfileView.vue)
6. 管理员面板 - 用户管理 (AdminView.vue)
7. 管理员面板 - 热门分析 (AdminView.vue)
| 组员 | 学号 | 角色 | 负责内容 |
|---|---|---|---|
| Person A | 102300316蒋嘉会 | 前端开发 | 负责所有 Vue.js 页面视图(Views)和组件(Components)的编写、美化和联调。 |
| Person B | 102300203郭昀琪 | 后端开发 (用户&DB) | 负责 Spring Boot 的用户模块(User)、认证(AuthController)、数据库建模(Consultation 等)和管理员模块(AdminController)的开发。 |
| Person C | 102300134曾威 | 后端开发 (LLM&咨询) | 负责核心的异步咨询逻辑(ConsultServiceImpl)、双 LLM API 的集成(LLMClientImpl)以及新增的积分和分析(AnalyticsServiceImpl)服务。 |
| Person D | 102300433袁昊 | PM/DevOps/QA | 负责 API 契约设计(API_CONTRACT.md)、Docker 容器化编排(docker-compose.yml)、云服务器部署、联调问题排查和压力测试执行。 |
在本次限时开发中,我们团队遇到了大量真实世界中的工程问题,涵盖了前端、后端、数据库和运维部署的各个层面。
ConsultationView 的表单,使用了现代化的"标签式复选框"来替代丑陋的原生多选框。marked 和 DOMPurify 库,我实现了 v-html 安全渲染,将 Markdown 文本正确转换为带表格和列表的富文本。router.push({ name: 'Home' }) 和 name: 'home' 的大小写不匹配导致的 vue-router 错误,修复后解决。authService 只存了 token,而 Navbar 依赖 currentUser。通过修改 Navbar.vue,使其监听 userLoggedIn 事件并主动调用 userService.getCurrentUserInfo() 来刷新 localStorage 解决了此问题。preferred_type 与 Java 字段 preferredType 命名规范不一致。通过使用 JPA 的 @Column(name = "preferred_type") 注解明确指定映射关系,解决了字段映射错误。UserRepository 中 findByPhone 方法最初返回 User,在用户不存在时易引发空指针。将返回类型改为 Optional<User>,利用 Optional 类优雅地处理了空值情况,提升了代码健壮性。findByUser_IdOrderByCreatedAtDesc 这样的命名约定,实现了高效的关联实体属性查询。UserServiceImpl 中添加了服务端的 isValidPhone 正则表达式校验,确保了无效数据无法入库。List<String>,而数据库存储的是 JSON 字符串。我们统一了 ObjectMapper 序列化/反序列化逻辑,并将其应用在 AnalyticsServiceImpl 和 ConsultServiceImpl 中。processLLM 任务必须在数据库事务提交后才能执行。我们使用 TransactionSynchronizationManager.registerSynchronization 并重写 afterCommit 方法,确保了 LLM 调用总是在主事务成功后才被触发。zai-sdk 频繁抛出 NoClassDefFoundError。原因是 systemPath 依赖不会自动下载传递性依赖。通过手动将 retrofit2 和 adapter-rxjava3 添加到 pom.xml 中,解决了运行时类缺失问题。Read timed out。通过修改 LLMClientImpl,将 setReadTimeout 从 30 秒延长至 60 秒解决。finishReason=length 且内容为空。通过修改 LLMClientImpl,将 maxTokens 从 1024 增加到 4096 解决。LLMClientImpl 中添加了 llm.mock.enabled 开关,跳过真实 API 调用,实现了对数据库和线程池的隔离压测。java-dev 容器反复启动失败,报错 ConflictingBeanDefinitionException(例如 security.JwtUtil 和 service.JwtUtil 冲突)。scanBasePackages 扫描了重复的包。通过 ls 命令确认 service/ 目录下存在多余的 JwtUtil.java, AuthService.java, SecurityConfig.java 文件,将其在本地删除。java-dev 依然报相同的 Bean 冲突错误。.class 文件。通过修改 docker-compose.yml 的 command,在 spring-boot:run 前强制添加 mvn clean,彻底清除了 target 目录缓存,问题解决。maven:3.9-openjdk-19 镜像时报 connection reset by peer。通过将镜像源替换为国内阿里镜像源 (registry.cn-hangzhou.aliyuncs.com/...) 解决。node-dev 启动失败,报 crypto.hash is not a function。经查 frontend/package.json 要求 Node 20+,而 docker-compose.yml 中是 node:18-alpine。将镜像升级为 node:20-alpine 解决。docker-compose V1 安装失败(下载了HTML文件)。docker compose V2(docker-compose-plugin),或使用 curl -L 重新安装 V1,并确保服务器安全组已放行 5173 和 8080 端口。llm.mock.enabled 开关,将压测目标从(不可控的)外部 API 转移到(可控的)内部应用瓶颈(llmExecutor 线程池和 mysql-dev 数据库连接池)。POST /api/consult/start 需要大量登录用户。| 阶段 | 任务 | 计划时间(小时) | 实际时间(小时) | 差异 | 备注 |
|---|---|---|---|---|---|
| 计划 | 需求分析与设计 | 1 | 1 | 0 | 快速梳理前端功能模块 |
| 开发 | 搭建Vue项目结构 | 1 | 1 | 0 | 使用Vite快速创建项目 |
| 开发 | 编写核心组件(Navbar, LoginModal等) | 3 | 4 | +1 | 增加了科技感UI效果 |
| 开发 | 实现路由和页面跳转 | 1 | 1 | 0 | - |
| 开发 | 集成Mock API服务 | 2 | 3 | +1 | 为联调做准备 |
| 测试 | 功能测试和UI调试 | 1 | 1 | 0 | - |
| 部署 | 构建和部署到公网 | 1 | 1 | 0 | - |
| 开发 | (迭代) 修复联调Bug | 2 | 4 | +2 | 修复 v-html 渲染、路由跳转、登录状态刷新等问题 |
| 总计 | (更新后) | 12 | 15 | +3 | 联调和UI美化耗时超出预期 |
| PSP 阶段 | 预计耗时(小时) | 实际耗时(小时) | 完成内容 |
|---|---|---|---|
| 计划分析 | 1 | 1 | 分析用户模块需求,确定用户表和咨询记录表 |
| 数据库设计 | 2 | 1.5 | 编写SQL,创建表结构、字段、索引和外键约束 |
| 模型层、数据库访问层 | 3 | 3.5 | 创建User等实体类,编写UserRepository等接口 |
| 业务逻辑层开发 | 2 | 2 | 实现AuthService/UserService用户认证、用户信息管理的核心业务逻辑 |
| 控制层开发 | 2 | 2 | 编写AuthController注册登录接口、UserController用户信息管理接口 |
| 后端合并 | 3 | 2.5 | 与C同学的代码合并,解决 pom.xml 和 model 的冲突 |
| 测试、修复bug | 5 | 4.5 | 测试数据库连接,Postman 验证API接口功能,修复字段映射 |
| PSP、文档撰写 | 4 | 2.5 | 个人部分PSP表、困难与解决方案撰写 |
| 合计 | 22 | 19.5 |
| PSP 阶段 | 任务描述 | 预计耗时 (h) | 实际耗时 (h) |
|---|---|---|---|
| 规划 | 明确 C 角色范围:负责咨询流程串联、持久化与 LLM 调用闭环 | 1.0 | 0.8 |
| 需求分析 | 梳理 /consult 系列接口、与 B 的数据库字段对齐、确认异步回写要求 | 2.0 | 1.5 |
| 设计 | 设计事务包裹、TransactionSynchronization 触发点、JSON 列映射与 DTO 还原策略 | 3.0 | 3.0 |
| 设计复审 | 与团队评审咨询实体关系、回写状态机、失败兜底方案 | 1.0 | 0.7 |
| 编码 | 实现 ConsultServiceImpl, LLMClientImpl, AnalyticsServiceImpl, 积分逻辑 | 8.0 | 9.5 |
| 单元测试 | (主要通过 Postman 和联调测试) | 2.0 | 1.5 |
| 集成测试 | Postman 验证 /consult/start/history/result 数据一致性,联调 API | 1.5 | 2.0 |
| 部署验证 | 在部署环境触发真实咨询流程,检查数据库 JSON 字段与日志中的 LLM 回流 | 1.0 | 1.0 |
| 事后总结 | 记录事务回调、LLM 异常处理经验,整理后续优化清单 | 0.8 | 0.8 |
| 合计 | 20.3 | 20.8 |
| PSP 阶段 | 任务描述 | 预计耗时 (h) | 实际耗时 (h) |
|---|---|---|---|
| 规划 | 明确团队分工,分析作业需求 | 1.0 | 1.0 |
| 设计 | 撰写 API_CONTRACT.md,设计 docker-compose.yml 编排方案 | 2.0 | 2.5 |
| 编码 | 编写 docker-compose.yml, .env, README.md 部署指南 | 2.0 | 2.0 |
| 测试与联调 | (核心) 解决云服务器部署、Docker网络、Bean冲突、Maven缓存、Java依赖、Vite版本、前端路由等所有环境和联调 Bug | 4.0 | 10.0 |
| 压测 (QA) | 设计压测方案(Mock 模式)、设计批量用户生成SQL、编写 JMeter 脚本并执行 | 3.0 | 3.0 |
| 报告 | 撰写最终博客(汇总所有人材料) | 2.0 | 2.0 |
| 合计 | 14.0 | 20.5 |
| 组员 | 贡献比例 (%) |
|---|---|
| Person A (102300316蒋嘉会) | 24.5% |
| Person B (102300203郭昀琪) | 23.5 % |
| Person C (102300134曾威) | 22.5 % |
| Person D (102300433袁昊) | 29.5 % |
| 总计 | 100 % |