最近用一天时间做了个AI图片生成网站,没想到一个月后流量不错。今天跟大家分享一下整个开发过程。

为什么选Java?大家都在用Python啊

是的,我知道AI领域Python是主流,但是:

  1. 我Java最熟,用熟悉的工具效率最高
  2. Spring Boot生态完善,啥都有现成的
  3. 部署方便,一个jar包搞定
  4. 性能够用,并发处理比Python强

核心思路:Java做Web服务,调用第三方AI API生成图片,不需要自己训练模型。


在这里插入图片描述

技术栈选择(都是最简单的)

后端:Spring Boot 3.2 + JDK 21
数据库:PostgreSQL(存用户和订单)
缓存:Redis(存图片生成队列)
存储:AWS S3(存生成的图片)
AI API:nona bananan pro
前端:Thymeleaf + Alpine.js(是的,没用React)

为什么这么选?

  • Spring Boot:开箱即用,不用折腾配置
  • PostgreSQL:免费,性能够用
  • Thymeleaf:服务端渲染,SEO友好(重要!)
  • Alpine.js:轻量级,学习成本低

一天开发时间表(真实记录)

上午 9:00 - 12:00:搭架子(3小时)

1. 初始化项目(30分钟)
# 用 Spring Initializr 生成项目
curl https://start.spring.io/starter.zip \
  -d dependencies=web,data-jpa,redis,thymeleaf,security \
  -d type=maven-project \
  -d javaVersion=21 \
  -o nano-banana.zip

unzip nano-banana.zip
cd nano-banana
2. 配置文件(30分钟)
# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/nanobana
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  
  redis:
    host: localhost
    port: 6379
  
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

# 自定义配置
ai:
  api-key: ${STABILITY_API_KEY}
  api-url: https://api.stability.ai/v1/generation

aws:
  s3:
    bucket: nano-banana-images
    region: us-east-1
3. 核心实体类(1小时)
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String email;
    private String passwordHash;
    private Integer credits = 2400; // 初始积分
    
    @CreationTimestamp
    private LocalDateTime createdAt;
}

@Entity
@Table(name = "images")
public class GeneratedImage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne
    private User user;
    
    private String prompt; // 用户输入的提示词
    private String imageUrl; // S3存储地址
    private String status; // pending, completed, failed
    
    @CreationTimestamp
    private LocalDateTime createdAt;
}

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne
    private User user;
    
    private BigDecimal amount; // 4.99
    private Integer credits; // 2400
    private String status; // paid, pending
    
    @CreationTimestamp
    private LocalDateTime createdAt;
}
4. Repository层(30分钟)
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

public interface ImageRepository extends JpaRepository<GeneratedImage, Long> {
    List<GeneratedImage> findByUserOrderByCreatedAtDesc(User user);
    
    @Query("SELECT COUNT(i) FROM GeneratedImage i WHERE i.createdAt > :date")
    Long countGeneratedToday(@Param("date") LocalDateTime date);
}

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByUserAndStatus(User user, String status);
}

下午 13:00 - 18:00:核心功能(5小时)

5. AI图片生成服务(2小时)

这是核心中的核心!

@Service
@Slf4j
public class ImageGenerationService {
    
    @Value("${ai.api-key}")
    private String apiKey;
    
    @Value("${ai.api-url}")
    private String apiUrl;
    
    @Autowired
    private RestTemplate restTemplate;
    
    @Autowired
    private S3Service s3Service;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 生成图片(异步)
     */
    public CompletableFuture<String> generateImage(String prompt, User user) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                // 1. 调用AI API
                HttpHeaders headers = new HttpHeaders();
                headers.set("Authorization", "Bearer " + apiKey);
                headers.setContentType(MediaType.APPLICATION_JSON);
                
                Map<String, Object> request = Map.of(
                    "text_prompts", List.of(Map.of("text", prompt)),
                    "cfg_scale", 7,
                    "height", 1024,
                    "width", 1024,
                    "samples", 1,
                    "steps", 30
                );
                
                HttpEntity<Map<String, Object>> entity = 
                    new HttpEntity<>(request, headers);
                
                ResponseEntity<Map> response = restTemplate.postForEntity(
                    apiUrl + "/text-to-image",
                    entity,
                    Map.class
                );
                
