技术栈

springboot+vue前后端分离框架,maven进行项目管理,使用websocket+netty技术保证与ai大模型稳定交互,

JWT,mybatis

工具

mysql数据库,Hutool,Git

项目流程

本项目一共可分为四大板块:用户模块后勤处处长模块维修人员模块管理员模块

基本的业务流程

用户登录用户端申请报修,依据故障类型、故障位置、信息描述等基本信息生成一份报修工单。用户提交工单之后该工单信息会被传输道ai大模型当中,ai大模型会根据历史未完成的工单的相似度阈值进行整合,如果该工段与历史某个工单的相似度高于设置的阈值将不再生成新工单;如果低于阈值则生成一个新的工单保存道数据库并根据工单的基本信息按照ai设定的紧急度权重进行紧急度划分,这个就生成一份待接收工单,如果出现一级工单会将该工单短信发送给所有的对应类型的维修人员。(在这一流程中用户端和ai大模型之间使用websocket长连接+netty技术保证用户可以和ai大模型进行稳定交互)

维修人员登录系统会按照工人的类型展示该类型可以接收的工单,成功登录时会将未接收工单进行弹框提示。工人在接受工单的时候需要填写预计完成时间和该工单的预估耗资。提交申请后工人的状态由休息中变为工作中,防止工人重复接单;如果提交的预估耗资高于管理员设置的阈值就需要后勤处处长进行审批:1.通过,工单状态就会变为待处理状态,员工就可以进行维修;2.驳回,员工从工作中恢复到休息状态,工单状态保存待接取,并清空该员工的接单提交等待再次接受直到后勤处处长通过审批。如果提交预估耗资低于阈值就无需后勤处处长经行审批。员工维修完成提交维修结果工单就会变为待评价,用户评鉴完成该工单就完整结束。

四大模块的内容:

用户端: 只有申请报修工单和评价

后勤处端:查看全校所有的维修工单数据依据类型/区域进行统计;审批大额工单;资源调配手动进行跨维修人员类型进行维修支援;考核管理依据用户的反馈,完工率,响应时间等进行员工的绩效考核;审批管理员申请的ai合并及紧急度权重调整申请;审批员工的异常反馈(如需要援助协同,采购配件等)。

员工端:接取工单,提交完成工单详情和异常反馈申请。

管理员端:进行新用户和员工注册,调整可申请维修地点,完善员工类型可接取的工单类型;维护ai大模型参数设置和大额工单阈值调整。

技术亮点:

使用websocket+netty技术保证与ai大模型稳定交互

相关面试题

MySQL

1. 关系型和非关系型数据库的核心区别?
关系型(如MySQL):用表结构组织数据,支持SQL、事务、数据一致性,适合复杂查询;
​
非关系型(NoSQL,如Redis、MongoDB):键值对/文档等存储,无固定结构,读写快、易扩展,适合大数据量、高并发场景。
2. 关系型数据库的优点?
易理解(关系模型)、保证数据一致性、数据更新开销小、支持复杂查询(where子句)。
3. NoSQL数据库的优点?
读写效率高(无需SQL解析)、基于键值对易扩展、支持多类型数据(图片、文档)。
4. 一条MySQL语句的执行步骤?
客户端请求 → 连接器(验证身份、授权)→ 查询缓存(有则返回,无则继续)→ 分析器(词法+语法分析)→ 优化器(选最优执行方案)→ 执行器(校验权限,调用引擎接口)→ 引擎层取数据返回(开启缓存则缓存结果)。
5. MySQL使用索引的核心原因?
提高数据查询效率(类似书的目录);额外作用:保证数据唯一性、避免排序和临时表、将随机IO转为顺序IO。
6. 索引常见的三种底层数据结构及优缺点?
- 哈希表:等值查询快(O(1)),不支持范围查询;
​
- 有序数组:等值/范围查询快,适合静态数据,更新成本高;
​
- N叉树(B+树):适配磁盘访问,读写性能均衡,广泛用于数据库引擎。
7. 索引的常见类型(聚簇索引 vs 二级索引)?
- 聚簇索引(主键索引):InnoDB中,叶子节点存整行数据;
​
- 二级索引(非主键索引):叶子节点存主键值,查询需回表(除非覆盖索引)。
8. InnoDB和MyISAM的B+树索引区别?

