本文介绍了一个基于 Java 的 AI 图片生成网站的开发过程。
一、项目背景
项目初衷源于对最新模型绘画效果的认可,尝试自行实现。设计思路如下:
- 熟悉 Java 后端技术栈
- AI 绘画 API 已成熟
- 市面上的工具要么太贵,要么太复杂
- 目标是做一个简单好用的工具
二、技术选型
后端技术
- 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
}
注意事项:
- 使用 JWT 替代 Session,方便扩展
- 密码必须加密,采用 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);
task.setImageUrl(finalUrl);
task.setStatus();
taskMapper.updateById(task);
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);
分页查询:
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
七、总结
技术方面
- 选熟悉的技术栈:快速上线比技术炫酷更重要。
- 异步处理很重要:耗时操作务必异步,队列配合 WebSocket 提升体验。
- 做好监控和日志:ELK 收集日志,关键指标监控,便于定位问题。
产品方面
- 功能要简单:先验证核心需求,再考虑扩展。
- 用户体验第一:实时反馈、加载动画、友好提示。
- 控制成本:AI API 昂贵,需通过积分系统和数据清理控制成本。