                // 2. 获取生成的图片(base64)
                List<Map<String, String>> artifacts = 
                    (List<Map<String, String>>) response.getBody().get("artifacts");
                String base64Image = artifacts.get(0).get("base64");
                
                // 3. 上传到S3
                byte[] imageBytes = Base64.getDecoder().decode(base64Image);
                String fileName = UUID.randomUUID().toString() + ".png";
                String imageUrl = s3Service.uploadImage(fileName, imageBytes);
                
                // 4. 扣除积分
                user.setCredits(user.getCredits() - 4);
                userRepository.save(user);
                
                log.info("图片生成成功: {}", imageUrl);
                return imageUrl;
                
            } catch (Exception e) {
                log.error("图片生成失败", e);
                throw new RuntimeException("生成失败,请重试");
            }
        });
    }
    
    /**
     * 检查用户积分
     */
    public boolean hasEnoughCredits(User user) {
        return user.getCredits() >= 4;
    }
}
6. S3存储服务(1小时)
@Service
public class S3Service {
    
    @Autowired
    private AmazonS3 s3Client;
    
    @Value("${aws.s3.bucket}")
    private String bucketName;
    
    public String uploadImage(String fileName, byte[] imageData) {
        try {
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentLength(imageData.length);
            metadata.setContentType("image/png");
            
            InputStream inputStream = new ByteArrayInputStream(imageData);
            
            s3Client.putObject(
                bucketName,
                fileName,
                inputStream,
                metadata
            );
            
            // 返回公开访问URL
            return s3Client.getUrl(bucketName, fileName).toString();
            
        } catch (Exception e) {
            throw new RuntimeException("上传失败", e);
        }
    }
}
7. 支付服务(Stripe集成)(1小时)
@Service
public class PaymentService {
    
    @Value("${stripe.api-key}")
    private String stripeApiKey;
    
    @Autowired
    private OrderRepository orderRepository;
    
    /**
     * 创建支付会话
     */
    public String createCheckoutSession(User user) throws StripeException {
        Stripe.apiKey = stripeApiKey;
        
        SessionCreateParams params = SessionCreateParams.builder()
            .setMode(SessionCreateParams.Mode.PAYMENT)
            .setSuccessUrl("https://yourdomain.com/payment/success")
            .setCancelUrl("https://yourdomain.com/payment/cancel")
            .addLineItem(
                SessionCreateParams.LineItem.builder()
                    .setPriceData(
                        SessionCreateParams.LineItem.PriceData.builder()
                            .setCurrency("usd")
                            .setUnitAmount(499L) // $4.99
                            .setProductData(
                                SessionCreateParams.LineItem.PriceData.ProductData.builder()
                                    .setName("2400 Credits")
                                    .build()
                            )
                            .build()
                    )
                    .setQuantity(1L)
                    .build()
            )
            .putMetadata("userId", user.getId().toString())
            .build();
        
        Session session = Session.create(params);
        
        // 创建订单记录
        Order order = new Order();
        order.setUser(user);
        order.setAmount(new BigDecimal("4.99"));
        order.setCredits(2400);
        order.setStatus("pending");
        orderRepository.save(order);
        
        return session.getUrl();
    }
    
    /**
     * 处理支付回调
     */
    public void handleWebhook(String payload, String sigHeader) {
        // Stripe webhook验证和处理
        // 支付成功后给用户加积分
    }
}
8. Controller层(1小时)
@Controller
@RequestMapping("/")
public class HomeController {
    
    @Autowired
    private ImageGenerationService imageService;
    
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 首页
     */
    @GetMapping
    public String home(Model model, @AuthenticationPrincipal UserDetails userDetails) {
        if (userDetails != null) {
            User user = userRepository.findByEmail(userDetails.getUsername())
                .orElseThrow();
            model.addAttribute("credits", user.getCredits());
        }
        return "index";
    }
    