InnoDB:叶子节点存数据本身,数据文件即索引文件;

MyISAM:叶子节点存数据物理地址,索引文件与数据文件分离。

9. InnoDB为什么选择B+树作为索引结构?
哈希索引不支持范围/排序;B树非叶子节点存数据,会增加随机IO;B+树叶节点连表,减少顺序遍历的随机IO,适配磁盘访问和复杂查询。
10. 覆盖索引和索引下推的定义?
- 覆盖索引:索引包含查询所需所有字段,无需回表,提升查询性能;

- 索引下推:MySQL5.6引入,索引遍历中先过滤不满足条件的记录,减少回表次数。
11. 哪些操作会导致索引失效?
1. like %xx、like %xx%(左/左右模糊);2. 索引字段用函数/表达式计算;3. 索引隐式转换;4. where中or有非索引列。
12. 字符串如何高效加索引?
1. 完整索引(占用空间大);2. 前缀索引(省空间,无法用覆盖索引);3. 倒序前缀索引(解决前缀区分度低);4. 哈希字段索引(查询稳,不支持范围)。
13. redo log和binlog的核心区别?
- redo log:InnoDB层,循环写,记录未刷盘的物理修改,用于crash-safe,保障数据一致性;

- binlog:Server层,追加写,记录全量逻辑操作,用于主从复制和数据恢复,不支持crash-safe。
14. 为什么需要redo log?
配合WAL机制(先写日志再写磁盘),解决MySQL异步刷盘的内存数据丢失问题,实现crash-safe,确保异常重启后数据可恢复。
15. 为什么redo log能实现crash-safe,binlog不能?
redo log循环写,只记录未刷盘数据,刷盘后删除;binlog追加写(全量日志),无标识区分已刷盘/未刷盘数据,无法单独恢复未刷盘数据。
16. 什么是两阶段提交?作用是什么?
将redo log的写入拆分为prepare和commit,中间插入binlog写入,确保redo log和binlog逻辑一致,避免主从同步、数据恢复时的数据不一致。
17. WAL技术是什么?优点?
 Write-Ahead Logging(预写日志),先写日志到内存,再异步写磁盘;优点:减少磁盘IO,提升响应速度,配合redo log实现crash-safe。
18. binlog的三种格式及特点?
- Statement:记录SQL语句,日志量小,可能存在主从不一致;

- Row:记录行修改细节,主从一致,日志量大;

- Mixed:混合前两种,普通语句用Statement,特殊语句用Row。
19. change buffer是什么?适用场景?
更新未在内存中的数据页时,将更新操作缓存到change buffer,下次访问该页时合并操作;仅支持普通索引。

适用场景:写多读少(如账单、日志系统),避免频繁读磁盘。
20. MySQL如何保证数据不丢失?
确保redo log和binlog持久化到磁盘;异常重启后,根据两阶段提交逻辑恢复:redo log为commit则直接恢复,为prepare则校验binlog决定回滚或执行。
21. drop、truncate、delete的区别?
- delete:逐行删除,记录日志,可回滚,触发触发器,速度慢;

- truncate:全量删除,不记录日志,不可回滚,不触发触发器,速度快;

- drop:删除表结构+数据,释放空间,不可回滚,速度最快。
22. 什么是幻读?如何解决?
定义:同一事务中,两次查询同一范围,第二次看到第一次未出现的新插入行(可重复读隔离级别+当前读场景)。

解决:加间隙锁(Gap Lock),锁住行间隙,阻止新插入。
23. InnoDB的锁分类(核心)?
- 表级锁:MDL(元数据锁,保护表结构)、AUTO-INC(自增锁,语句级);

- 行级锁:RecordLock(记录锁)、GapLock(间隙锁)、Next-KeyLock(记录+间隙锁)。
24. MySQL主备同步的核心流程?
  1. 备库通过change master指定主库信息和同步位点;2. 备库启动io_thread(拉取主库binlog,存为中转日志)和sql_thread(执行中转日志);3. 主库有新binlog时,主动推送至备库。

