副业实战:一个Java程序员如何用一天时间做出赚钱的AI网站
本文分享了一个使用Java开发的AI图片生成网站开发经验。作者选择Java而非主流Python的原因包括:熟悉Java语言、Spring Boot生态完善、部署便捷以及性能优势。技术栈采用Spring Boot 3.2+JDK 21后端,PostgreSQL数据库,Redis缓存,AWS S3存储,前端使用Thymeleaf+Alpine.js。开发过程仅用一天时间完成,上午搭建项目框架(初始化配
·
最近用一天时间做了个AI图片生成网站,没想到一个月后流量不错。今天跟大家分享一下整个开发过程。
为什么选Java?大家都在用Python啊
是的,我知道AI领域Python是主流,但是:
- 我Java最熟,用熟悉的工具效率最高
- Spring Boot生态完善,啥都有现成的
- 部署方便,一个jar包搞定
- 性能够用,并发处理比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应用完全可行,关键是:
- 别纠结技术栈,用你最熟的
- 快速上线,边做边改
- 关注用户需求,不是技术炫技
- 数据驱动,看数据做决策
更多推荐



所有评论(0)