    /**
     * 生成图片
     */
    @PostMapping("/generate")
    @ResponseBody
    public ResponseEntity<?> generateImage(
        @RequestParam String prompt,
        @AuthenticationPrincipal UserDetails userDetails
    ) {
        User user = userRepository.findByEmail(userDetails.getUsername())
            .orElseThrow();
        
        // 检查积分
        if (!imageService.hasEnoughCredits(user)) {
            return ResponseEntity.badRequest()
                .body(Map.of("error", "积分不足,请充值"));
        }
        
        // 异步生成
        CompletableFuture<String> future = imageService.generateImage(prompt, user);
        
        return ResponseEntity.ok(Map.of(
            "status", "processing",
            "message", "图片生成中,请稍候..."
        ));
    }
    
    /**
     * 我的图片
     */
    @GetMapping("/my-images")
    public String myImages(Model model, @AuthenticationPrincipal UserDetails userDetails) {
        User user = userRepository.findByEmail(userDetails.getUsername())
            .orElseThrow();
        
        List<GeneratedImage> images = imageRepository
            .findByUserOrderByCreatedAtDesc(user);
        
        model.addAttribute("images", images);
        return "my-images";
    }
}

晚上 19:00 - 22:00:前端和部署(3小时)

9. 前端页面(Thymeleaf)(1.5小时)
<!-- index.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Nano Banana Pro - AI Image Generator</title>
    <meta name="description" content="Generate stunning images with AI in seconds">
    <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
    <div x-data="imageGenerator()" class="container mx-auto px-4 py-8">
        <!-- Header -->
        <header class="mb-8">
            <h1 class="text-4xl font-bold">Nano Banana Pro</h1>
            <p class="text-gray-600">AI-Powered Image Generation</p>
            <div class="mt-4">
                <span class="text-lg">Credits: <strong th:text="${credits}">0</strong></span>
                <a href="/pricing" class="ml-4 text-blue-600">Buy More</a>
            </div>
        </header>
        
        <!-- Generation Form -->
        <div class="max-w-2xl mx-auto">
            <textarea 
                x-model="prompt"
                placeholder="Describe your image..."
                class="w-full p-4 border rounded-lg"
                rows="4"
            ></textarea>
            
            <button 
                @click="generate()"
                :disabled="loading"
                class="mt-4 px-6 py-3 bg-blue-600 text-white rounded-lg"
            >
                <span x-show="!loading">Generate Image</span>
                <span x-show="loading">Generating...</span>
            </button>
            
            <!-- Result -->
            <div x-show="imageUrl" class="mt-8">
                <img :src="imageUrl" class="w-full rounded-lg shadow-lg">
                <button @click="download()" class="mt-4 px-4 py-2 bg-green-600 text-white rounded">
                    Download
                </button>
            </div>
        </div>
    </div>
    
    <script>
        function imageGenerator() {
            return {
                prompt: '',
                loading: false,
                imageUrl: null,
                
                async generate() {
                    if (!this.prompt) return;
                    
                    this.loading = true;
                    
                    try {
                        const response = await fetch('/generate', {
                            method: 'POST',
                            headers: {
                                'Content-Type': 'application/x-www-form-urlencoded',
                            },
                            body: `prompt=${encodeURIComponent(this.prompt)}`
                        });
                        
                        const data = await response.json();
                        
                        if (data.error) {
                            alert(data.error);
                            return;
                        }
                        
                        // 轮询检查生成状态
                        this.pollStatus();
                        
                    } catch (error) {
                        alert('Generation failed');
                    } finally {
                        this.loading = false;
                    }
                },
                
                async pollStatus() {
                    // 每2秒检查一次
                    const interval = setInterval(async () => {
                        const response = await fetch('/status');
                        const data = await response.json();
                        
                        if (data.status === 'completed') {
                            this.imageUrl = data.imageUrl;
                            clearInterval(interval);
                        }
                    }, 2000);
                },
                
                download() {
                    window.open(this.imageUrl, '_blank');
                }
            }
        }
    </script>
</body>
</html>
10. Docker部署(1小时)
# Dockerfile
FROM eclipse-temurin:21-jdk-alpine

WORKDIR /app

COPY target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - STABILITY_API_KEY=${STABILITY_API_KEY}
      - STRIPE_API_KEY=${STRIPE_API_KEY}
    depends_on:
      - db
      - redis
  
  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=nanobana
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
  
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:
11. 部署到AWS(30分钟)
# 打包
./mvnw clean package -DskipTests