25. 主备延迟的主要原因?
备库性能差、备库压力大、主库执行大事务(如执行10分钟的SQL,备库延迟10分钟)。
26. InnoDB为什么用自增ID作为主键?
自增ID有序,插入时追加操作,不触发叶子节点分裂,无碎片,写入效率高;业务字段做主键易随机插入,导致频繁分页、碎片增多。
27. 自增主键ID为什么不连续?
唯一键冲突、事务回滚、自增主键批量申请;MySQL为提升性能,不回退自增值,导致不连续。
28. count(*)、count(1)、count(字段)的效率对比?
效率排序:count(字段) < count(主键id) < count(1) ≈ count(*);优先用count(*)(InnoDB优化,不取值,直接累加)。
29. 原本快的SQL突然变慢,可能原因及解决?
原因:资源不足、表/行锁阻塞、索引使用不当、回表次数多;

解决:force index强行选索引、优化SQL引导索引、新建合适索引、删除无用索引。
30. MySQL大表查询为什么不会爆内存?
采用“边读边发”,不保存完整结果集;InnoDB用改进LRU算法管理Buffer_Pool,冷数据全扫描影响可控,不会耗尽内存。

websocket+netty

问题 1:什么是 WebSocket?它和 HTTP 有什么区别?

答案:

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,属于应用层协议,能实现客户端和服务端的双向实时通信。

与 HTTP 的核心区别:

  1. 通信模式:HTTP 是单向(请求 - 响应),客户端主动发起,服务端被动响应;WebSocket 是双向,连接建立后双方可主动发消息。

  2. 连接状态:HTTP 是无状态、短连接(HTTP/1.1 默认长连接但仍基于请求 - 响应);WebSocket 是有状态、长连接。

  3. 握手方式:WebSocket 基于 HTTP 握手(通过 Upgrade 头升级协议),握手成功后脱离 HTTP。

  4. 头部开销:HTTP 每次请求携带大量头部信息;WebSocket 仅握手时开销大,后续通信头部极小。

问题 2:Netty 实现 WebSocket 服务的核心步骤是什么?

答案:核心步骤(以 Netty 4.x 为例):

  1. 配置 Netty 服务端启动类(ServerBootstrap),设置主从 Reactor 线程组;

  2. 设置 Channel 类型(NIO 的NioServerSocketChannel);

  3. 配置初始化器(ChannelInitializer),向 pipeline 添加处理器:

  • HTTP 编解码器(HttpServerCodec);

  • 聚合 HTTP 请求 / 响应(HttpObjectAggregator,处理大报文);

  • 支持 WebSocket 的 HTTP 压缩(可选,HttpContentCompressor);

  • WebSocket 协议升级处理器(WebSocketServerProtocolHandler,指定 WebSocket 路径如/ws);

  • 自定义业务处理器(继承SimpleChannelInboundHandler,处理消息收发);

  1. 绑定端口并启动服务。

问题 3:Netty 中如何处理 WebSocket 的粘包 / 半包问题?

答案:

  1. WebSocket 基于 TCP,TCP 是流式协议,必然存在粘包 / 半包;

  2. Netty 通过内置的帧解码器解决:

  • 对于 HTTP 阶段:使用HttpObjectAggregator聚合 HTTP 请求的多个分段,避免半包;

  • 对于 WebSocket 阶段:WebSocketServerProtocolHandler会自动处理 WebSocket 的帧(如 TextWebSocketFrame、BinaryWebSocketFrame),Netty 的 WebSocket 帧解码器会根据帧的边界(FIN 位、长度字段)拆分数据,无需手动处理;

  • 若自定义二进制帧,可配合LengthFieldBasedFrameDecoder指定长度字段,进一步确保帧的完整性。

问题 4:WebSocket 连接断开后如何重连?Netty 服务端如何检测离线客户端?

答案:

  • 客户端重连:

  1. 监听 WebSocket 的onclose事件,触发后通过定时器(如 setTimeout)尝试重新建立连接,可设置重连间隔递增(避免频繁重试);

  2. 增加重连次数限制,防止无限重连。

  • 服务端检测离线:

  1. 使用 Netty 的IdleStateHandler(空闲状态处理器),设置读空闲时间(如 30 秒),若客户端在指定时间内无消息,触发userEventTriggered事件;

  2. 在自定义处理器中捕获IdleStateEvent,主动关闭连接并清理客户端状态;

1. 在你的工单系统中,为什么选择 WebSocket + Netty 而不是 HTTP 轮询 / 长轮询?

