鱼答答
鱼答答
项目经历
项目架构
Spring Boot + Redis + ChatGLM + RxJava + SSE + Caffeine
项目简介
智能化应用答题平台,用户可以基于AI自动生成题目并制作应用,经管理员审核后,可在线答题并基于多种评分算法或AI得到回答总结,管理员还可通过后台管理系统整理网站内容,并进行统计分析
我负责的部分
- 库表设计:根据业务设计用户/应用/题目/评分结果/用户答题表。其中题目采用JSON存储复杂的嵌套题目和选项,便于维护扩展,并通过题目表格添加appId索引提升检索性能。
- 评分模块:基于策略模式实现了多种用户回答评分算法(如统计得分、AI评分等),全局执行器会扫描策略类上的自定义注解并选取策略,相比于if-else提高了系统的可扩展性
- 基于ChatGLM AI封装了通用AI模块,并通过配置类自动读取key配置初始化AI客户端Bean,便于全局使用
- AI生产题目:通过系统预设、少样本学习、任务拆建等手段优化Prompt,让AI返回JSON格式的题目,便于后端处理
- 由于AI生成题目较慢,选用ChatGLM的流式API并通过SSE实时推送单道题目给前端,提高用户体验
- 基于RxJava的操作符链式调用处理AI异步处理数据,先通过map获取并处理字符串、filter过滤空值、flatMap映射串为单个字符,再通过括号匹配算法精确拼出单道题目,使得逻辑简单清楚
- 为防止用户多次提交重复答案,基于雪花算法为每次答题分配唯一id,并通过数据库实现幂等设计,避免了重复的脏操作
- 由于相同答案的AI评分应该也是相同的,使用Caffeine本地缓存答案Hash对应的AI评分结果,并通过Redisson分布式锁解决缓存击穿问题
- 为应对用户答题数的激增,基于Sharding JDBC配置取模分片算法,根据应用id对用户答题记录分表,提高了单表查询性能
- 由于服务器资源有限,为会员创建核心线程数更大的隔离线程池,保证普通用户不会影响到会员使用AI的体验
架构设计
语雀(画图)
核心业务流程图
时序图
架构设计图
MBTI 实现方案介绍
核心组成部分:题目、用户答案、评分规则
1.题目结构(用json表示结构,前端结果看似是数组,但实际上是因为后端封装的是类)
2.用户答案结构(传递一个数组,如["A","B"])【数组顺序也是题目顺序,不用传递题目,节省传输体积】
3.评分规则(权威著作)
需求分析
1.按模块梳理
用户模块
应用模块
题目模块
评分模块
回答模块
统计分析模块
2.核心业务逻辑
流程图
文字描述
3.进行需求优先级分析
库表设计
用户表
应用表
题目表
题目内容使用json格式
评分结果表
resultId可能为空,是因为AI分析策略不会从结果表中选取结果,没有resultId
评分结果基本不会更改,是一次的提交数据,因此在表中设计冗余字段,便于查询,不用列表,节约开发成本
还有可能通过异步的方式、或者题目答案没提交(只答一半的时候,先临时保存题目回答记录)
用户答题记录表
后端初始化
引用万用后端模块功能
准备依赖
执行初始化SQL脚本
模板改造【ctrl+shift+r】(模块名、包名【Shift+F6】、移除不必要的模块、更改数据库)
后端基础开发 - 增删改查
先写最简单的增删改查的功能,不包含任何业务逻辑
1.数据访问层代码生成
使用MybatisX插件根据数据库生成实体类和mapper接口
改造Enity类,将id改为雪花算法(防止他人爬虫),添加逻辑删除的注解
2.业务逻辑代码生成
使用万用模板的代码生成器工具(CodeGenerator)生成代码(Controller、Service接口和实现类、数据模型包装类和枚举类)
3.数据模型开发
编写数据模型包装类(请求类和视图类)、JSON结构对应的类、枚举类
包装类需要根据前端传递的请求参数或需要的响应结果自行修改
问题
使用json格式,但是前端传递json格式就是字符串
将前端转递的字符串接受为一个集合(如List),实体类可以用String接受,然后在VO类里面进行转换,将json和对象互相转换
今天做的最多的就是在controller层中将DTO转化为json数组,其次就是在service层中校验数据
查询Query请求里面的是String对象,因为数据库中存储的是text,二进制存储,没有对象这一说,这里的VO对象也像是工具类,里面有objToVo的方法
后端核心业务流程开发
核心业务流程开发
1.用户注册 - 用户登录
2.用户创建应用 - 创建题目(包括题目选项得分)- 创建评分规则(评分方式和评分结果)
3.管理员管理应用,审核发布(或下架)应用
4.用户查看和检索应用列表,进入应用详情页,在线答题并提交回答
5.经过评分模块计算后,用户查看本次评分结果
需要额外开发的能力:
应用模块
审核发布和下架应用(仅管理员可用)
回答模块
提交回答(创建)。需要额外调整,提交回答后就可以调用评分模块并更新回答表
评分模块
根据回答计算评分结果(多种评分策略)
自定义规则 -- 测评类
自定义规则 -- 打分类
最后通过swagger接口文档来测试业务逻辑
评分模块实现
针对不同的应用类型和评分策略编写不同的实现方式
自定义测评类思路
通过appId得到app相关的问题和结果集合(通过属性),通过用户的答案来判断收集答案属性(遍历题目列表以及题目选项),遍历每种评分结果(计算哪个结果分数最高),构造答案返回值
自定义得分类思路
通过appId得到app相关的问题和结果集合(通过得分范围),统计用户总得分,遍历得分结果,找到第一个用户分数大于得分范围的结果,作为最终结果,构造答案返回值
全局执行器
两种实现方式
1.编程式(硬编码)
2.声明式(在每个策略类中通过接口声明对应的生效条件
接口
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ScoringStrategyConfig {
▼text复制代码int appType(); int scoringStrategy();
}
给策略类补充注解
@ScoringStrategyConfig(appType = 0, scoringStrategy = 0)
该注解使用了@component注解,会被当做bean来注入
平台智能化
智谱AI有3种接入方式(SDK、http、第三方框架)
封装AI模块
AI生成题目
主要的点就是调Prompt参数
问题
现在AI生成的题目依旧不稳定,但是测验的时候,其实是不同的
优化
如果要生成大量题目,gpt无法实现,只能分批生成,将历史生成的题目关联到上下文中,防止重复
AI智能评分
主要难点:调AI的Prompt参数
性能优化
当前问题:AI生成过慢,影响用户体验
解决方案:使用流,AI生成多少就让前端看到多少
RxJava响应式编程
响应式编程:
1.数据流:数据以流的形式存在(理解为河流),它可以被过滤、合并(stream流)
2.异步处理:响应式编程是异步的,操作不会阻塞主线程
3.变化传播:数据有变化就会传递【举例:可能啊可能,鱼皮说的是订阅股票的人会收到通知,我想的是现在手机上的外卖进度】
RxJava
RxJava是一个基于事件驱动的、利用可观测序列来实现异步编程的类库,是响应式编程在Java语言上的实现
事件驱动:出现操作行为。每当一个事件发生的时候,这个事件就会被推送给那些对它感兴趣的观察者
可观测序列:指一系列按照时间顺序发出的数据项,可以被观测和处理
RxJava应用场景
核心应用场景:UI场景
UI场景天然涉及响应和事件两点。eg:当我们在app上点击某个按钮(事件)的时候,就会有对应的弹窗(响应)
好处:异步操作(1.安卓的主线程是需要试试响应用户的请求,如果它去执行查询图片,app可能就会卡顿,影响用户体验
2.耗时的操作需要到子线程中操作,然后最终显示结果才需用利用主线程)
理解:可以让耗时的操作去子线程中执行,RxJava里面有写好的方法
RxJava核心知识点
基于观察者模式,观察者是Observer,被观察者是Observable和Flowable
Observable不具备反压处理能力,当生产速度超过消费速度的时候,会出现问题,导致内存溢出
Flowable可针对上述情况,有多种背压策略处理这种情况
常用操作符
1.变化类,map
2.聚合类,toList、toMap
3.过滤类,skip
4.连接,concat
5.排序
事件
1.onNext,被观察者每发送一次,触发一次
2.onError,错误触发
3.onComplete,最后一次调用onNext触发
依赖引入:因为智谱依旧引入了,不需要再引入
前后端实时通讯方案
1.轮询
前端定时调用后端接口获取数据,主动轮询是一种伪轮询,有可能会造成消息滞后
2.SSE
后端实时发送数据给前端
3.WebSocket
双向沟通
SSE技术
基本概念
服务器发送事件,一种用于服务器到客户端的单向、实时数据传输技术,基于HTTP协议
重要的特点:
单向通信、纯文本格式、保持连接、自动重连
SSE数据格式
注意
一定要实现Get请求
AI评分优化
需求分析
问题:AI评分响应时间较长;AI调用需要费用,如果用户对同样的题目做出同样的选择,理论会得到一样的解答
解决方案
缓存【响应慢、数据可重复】
方案设计
本地缓存和分布式缓存
分布式缓存劣势:成本高、增加系统复杂度
缓存设计
1.缓存key设计:
需求:如果用户对同样的题目做出同样的选择,理论会得到一样的解答
将应用id和用户的答案列表作为key【答案列表可能很长,可以使用哈希算法压缩key】
注意
如果是分布式缓存,可以在key开头拼接业务前缀,单独为每个业务创建缓存(类似苍穹外卖里面缓存菜品、套餐,黑马点评里面短信验证码、用户token值)
2.缓存value设计
缓存AI回答的结果,为了可读性存json结构,为了压缩空间可以使用二进制结构
3.设置缓存过期时间
业务流程
- 在AI答题之前,获取appId、用户选项,拼接key
- 去Redis里面寻找
- 没有调用ai接口
- ai接口返回json数组,缓存在Redis
注意
应用题目变更的时候自行清理Redis缓存数据
该缓存主要针对AI答案
分库分表设计
分表:单张表数据量过大,达到三、四千万的数据
如何分表:按照日期分配,打算在哪里加索引就用哪个字段分表,核心在于业务的查询,减少跨分库发表的查询
Sharding_JDBC原理
改写SQL(简化代码、提供人的信息即可)
注意
当user_answer更新的时候,注意先不要更新appId,因为更改appId的时候是不会自动更改库的
系统优化
幂等设计
场景问题:用户多次点击按钮,发送同一请求
电商平台里面当用户付款的时候出现付款不成功的问题,造成多次扣款
特点:一个操作执行多次和一个操作执行一次的结果是一致的
主流的幂等性设计
1.数据库唯一索引
2.数据库乐观锁
给表中的某一列增加一个字段
3.天然幂等操作
例如数据库中的查询、更新操作
4.分布式锁
解决并发操作
方案设计
业务流程
给用户答题记录表的哪一个字段添加唯一索引呢
【此时尚未插入数据,id还未生成】造一个字段作为唯一索引
1.前端进入答题页面的时候,请求后端发送一个全局唯一id
2.用户提交回答的时候将这个全局唯一id带上
3.插入数据库的时候也将这个保存到数据库中【使用数据库实习幂等性】
线程池隔离
将核心业务进行隔离,防止其它业务影响核心线程池
性能优化:针对不同任务类型配置不同的线程池(CPU密集型)
RxJava使用的线程是全局共享,使用的是无界队列,并且没有做限制,来一个任务创建一个线程池,容易造成OOM
方案设计
VIP专用线程池定义
创建一个Configuration配置类,创建线程池作为bean,将vip线程注入题目控制类里面,将SSE推送技术中使用到的RxJava使用到的线程池替换,判断完是否是会员的时候进行替换
统计分析
需求分析
1.根据数据统计,去进行功能优化,将热门应用排在前面,并添加缓存提升访问速度
2.分析应用内用户的答案结果分布,根据结果发布,可以优化对群体大的用户进行定制化开发或广告投放
技术选型
1.Excel(适合小规模的数据分析场景)
2.关系型数据库(数据规模不大、简答的数据分析)
3.第三方数据分析平台(要钱)
4.大数据分析(建设成本高)
后端开发
基于答题记录统计热门应用