# 构建Docker镜像
docker build -t nano-banana:latest .

# 推送到ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin xxx.dkr.ecr.us-east-1.amazonaws.com
docker tag nano-banana:latest xxx.dkr.ecr.us-east-1.amazonaws.com/nano-banana:latest
docker push xxx.dkr.ecr.us-east-1.amazonaws.com/nano-banana:latest

# 在EC2上运行
ssh ec2-user@your-server
docker-compose up -d

遇到的坑和解决方案

坑1:AI API超时

问题:Stability AI生成图片需要30-60秒,HTTP请求超时。

解决

// 改成异步 + WebSocket推送结果
@Service
public class ImageGenerationService {
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    public void generateImageAsync(String prompt, User user) {
        CompletableFuture.runAsync(() -> {
            String imageUrl = callAIAPI(prompt);
            
            // 通过WebSocket推送给前端
            messagingTemplate.convertAndSendToUser(
                user.getEmail(),
                "/queue/images",
                Map.of("imageUrl", imageUrl)
            );
        });
    }
}

坑2:S3上传慢

问题:图片上传到S3要5-10秒。

解决

// 使用S3 Transfer Manager加速
@Bean
public TransferManager transferManager(AmazonS3 s3Client) {
    return TransferManagerBuilder.standard()
        .withS3Client(s3Client)
        .withMultipartUploadThreshold(5 * 1024 * 1024L) // 5MB
        .withMinimumUploadPartSize(5 * 1024 * 1024L)
        .build();
}

坑3:并发问题

问题:多个用户同时生成图片,积分扣除出现负数。

解决

// 使用Redis分布式锁
@Service
public class CreditService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    public boolean deductCredits(User user, int amount) {
        RLock lock = redissonClient.getLock("user:credits:" + user.getId());
        
        try {
            lock.lock(5, TimeUnit.SECONDS);
            
            if (user.getCredits() < amount) {
                return false;
            }
            
            user.setCredits(user.getCredits() - amount);
            userRepository.save(user);
            
            return true;
            
        } finally {
            lock.unlock();
        }
    }
}

性能优化

1. 数据库索引

CREATE INDEX idx_images_user_created ON images(user_id, created_at DESC);
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

2. Redis缓存

@Cacheable(value = "user", key = "#email")
public User findByEmail(String email) {
    return userRepository.findByEmail(email).orElseThrow();
}

@CacheEvict(value = "user", key = "#user.email")
public void updateUser(User user) {
    userRepository.save(user);
}

3. 图片CDN

// 使用CloudFront加速S3图片访问
@Value("${cloudfront.domain}")
private String cdnDomain;

public String getImageUrl(String s3Key) {
    return "https://" + cdnDomain + "/" + s3Key;
}

运营数据(一个月后)

月访问量:18,710
注册用户:1,240
付费用户:89 (7.2% 转化率)
月收入:$444 (89 × $4.99)
服务器成本:$50/月
净利润:$394/月

关键指标

  • 用户留存率(7天):45%
  • 平均每用户生成图片:12张
  • 付费转化周期:平均3天

给独立开发者的建议

1. 不要重复造轮子

  • 用现成的AI API,别自己训练模型
  • 用成熟的支付方案(Stripe),别自己搞
  • 用云服务(AWS/Vercel),别自己运维

2. MVP先行

我第一版只有3个功能:

  • 生成图片
  • 购买积分
  • 查看历史

其他功能都是后来加的。

3. SEO从第一天开始

  • 服务端渲染(Thymeleaf)
  • 语义化HTML
  • 合理的URL结构
  • sitemap.xml

4. 监控很重要

// 加上监控
@Slf4j
@Aspect
@Component
public class PerformanceMonitor {
    
    @Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        try {
            return joinPoint.proceed();
        } finally {
            long duration = System.currentTimeMillis() - start;
            log.info("Method {} took {}ms", 
                joinPoint.getSignature().getName(), duration);
        }
    }
}

总结

用Java做AI应用完全可行,关键是:

  1. 别纠结技术栈,用你最熟的
  2. 快速上线,边做边改
  3. 关注用户需求,不是技术炫技
  4. 数据驱动,看数据做决策
Logo

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

更多推荐