答案

工单系统核心场景是实时性要求高的消息推送(如工单派单、状态变更、催单提醒、客服回复),HTTP 轮询 / 长轮询存在明显缺陷:

  • 轮询:客户端定时请求服务端,存在无效请求(多数请求无新数据),浪费带宽和服务器资源;

  • 长轮询:服务端挂起请求直到有数据 / 超时,虽减少请求数,但仍基于 HTTP 短连接,每次请求需携带完整头部,开销大;

  • WebSocket + Netty:一次握手建立长连接,全双工通信,服务端可主动推送工单状态变更,延迟 < 100ms,且 Netty 基于 Reactor 模型支持高并发(工单系统峰值需同时推送数千个在线客服的工单消息),资源占用远低于轮询方案。

2. 你的工单系统中,WebSocket + Netty 具体用在哪些核心场景?

答案

核心场景:

  1. 工单状态实时推送:工单创建 / 派单 / 接单 / 完结 / 驳回等状态变更时,服务端主动推送给对应的客服、工程师、客户;

  2. 工单催单 / 提醒推送:客户催单、工单超时提醒、待处理工单提醒等实时推送给责任人;

  3. 客服 - 客户实时沟通:工单附带的在线沟通功能,基于 WebSocket 实现双向消息收发;

  4. 工单进度实时同步:工程师处理工单(如上门、维修中)的进度,实时推送给客户和后台管理端;

  5. 在线状态检测:客服 / 工程师的在线 / 离线状态,通过 WebSocket 心跳实时同步到管理端。

3. 工单系统中,WebSocket 连接如何和用户 / 工单绑定?

答案

核心实现逻辑:

  1. 客户端建立 WebSocket 连接时,携带用户 ID / 令牌 / 工单 ID(如在 URL 中:ws://xxx/ws?userId=123&token=xxx);

  2. 服务端(Netty)在 handlerAdded 阶段验证令牌有效性,通过后将 Channel(连接)与 userId/工单ID 绑定,存入全局映射表(如 ConcurrentHashMap<String, Channel>);

  3. 当某个工单状态变更时,根据工单 ID 查询关联的用户 ID,再从映射表中获取对应的 Channel,通过 writeAndFlush 推送消息;

  4. 连接断开(handlerRemoved)时,从映射表中移除该 Channel,避免内存泄漏。

4. 工单系统中,如何保证 WebSocket 推送的消息不丢失?

答案

针对工单消息(如派单、状态变更)的核心保障方案:

  1. 消息持久化:推送前将消息存入数据库 / Redis(标记 “未推送”),推送成功后更新状态为 “已推送”;

  2. 确认机制:客户端收到消息后,立即返回 Ack 确认帧,服务端收到 Ack 后才标记消息 “已推送”;

  3. 重推机制:若服务端未收到 Ack,或连接断开,待客户端重连后,查询该用户 / 工单的 “未推送” 消息,重新推送;

  4. 失败降级:WebSocket 推送失败时,自动降级为短信 / 邮件 / APP 推送,确保工单提醒触达责任人。

5. 工单系统高并发场景下(如高峰期同时创建上千工单),Netty 如何优化?

答案

针对工单系统高并发优化措施:

  1. 线程池隔离:将 Netty 的 IO 线程(WorkerGroup)与工单业务线程池分离,Handler 中仅处理消息编解码,耗时操作(如查询工单、更新数据库)提交到业务线程池,避免阻塞 IO 线程;

  2. 连接限流:通过 ChannelLimitHandler 限制单 IP 最大连接数,防止恶意连接占用资源,保障工单推送核心链路;

  3. 内存优化:使用 PooledByteBufAllocator 池化内存,设置合理的 ByteBuf 初始容量(适配工单消息大小,如 1024 字节),避免频繁扩容;

  4. 批量推送:对同一客服的多个工单消息(如多个待处理工单提醒)进行批量聚合后推送,减少 IO 次数;

  5. 空闲连接清理:通过 IdleStateHandler 设置 5 分钟读空闲超时,自动关闭无活动的客服 / 客户连接,释放资源。

6. Netty 服务如何监控和排查问题(如推送延迟、连接异常)?

答案

项目中落地的监控 / 排查方案:

  1. 关键指标监控:

    • 连接数:通过 ChannelGroup.size() 监控在线客服 / 客户数,配置阈值告警(如连接数 > 5000 告警);

    • 推送延迟:记录工单消息从生成到推送成功的耗时,超过 500ms 记入慢日志;

    • 失败率:统计工单消息推送失败数 / 总数,失败率 > 1% 触发告警;

  2. 日志排查:

    • 记录每个工单消息的推送日志(包含 ticketId、userId、推送时间、是否成功);

    • 记录 Netty 异常日志(如连接断开原因、解码失败原因);

  3. 性能分析:

    • 开启 Netty 自带的内存泄漏检测(ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.ADVANCED));

    • 使用 Arthas 监控 Netty 线程池状态、Channel 读写耗时,定位瓶颈。

