听说你毕业很多年了?那么来做题吧🦶
序言
![]()
自别学宫,岁月如狗,撒腿狂奔,不知昔日学渣今何在?
左持键盘,右捏鼠标,微仰其首,竟在屏幕镜中显容颜!
心中微叹,曾几何时,提笔杀题,犹如天上人间太岁神。
知你想念,故此今日,鄙人不才,出题小侠登场献丑了。
起因
在这一篇《从 0 到上架:用 Flutter 一天做一款功德木鱼》文章中,我的 木鱼APP 最终陨落了,究其原因就是这种 APP 在 商店中太多了,如果你要想成功上架,无异于要脱胎换骨。
后面有时间了,我打算将其重铸为
修仙敲木鱼,通过积攒鱼力,突破秩序枷锁,成就无上木鱼大道。
因此,我吸取失败的教训,着力于开发一款比较 独特的APP ,结合这个AI大时代的背景,这款AI智能 出题侠 就应运而生了。最后总算是不辜负我的努力,成功上架了。
接下来就向大家说说 它的故事 吧。
![]()
实践
一. 准备阶段
1.流程设计
flowchart TD
%% 启动与登录
A[启动页] --> B[无感登录]
B --> C[进入导航页]
%% 主壳导航
C --> H[首页]
C --> R[记录]
C --> T[统计]
C --> P[我的]
%% 首页出题 → 记录
H --> H1[输入主题/高级设置]
H1 --> H2[生成题目]
H2 --> H3[提示后台生成]
H3 --> R
%% 记录 → 答题/详情
R --> R1{记录状态}
R1 -->|进行中| Q[进入答题页]
R1 -->|已完成| RD[记录详情]
RD --> E[秒懂百科]
%% 答题流程
Q --> Q1[作答 / 提交]
Q1 --> Q2[保存成绩]
Q2 --> R
%% 统计页
T --> T1[刷新统计数据]
%% 我的页
P --> P1[设置/关于]
P --> P2[隐私政策]
P --> P3[注销]
P3 --> |确认后| P4[清除 token / 返回未登录状态]
2. 素材获取
![]()
App的 logo 和其中的 插图,我都是用的 Doubao-Seedream-4.0 生成的,一次效果不行就多生成几次,最终还是能得到相对满意的结果。
到我写文章的时候,已经有了 Doubao-Seedream-4.5,大家可以去体验体验。
![]()
二. 开发阶段
1. 前端
前端毫无争议的使用的是 Flutter,毕竟要是以后发行 Android 也是非常方便的,无需重新开发。再结合 Trae,我只需要在口头上指点指点,那是开发的又快又稳,非常的轻松加愉快。
无须多言,这就是赛博口嗨程序员!🫡
![]()
2. 后端
后端就是,世界上最好的编程语言 JAVA 了,毕竟 SpringBoot 可太香了,我也是亲自上手。
2.1 依赖概览
<!-- ✅ 核心 LangChain4j 依赖 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- HuTool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<!-- MyBatis-PageHelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<!-- Sa-Token -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>1.42.0</version>
</dependency>
<!-- 提供 Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Knife4j -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<!-- 短信验证码 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-auth</artifactId>
<version>0.2.0-beta</version>
</dependency>
<!-- 阿里云短信服务 SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.24</version> <!-- 使用最新版 -->
</dependency>
......
这依赖一添加,满满的安全感:
- 数据库:我有MyBatis。
- AI:我有LangChain4j。
- 登录鉴权:我有Sa-Token。
- ......
2.2 接口限流
要说到项目中最需要重点关注的部分,接口限流 无疑排在首位。无论是短信发送接口,还是调用 AI 的接口,一旦被恶意刷取或滥用,都可能导致资源耗尽、费用爆炸💥。
因此,本项目采用 注解 + AOP + Redis 的方式,构建了一套 轻量级、可配置、低侵入 的接口限流方案,在不影响业务代码结构的前提下,对高风险接口进行有效保护,确保系统在高并发场景下依然稳定可控。
代码示例:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限流 key 的前缀(唯一标识一个限流维度)
*/
String key();
/**
* 时间窗口,单位秒
*/
long window() default 60;
/**
* 时间窗口内允许的最大次数
*/
int limit() default 10;
/**
* 是否按 IP 维度区分限流
*/
boolean perIp() default false;
/**
* 是否按用户维度区分限流
*/
boolean perUser() default false;
/**
* 自定义提示信息
*/
String message() default "请求过于频繁,请稍后再试";
}
@Slf4j
@Aspect
@Component
public class RateLimitAspect {
@Resource
private RateLimitRedisUtil rateLimitRedisUtil;
@Around("@annotation(org.dxs.problemman.annotation.RateLimit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
String key = buildKey(rateLimit);
boolean allowed = rateLimitRedisUtil.tryAcquire(
key, rateLimit.limit(), rateLimit.window());
if (!allowed) {
log.warn("限流触发:key={}, limit={}, window={}s", key, rateLimit.limit(), rateLimit.window());
throw new RateLimitException(rateLimit.message());
}
return joinPoint.proceed();
}
private String buildKey(RateLimit rateLimit) {
StringBuilder key = new StringBuilder("ratelimit:").append(rateLimit.key());
if (rateLimit.perIp()) {
String ip = IpUtils.getIpAddress();
key.append(":").append(ip);
}
if (rateLimit.perUser()) {
String userId = StpUtil.getLoginIdAsString();
key.append(":").append(userId);
}
return key.toString();
}
}
在使用上,开发者只需在需要保护的接口方法上添加 @RateLimit 注解,即可声明该接口的限流规则。通过 key 区分不同业务场景,并可按需开启 IP 维度 或 用户维度 的限流控制,从而精确限制单一来源或单一用户的访问频率。
@NotLogin
@PostMapping("/sms")
@RateLimit(key = "sms", limit = 200, window = 3600, message = "短信调用太频繁,请1小时后再试")
public AjaxResult<String> sms(@Validated @RequestBody PhoneDTO dto) {
return AjaxResult.success(loginService.sms(dto));
}
@PostMapping("/generate")
@RateLimit(key = "generate", limit = 3, perUser = true, window = 3600*24, message = "每人每天仅可体验三次!")
@Operation(summary = "依据条件,生成题目")
public AjaxResult<Object> generate(@Validated @RequestBody GenerateRequestDTO dto) throws IOException, InterruptedException {
questionService.generate(dto);
return AjaxResult.success();
}
请求进入时,AOP 切面会拦截带有 @RateLimit 注解的方法,根据注解配置动态构建限流 Key,并交由 Redis 进行原子计数校验;若在指定时间窗口内超过访问上限,则直接中断请求并返回友好的限流提示,同时记录告警日志,便于后续排查与监控。
限流 Key 的结构统一为:
ratelimit:{业务key}:{ip}:{userId}
通过 Redis 过期机制自然形成时间窗口,既保证了并发场景下的准确性,也避免了额外的清理成本。
三. 上架备案
1.前提
![]()
![]()
想要备案上架,域名和服务器是必不可少的。
- 域名:你是在手机上,其实不需要啥好域名,因为大家根本看不见,十几块钱一年就行了。
- 服务器:花了三四百买个轻量级服务器就行了。
2. 阿里云备案
![]()
![]()
💡
小建议
像这里阿里云备案和获取管局审核,可以先行一步,在app开发完之前就可以提交了。
因为管局审核是要2-3周的,有可能我们的小APP开发好了,备案号都没有下来。
3. 苹果商店上架
![]()
信息这里按部就班,按照提示,一点点填写完成就行了,没啥特别的。
踩坑总结:
- 测试账号:你的APP中只要有登录模块,就一定要提供测试账号,就算你纯手机号登录也不行,必须提供测试账号。
-
注销功能:苹果商店硬性要求,必须要有
注销功能,但其实也没那么严格,你只要UI显示是那么回事就行,就当退出登录功能去做就行了。
4. 预览图制作推荐
![]()
![]()
![]()
注意是需要订阅付费的,要是有什么更好的,希望评论告知。😂
展望
在后续规划的新功能中,将以大学期末考试复习作为典型应用场景进行设计。通常在期末阶段,老师都会给出明确的考试范围、复习大纲以及相关资料文档,而临阵磨枪的学生往往面临资料繁多、重点分散、不知从何下手的问题。
针对这一痛点,用户可以将老师提供的复习文档直接导入 App,系统会基于 AI 对内容进行自动解析与归纳,将零散的文本信息整理为思维导图形式的知识图谱,清晰呈现各章节与知识点之间的层级与关联关系。
在此基础上,用户可围绕任意知识节点一键生成对应题目,用于针对性复习与自测,做到哪里薄弱练哪里。通过文档 → 知识图谱 → 题目练习 的闭环方式,帮助用户更高效地理解重点内容,提升期末复习的针对性与整体效率。
![]()
😭 作为大学毕业生的深彻感悟。
支持
![]()
AppStore 搜索 出题侠 即可,每个用户每天可免费使用三次。
感谢大家的支持与反馈。🙏