2026/1/16 16:05:45
网站建设
项目流程
龙岩网站建设公司,制作灯笼的手工做法简单漂亮,sem账户托管公司,营销网站建设费用最近做了个AI图片生成的小项目#xff0c;今天想跟大家分享一下整个开发过程。
一、为什么要做这个项目#xff1f;
说实话#xff0c;最开始就是看到最新模型的绘画效果非常好#xff0c;想着能不能自己也搞一个。
我的想法很简单#xff1a;
我会Java后端AI绘画API现在很…最近做了个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)publicResultregister(RequestBodyUserDTOuserDTO){// 1. 校验邮箱格式// 2. 检查邮箱是否已注册// 3. 密码加密BCrypt// 4. 生成token// 5. 返回用户信息}// 用户登录PostMapping(/login)publicResultlogin(RequestBodyLoginDTOloginDTO){// 1. 验证邮箱密码// 2. 生成JWT token// 3. 存入Redis设置过期时间// 4. 返回token}踩过的坑一开始用Session后来改成JWT方便扩展密码一定要加密我用的BCryptToken过期时间设置7天太短用户体验不好2. 图片生成核心逻辑这是最重要的部分我用了异步队列的方式。为什么用队列AI生成图片需要时间10-30秒不能让用户一直等着可以控制并发避免API被打爆实现思路ServicepublicclassImageGenerateService{// 提交生成任务publicStringsubmitTask(GenerateRequestrequest){// 1. 创建任务记录TasktasknewTask();task.setUserId(request.getUserId());task.setPrompt(request.getPrompt());task.setStatus(pending);taskMapper.insert(task);// 2. 放入Redis队列redisTemplate.opsForList().rightPush(task:queue,task.getId());// 3. 返回任务IDreturntask.getId();}// 异步处理任务AsyncpublicvoidprocessTask(){while(true){// 1. 从队列取任务StringtaskIdredisTemplate.opsForList().leftPop(task:queue);if(taskIdnull){Thread.sleep(1000);continue;}// 2. 调用AI API生成图片TasktasktaskMapper.selectById(taskId);StringimageUrlcallAIApi(task.getPrompt());// 3. 上传到对象存储StringfinalUrluploadToOSS(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})ComponentpublicclassWebSocketServer{// 存储所有连接privatestaticMapString,SessionsessionsnewConcurrentHashMap();OnOpenpublicvoidonOpen(Sessionsession,PathParam(userId)StringuserId){sessions.put(userId,session);System.out.println(用户连接userId);}OnClosepublicvoidonClose(PathParam(userId)StringuserId){sessions.remove(userId);System.out.println(用户断开userId);}// 发送消息给指定用户publicvoidsendToUser(StringuserId,Objectmessage){Sessionsessionsessions.get(userId);if(session!nullsession.isOpen()){session.getAsyncRemote().sendText(JSON.toJSONString(message));}}}效果用户提交任务后页面显示生成中…生成完成后自动刷新显示图片不需要用户手动刷新4. 积分系统为了控制成本我加了个积分系统。逻辑很简单新用户注册送100积分生成一张图消耗10积分可以充值购买积分ServicepublicclassCreditService{// 扣除积分TransactionalpublicbooleandeductCredit(LonguserId,intamount){UseruseruserMapper.selectById(userId);// 检查余额if(user.getCredit()amount){returnfalse;}// 扣除积分user.setCredit(user.getCredit()-amount);userMapper.updateById(user);// 记录流水CreditLoglognewCreditLog();log.setUserId(userId);log.setAmount(-amount);log.setType(generate);creditLogMapper.insert(log);returntrue;}}注意事项一定要加事务避免并发问题记录详细的流水方便对账可以加个定时任务清理过期积分四、遇到的坑和解决方案坑1并发问题问题多个用户同时生成图片有时候会出现任务丢失。原因Redis队列操作不是原子的。解决改用Redis的BLPOP命令阻塞式获取保证原子性。// 改进后的代码StringtaskIdredisTemplate.opsForList().leftPop(task:queue,10,TimeUnit.SECONDS);坑2内存溢出问题跑了几天后服务器内存爆了。原因WebSocket连接没有正确关闭导致Session堆积。解决加了心跳检测定时清理无效连接设置连接超时时间限制单个用户的最大连接数Scheduled(fixedRate60000)// 每分钟执行一次publicvoidcleanInvalidSessions(){sessions.forEach((userId,session)-{if(!session.isOpen()){sessions.remove(userId);}});}坑3AI API限流问题调用AI API太频繁被限流了。解决加了令牌桶限流控制每秒最多调用5次超过限制的任务延迟处理ComponentpublicclassRateLimiter{privatefinalSemaphoresemaphorenewSemaphore(5);publicbooleantryAcquire(){returnsemaphore.tryAcquire();}publicvoidrelease(){semaphore.release();}}坑4图片存储成本问题用户生成的图片越来越多存储费用暴涨。解决定期清理30天前的图片压缩图片质量从原图2MB压到500KB用户可以选择下载到本地五、性能优化1. 数据库优化加索引-- 用户表CREATEINDEXidx_emailONuser(email);-- 任务表CREATEINDEXidx_user_statusONtask(user_id,status);CREATEINDEXidx_create_timeONtask(create_time);分页查询// 使用MyBatis Plus的分页PageTaskpagenewPage(pageNum,pageSize);taskMapper.selectPage(page,newQueryWrapperTask().eq(user_id,userId).orderByDesc(create_time));2. Redis缓存缓存用户信息// 先查缓存StringuserJsonredisTemplate.opsForValue().get(user:userId);if(userJson!null){returnJSON.parseObject(userJson,User.class);}// 缓存未命中查数据库UseruseruserMapper.selectById(userId);redisTemplate.opsForValue().set(user:userId,JSON.toJSONString(user),1,TimeUnit.HOURS);3. 接口优化批量查询// 不好的做法循环查询for(Tasktask:tasks){UseruseruserMapper.selectById(task.getUserId());task.setUser(user);}// 好的做法批量查询ListLonguserIdstasks.stream().map(Task::getUserId).collect(Collectors.toList());ListUserusersuserMapper.selectBatchIds(userIds);MapLong,UseruserMapusers.stream().collect(Collectors.toMap(User::getId,u-u));tasks.forEach(task-task.setUser(userMap.get(task.getUserId())));六、部署上线Docker部署DockerfileFROM openjdk:17-jdk-slim WORKDIR /app COPY target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]docker-compose.ymlversion:3services:app:build:.ports:-8080:8080environment:-SPRING_PROFILES_ACTIVEproddepends_on:-mysql-redismysql:image:mysql:8.0environment:MYSQL_ROOT_PASSWORD:your_passwordMYSQL_DATABASE:image_genvolumes:-mysql-data:/var/lib/mysqlredis:image:redis:7volumes:-redis-data:/datavolumes:mysql-data:redis-data:一键部署docker-composeup -d七、经验总结技术方面1. 选熟悉的技术栈我用Java是因为我最熟不要为了新技术而用新技术快速上线比技术炫酷更重要2. 异步处理很重要耗时操作一定要异步队列是个好东西WebSocket提升体验3. 做好监控和日志我用的ELK收集日志关键指标要监控出问题能快速定位产品方面1. 功能要简单我只做了核心功能不要一开始就做大而全先验证需求再扩展2. 用户体验第一实时反馈很重要加载动画不能少错误提示要友好3. 控制成本AI API很贵要控制调用积分系统是必须的定期清理无用数据