JWT和Hutool

1. JWT 是什么?适合解决什么问题?

答案:

JWT(JSON Web Token)是一种无状态的身份凭证格式,常用于登录后给前端签发 token,让前端在后续请求中携带该 token 完成身份验证。

特点:

  • 无状态:服务端一般不需要保存会话(Session)

  • 可自包含:token 内可携带少量声明(如用户标识、过期时间)

  • 跨端友好:Web/小程序/App 都可用同一套机制


2. JWT 的组成结构是什么?

答案:

JWT 一般由三段组成:Header.Payload.Signature

  • Header:声明算法(如 HS256

  • Payload:业务声明(如 subexpiat

  • Signature:对前两段签名,防篡改


3. 本项目 JWT 的 token 是如何签发的?

答案:

登录成功后,后端调用 JwtUtil.generateToken(id) 生成 token,核心步骤:

  • setSubject(id):把用户标识写入 sub(本项目存放的是 id

  • setIssuedAt(...):签发时间 iat

  • setExpiration(...):过期时间 exp

  • signWith(key, HS256):签名生成 token 字符串

前端拿到 token 后存入 localStorage,供后续请求复用。


4. 本项目是如何校验 token 并拿到当前登录人信息的?

答案:

后端在拦截器 preHandle 做统一认证:

  • 从请求头 Authorization 读取 token(兼容 Bearer 前缀)

  • jwtUtil.validateToken(token):验签 + 过期校验

  • 校验失败返回 401

  • 校验通过后用 jwtUtil.getUsernameFromToken(token) 解析出 sub(实际是 id),并 request.setAttribute("id", userId)

业务接口再通过 request.getAttribute("id") 获取当前登录人 id,用于查询用户信息或做数据隔离。


5. 为什么 token 一般放在 Authorization: Bearer <token>

答案:

这是业界通用约定,优点:

  • 语义清晰:Authorization 表示认证信息

  • 与业务参数解耦:不污染 URL/Body

  • 便于统一拦截器处理与网关透传

本项目中前端通过 Axios request 拦截器统一加上该请求头。


6. token 过期/无效在本项目里如何处理?

答案:

  • 后端:validateToken 失败直接返回 401(提示 token 无效或过期)

  • 前端:Axios response 拦截器捕获 401,清除 localStorage 中的 token,并跳转登录页(提示重新登录)


7. JWT 的安全风险有哪些?如何规避?

答案:

常见风险:

  • token 被窃取:任何人拿到都能冒用

  • 无状态难“强制下线”:服务端不存会话

  • payload 可解码:不应存敏感信息(如密码)

常见规避:

  • 全站 HTTPS

  • token 只存用户标识/权限标识等非敏感信息

  • 设置合理过期时间(短 token)

  • 需要强制下线时引入黑名单/版本号/刷新机制


8. Hutool 是什么?在项目里一般用来做什么?

答案:

Hutool 是一个 Java 工具库集合,提供大量开箱即用的工具能力。本项目中 Hutool 主要用于 Excel 导入/导出(对 POI 做了封装),降低读写表格的代码量。


9. Hutool Excel 导出常见流程与方法有哪些?

答案:

典型导出链路:

  • ExcelUtil.getWriter(true) 创建 ExcelWriter

  • ExcelWriter.addHeaderAlias(字段名, 中文列名) 配表头映射

  • ExcelWriter.setOnlyAlias(true) 控制只导出映射列

  • ExcelWriter.write(list, true) 写入数据

  • writer.flush(outputStream) 输出到响应流

  • writer.close() 关闭释放资源

配合设置响应头(如 Content-Disposition)让浏览器下载。


10. Hutool Excel 导入常见流程与方法有哪些?

答案:

典型导入链路:

  • ExcelUtil.getReader(inputStream) 创建 ExcelReader

  • ExcelReader.addHeaderAlias("中文表头", "字段名") 配表头映射

  • ExcelReader.readAll(实体类.class) 读取为对象列表

  • 读完后做二次校验/转换(例如名称转 ID、不存在则报错)

  • 再批量/逐条入库

Git

1. Git 和 SVN 有什么区别?

考点:是否理解分布式 vs 集中式 参考答案

  • Git 是分布式,本地拥有完整历史记录,离线可提交;SVN 是集中式,必须联网才能提交。

  • Git 分支更轻量、更灵活;SVN 分支是完整目录拷贝,较笨重。

2. 列举你常用的 Git 命令

考点:日常使用频率 参考答案

  • git clonegit addgit commitgit pushgit pullgit mergegit branchgit checkoutgit loggit status

3. 什么是 HEAD?什么是 master?

考点:对指针概念的理解 参考答案

  • HEAD 是一个指针,指向当前所在的本地分支(也可以指向某个提交,即“分离头指针”状态)。

  • master 是 Git 默认创建的主分支名称,现在很多团队改用 main。

4. 提交时写错了 commit message,怎么办?

参考答案

  • 如果还没 push:git commit --amend -m "新的注释"

  • 如果已经 push:修改后再 git push --force(⚠️ 需谨慎,会覆盖远程)

5. 不小心把代码提交错了分支,怎么挪到正确分支?

参考答案

git checkout 正确分支
git cherry-pick 那个错误的commit的哈希值
git checkout 原分支
git reset --hard HEAD~1  # 删掉原来错误分支上的提交
6. git pull 和 git fetch 有什么区别?

参考答案

  • git fetch:只下载远程更新,不合并到本地工作区

  • git pull = git fetch + git merge(或 git rebase

7. 合并时遇到冲突怎么解决?

参考答案

  1. 打开冲突文件,搜索 <<<<<<<=======>>>>>>>

  2. 手动修改成最终想要的样子

  3. git add 标记为已解决

  4. git commit 完成合并

8. merge 和 rebase 有什么区别?各自优缺点?

考点:是否理解两种合并逻辑 参考答案

  • merge:保留完整历史,会产生一个 merge commit,适合记录真实合并节点

  • rebase:将提交“移植”到目标分支,形成线性历史,更整洁,但不要对公共分支做 rebase

  • 一句话原则:公共分支用 merge,个人分支用 rebase

9. 如何撤销一个已经 push 到远程的提交?

参考答案

  • 安全做法:git revert <commit>(生成一个新提交,反做之前的改动)

  • 危险做法:git reset --hard <旧commit> + git push --force(会覆盖历史,仅用于个人分支)

10. 误删了一个还没合并的分支,能找回吗?

参考答案

  • git reflog 找到分支最后一次的 commit 哈希

  • git branch 新分支名 那个哈希 即可恢复

11. 想暂存当前工作区去修一个紧急 bug,回来继续,怎么做?

参考答案

git stash        # 暂存当前修改
git checkout bug分支
# 修复并提交
git checkout 原分支
git stash pop    # 恢复之前的工作
12. 提交了一个大文件,push 时被拒绝怎么办?

考点:是否遇到过 .gitignore 或清理问题 参考答案

  • 如果刚提交:git rm --cached 大文件,重新 commit

  • 如果已经 push:需要用 git filter-branch 或 BFG 工具重写历史(⚠️ 会影响所有人,需团队协调)

13. 你们团队用 Git Flow 吗?简单描述一下流程

参考答案

  • master:主分支,始终可发布

  • develop:开发主干

  • feature/*:新功能分支

  • release/*:预发布分支

  • hotfix/*:紧急修复分支

14. 什么是 PR / MR?code review 怎么配合 Git 做?

参考答案

  • PR(Pull Request)或 MR(Merge Request)是代码审查的机制

  • 开发者在 feature 分支完成后,发起 PR,指定 reviewer

  • 审查通过后,由 reviewer 合并到目标分支

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