我用Java做了个AI图片生成网站,3天上线,现在月收入一般人我不告诉他
最近做了个AI图片生成的小项目,今天想跟大家分享一下整个开发过程。说实话,最开始就是看到最新模型的绘画效果非常好,想着能不能自己也搞一个。我的想法很简单:于是就开干了。作为一个Java开发,我选了自己最熟悉的技术栈:这个比较简单,就是常规的注册登录。关键代码思路:踩过的坑:这是最重要的部分,我用了异步队列的方式。为什么用队列?实现思路:关键点:这个功能让用户体验提升了一大截。实现方式:效果:为了控
最近做了个AI图片生成的小项目,今天想跟大家分享一下整个开发过程。
一、为什么要做这个项目?
说实话,最开始就是看到最新模型的绘画效果非常好,想着能不能自己也搞一个。
我的想法很简单:
- 我会Java后端
- AI绘画API现在很成熟了
- 市面上的工具要么太贵,要么太复杂
- 为什么不做一个简单好用的?
于是就开干了。
二、技术选型
作为一个Java开发,我选了自己最熟悉的技术栈:
后端技术
- Spring Boot 3.2 - 主框架,快速开发
- MyBatis Plus - 数据库操作,省事
- Redis - 缓存和队列
- MySQL 8.0 - 数据存储
- WebSocket - 实时推送生成进度
前端技术
- Vue 3 - 前端框架(虽然我不太擅长😅)
- Element Plus - UI组件库
- Axios - HTTP请求
AI服务
- Stable Diffusion API - 图片生成
- 对象存储 - 图片存储(用的云服务)
部署
- Docker - 容器化部署
- Nginx - 反向代理
- 云服务器 - 2核4G就够了
三、核心功能实现
1. 用户系统
这个比较简单,就是常规的注册登录。
关键代码思路:
// 用户注册
@PostMapping("/register")
public Result register(@RequestBody UserDTO userDTO) {
// 1. 校验邮箱格式
// 2. 检查邮箱是否已注册
// 3. 密码加密(BCrypt)
// 4. 生成token
// 5. 返回用户信息
}
// 用户登录
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
// 1. 验证邮箱密码
// 2. 生成JWT token
// 3. 存入Redis(设置过期时间)
// 4. 返回token
}
踩过的坑:
- 一开始用Session,后来改成JWT,方便扩展
- 密码一定要加密,我用的BCrypt
- Token过期时间设置7天,太短用户体验不好
2. 图片生成核心逻辑
这是最重要的部分,我用了异步队列的方式。
为什么用队列?
- AI生成图片需要时间(10-30秒)
- 不能让用户一直等着
- 可以控制并发,避免API被打爆
实现思路:
@Service
public class ImageGenerateService {
// 提交生成任务
public String submitTask(GenerateRequest request) {
// 1. 创建任务记录
Task task = new Task();
task.setUserId(request.getUserId());
task.setPrompt(request.getPrompt());
task.setStatus("pending");
taskMapper.insert(task);
// 2. 放入Redis队列
redisTemplate.opsForList()
.rightPush("task:queue", task.getId());
// 3. 返回任务ID
return task.getId();
}
// 异步处理任务
@Async
public void processTask() {
while (true) {
// 1. 从队列取任务
String taskId = redisTemplate.opsForList()
.leftPop("task:queue");
if (taskId == null) {
Thread.sleep(1000);
continue;
}
// 2. 调用AI API生成图片
Task task = taskMapper.selectById(taskId);
String imageUrl = callAIApi(task.getPrompt());
// 3. 上传到对象存储
String finalUrl = uploadToOSS(imageUrl);
// 4. 更新任务状态
task.setImageUrl(finalUrl);
task.setStatus("completed");
taskMapper.updateById(task);
// 5. 通过WebSocket推送给用户
webSocketService.sendToUser(
task.getUserId(),
task
);
}
}
}
关键点:
- 用Redis的List做队列,简单可靠
- 异步处理,不阻塞主线程
- WebSocket实时推送,用户体验好
3. WebSocket实时推送
这个功能让用户体验提升了一大截。
实现方式:
@ServerEndpoint("/ws/{userId}")
@Component
public class WebSocketServer {
// 存储所有连接
private static Map<String, Session> sessions =
new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session,
@PathParam("userId") String userId) {
sessions.put(userId, session);
System.out.println("用户连接:" + userId);
}
@OnClose
public void onClose(@PathParam("userId") String userId) {
sessions.remove(userId);
System.out.println("用户断开:" + userId);
}
// 发送消息给指定用户
public void sendToUser(String userId, Object message) {
Session session = sessions.get(userId);
if (session != null && session.isOpen()) {
session.getAsyncRemote()
.sendText(JSON.toJSONString(message));
}
}
}
效果:
- 用户提交任务后,页面显示"生成中…"
- 生成完成后,自动刷新显示图片
- 不需要用户手动刷新
4. 积分系统
为了控制成本,我加了个积分系统。
逻辑很简单:
- 新用户注册送100积分
- 生成一张图消耗10积分
- 可以充值购买积分
@Service
public class CreditService {
// 扣除积分
@Transactional
public boolean deductCredit(Long userId, int amount) {
User user = userMapper.selectById(userId);
// 检查余额
if (user.getCredit() < amount) {
return false;
}
// 扣除积分
user.setCredit(user.getCredit() - amount);
userMapper.updateById(user);
// 记录流水
CreditLog log = new CreditLog();
log.setUserId(userId);
log.setAmount(-amount);
log.setType("generate");
creditLogMapper.insert(log);
return true;
}
}
注意事项:
- 一定要加事务,避免并发问题
- 记录详细的流水,方便对账
- 可以加个定时任务,清理过期积分
四、遇到的坑和解决方案
坑1:并发问题
问题:
多个用户同时生成图片,有时候会出现任务丢失。
原因:
Redis队列操作不是原子的。
解决:
改用Redis的BLPOP命令,阻塞式获取,保证原子性。
// 改进后的代码
String taskId = redisTemplate.opsForList()
.leftPop("task:queue", 10, TimeUnit.SECONDS);
坑2:内存溢出
问题:
跑了几天后,服务器内存爆了。
原因:
WebSocket连接没有正确关闭,导致Session堆积。
解决:
- 加了心跳检测,定时清理无效连接
- 设置连接超时时间
- 限制单个用户的最大连接数
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void cleanInvalidSessions() {
sessions.forEach((userId, session) -> {
if (!session.isOpen()) {
sessions.remove(userId);
}
});
}
坑3:AI API限流
问题:
调用AI API太频繁,被限流了。
解决:
- 加了令牌桶限流
- 控制每秒最多调用5次
- 超过限制的任务延迟处理
@Component
public class RateLimiter {
private final Semaphore semaphore = new Semaphore(5);
public boolean tryAcquire() {
return semaphore.tryAcquire();
}
public void release() {
semaphore.release();
}
}
坑4:图片存储成本
问题:
用户生成的图片越来越多,存储费用暴涨。
解决:
- 定期清理30天前的图片
- 压缩图片质量(从原图2MB压到500KB)
- 用户可以选择下载到本地
五、性能优化
1. 数据库优化
加索引:
-- 用户表
CREATE INDEX idx_email ON user(email);
-- 任务表
CREATE INDEX idx_user_status ON task(user_id, status);
CREATE INDEX idx_create_time ON task(create_time);
分页查询:
// 使用MyBatis Plus的分页
Page<Task> page = new Page<>(pageNum, pageSize);
taskMapper.selectPage(page,
new QueryWrapper<Task>()
.eq("user_id", userId)
.orderByDesc("create_time")
);
2. Redis缓存
缓存用户信息:
// 先查缓存
String userJson = redisTemplate.opsForValue()
.get("user:" + userId);
if (userJson != null) {
return JSON.parseObject(userJson, User.class);
}
// 缓存未命中,查数据库
User user = userMapper.selectById(userId);
redisTemplate.opsForValue()
.set("user:" + userId,
JSON.toJSONString(user),
1, TimeUnit.HOURS);
3. 接口优化
批量查询:
// 不好的做法:循环查询
for (Task task : tasks) {
User user = userMapper.selectById(task.getUserId());
task.setUser(user);
}
// 好的做法:批量查询
List<Long> userIds = tasks.stream()
.map(Task::getUserId)
.collect(Collectors.toList());
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, u -> u));
tasks.forEach(task ->
task.setUser(userMap.get(task.getUserId()))
);
六、部署上线
Docker部署
Dockerfile:
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
docker-compose.yml:
version: '3'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: your_password
MYSQL_DATABASE: image_gen
volumes:
- mysql-data:/var/lib/mysql
redis:
image: redis:7
volumes:
- redis-data:/data
volumes:
mysql-data:
redis-data:
一键部署:
docker-compose up -d
七、经验总结
技术方面
1. 选熟悉的技术栈
- 我用Java是因为我最熟
- 不要为了新技术而用新技术
- 快速上线比技术炫酷更重要
2. 异步处理很重要
- 耗时操作一定要异步
- 队列是个好东西
- WebSocket提升体验
3. 做好监控和日志
- 我用的ELK收集日志
- 关键指标要监控
- 出问题能快速定位
产品方面
1. 功能要简单
- 我只做了核心功能
- 不要一开始就做大而全
- 先验证需求再扩展
2. 用户体验第一
- 实时反馈很重要
- 加载动画不能少
- 错误提示要友好
3. 控制成本
- AI API很贵,要控制调用
- 积分系统是必须的
- 定期清理无用数据
更多推荐


所有评论(0)