最近做了个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很贵,要控制调用
  • 积分系统是必须的
  • 定期清理无用数据
Logo

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

更多推荐