计划
第 1 周:基础架构 + 微服务搭建
学习目标: 项目脚手架、Nacos、数据库设计、基础 CRUD
核心任务:
1. 创建 Maven 多模块项目
2. 集成 Spring Cloud 框架
3. 设计核心数据库表(用户、创意、拍卖、交易等)
4. 实现用户服务和认证模块
5. 所有微服务注册到 Nacos
6. 配置 API Gateway
交付物:
- ✅ 完整的多模块项目结构
- ✅ 用户注册/登录/认证功能
- ✅ 所有服务能通过 Nacos 发现
第 2 周:创意服务 + 缓存优化
学习目标: Redis 多级缓存、布隆过滤器、ElasticSearch 搜索
核心任务:
1. 实现创意作品的 CRUD(发布、编辑、删除)
2. 集成 Redis + Caffeine 多级缓存
- 热门创意缓存
- 搜索结果缓存
3. 布隆过滤器防止缓存穿透
4. ElasticSearch 全文搜索集成
5. 缓存预热任务
重点代码示例:
// 创意详情查询 - 三层缓存防护
public Artwork getArtworkDetail(Long artworkId) {
// 第1层:本地缓存(Caffeine)
Artwork cached = localCache.getIfPresent(artworkId);
if (cached != null) {
return cached;
}
// 第2层:布隆过滤器检查数据是否存在
if (!bloomFilter.contains(artworkId)) {
// 数据不存在,直接返回 null
return null;
}
// 第3层:Redis 分布式缓存
String key = "artwork:" + artworkId;
Artwork redisData = redisTemplate.opsForValue()
.get(key);
if (redisData != null) {
localCache.put(artworkId, redisData);
return redisData;
}
// 都没有 → 查数据库(加分布式锁防击穿)
RLock lock = redisson.getLock("artwork:lock:" + artworkId);
try {
if (!lock.tryLock(2, 10, TimeUnit.SECONDS)) {
// 加锁失败,等待后重试
Thread.sleep(50);
return getArtworkDetail(artworkId);
}
Artwork dbData = artworkMapper.selectById(artworkId);
if (dbData != null) {
redisTemplate.opsForValue()
.set(key, dbData, Duration.ofHours(1));
localCache.put(artworkId, dbData);
}
return dbData;
} finally {
lock.unlock();
}
}
交付物:
- ✅ 创意作品完整的 CRUD
- ✅ 缓存系统防穿透、击穿、雪崩
- ✅ 搜索功能可用
第 3 周:消息队列 + 异步处理
学习目标: RabbitMQ、死信队列、消息可靠性、幂等性
核心任务:
1. 设计 MQ 消息体(事件驱动架构)
- 创意发布事件
- 拍卖结算事件
- 交易完成事件
2. 实现消息发送者和消费者
3. 配置死信队列处理异常
4. 实现幂等性处理(重复消息)
5. 消息可靠性保证(确认机制)
核心事件设计:
// 事件类型定义
enum EventType {
ARTWORK_PUBLISHED, // 创意发布
ARTWORK_REPORTED, // 创意举报
AUCTION_STARTED, // 拍卖开始
BID_PLACED, // 出价
AUCTION_ENDED, // 拍卖结束
TRANSACTION_COMPLETED, // 交易完成
PAYMENT_SUCCESS, // 支付成功
REVENUE_SETTLED, // 收益结算
BLINDBOX_OPENED // 盲盒打开
}
// 事件消息
@Data
public class DomainEvent {
private String eventId; // 幂等性 key
private EventType eventType;
private Long timestamp;
private Map<String, Object> payload;
private String source; // 来源服务
}
交付物:
- ✅ MQ 系统完整接入
- ✅ 创意发布 → 通知用户 的异步流程
- ✅ 消息可靠性测试通过
第 4 周:拍卖核心逻辑 + 实时竞价
学习目标: WebSocket、Sentinel 限流、复杂业务逻辑、Redis Lua 脚本
核心任务:
1. 设计拍卖数据模型和状态机
2. 实现三种拍卖策略
- 英式拍卖(逐步递增)
- 荷兰式拍卖(逐步递减)
- 密封竞价(一次性出价)
3. WebSocket 实时推送竞价信息
4. Sentinel 防止恶意出价(限流)
5. Redis Lua 脚本实现原子出价操作
6. 竞价锁防止并发冲突
Lua 脚本示例:
-- bid_lock.lua
-- 原子操作:检查出价有效性 + 记录出价 + 更新最高价
local auctionId = KEYS[1]
local userId = KEYS[2]
local bidAmount = ARGV[1]
local currentMaxBid = redis.call('GET', 'auction:' .. auctionId .. ':max_bid')
-- 检查出价是否有效
if tonumber(bidAmount) <= tonumber(currentMaxBid or 0) then
return {0, "出价必须高于当前最高价"}
end
-- 检查用户余额(从钱包服务缓存)
local userBalance = redis.call('GET', 'wallet:' .. userId .. ':balance')
if tonumber(userBalance or 0) < tonumber(bidAmount) then
return {0, "余额不足"}
end
-- 原子更新最高价和最高出价者
redis.call('SET', 'auction:' .. auctionId .. ':max_bid', bidAmount)
redis.call('SET', 'auction:' .. auctionId .. ':max_bidder', userId)
redis.call('LPUSH', 'auction:' .. auctionId .. ':bid_history',
userId .. ':' .. bidAmount .. ':' .. redis.call('TIME')[1])
-- 发布出价事件到 pub/sub(WebSocket 订阅)
redis.call('PUBLISH', 'auction:' .. auctionId .. ':bids',
cjson.encode({userId, bidAmount, redis.call('TIME')[1]}))
return {1, "出价成功"}
WebSocket 推送示例:
@ServerEndpoint("/ws/auction/{auctionId}")
public class AuctionWebSocketServer {
private static Set<Session> sessions = new CopyOnWriteArraySet<>();
private RedisTemplate<String, Object> redisTemplate;
@OnOpen
public void onOpen(@PathParam("auctionId") Long auctionId,
Session session) {
sessions.add(session);
// 订阅 Redis Pub/Sub 频道
subscribeToAuctionChannel(auctionId);
}
private void subscribeToAuctionChannel(Long auctionId) {
String channel = "auction:" + auctionId + ":bids";
redisTemplate.getConnectionFactory()
.getConnection()
.subscribe((message, pattern) -> {
// 实时广播给所有连接的客户端
String bidInfo = new String(message.getBody());
sessions.forEach(session -> {
try {
session.getBasicRemote()
.sendText(bidInfo);
} catch (IOException e) {
e.printStackTrace();
}
});
}, channel.getBytes());
}
}
交付物:
- ✅ 拍卖系统完整可用
- ✅ WebSocket 实时竞价
- ✅ 支持多种拍卖策略
- ✅ 并发安全性测试通过(万级并发)
第 5 周:盲盒 + 聚合拍卖 + 订单系统
学习目标: 复杂业务逻辑、抽奖算法、组合交易
核心任务:
1. 盲盒系统
- 盲盒创建和库存管理
- 奖品分布策略(概率分层)
- 开箱动画数据和结果
- 防止重复开箱(幂等性)
2. 聚合拍卖(组合购买)
- 选择多个创意组合
- 一次竞价多个作品
- 批量结算
3. 订单系统
- 订单创建、支付、确认
- 订单查询和历史记录
- 订单取消和退款
盲盒抽奖算法:
@Service
public class LotteryService {
private static final Random random = new Random();
// 加权随机抽奖
public Prize drawPrize(BlindBox blindBox) {
List<Prize> prizes = blindBox.getPrizes();
// 计算权重总和
int totalWeight = prizes.stream()
.mapToInt(Prize::getWeight)
.sum();
// 随机选择
int randomValue = random.nextInt(totalWeight);
int currentWeight = 0;
for (Prize prize : prizes) {
currentWeight += prize.getWeight();
if (randomValue < currentWeight) {
return prize;
}
}
return prizes.get(0); // 默认返回
}
// 开箱逻辑(包含防重复操作)
@Transactional
public BoxOpenResult openBox(String userId, Long boxId) {
// Redis 中记录用户是否已开过这个盒子
String openKey = "blindbox:opened:" + userId + ":" + boxId;
// 使用 Lua 脚本保证原子性
Boolean alreadyOpened = redisTemplate.execute(
new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) then " +
" return 1 " +
"else " +
" redis.call('set', KEYS[1], '1') " +
" redis.call('expire', KEYS[1], 86400) " +
" return 0 " +
"end",
Boolean.class
),
Collections.singletonList(openKey)
);
if (alreadyOpened) {
throw new BusinessException("已经开过这个盲盒了");
}
// 真正的抽奖逻辑
BlindBox box = blindBoxService.getById(boxId);
Prize prize = drawPrize(box);
// 发送 MQ 事件
eventPublisher.publish(new BoxOpenedEvent(
userId, boxId, prize.getId()
));
return BoxOpenResult.builder()
.prizeId(prize.getId())
.prizeName(prize.getName())
.prizeImage(prize.getImage())
.rarity(prize.getRarity()) // 稀有度
.animationDelay(800) // 动画延迟(毫秒)
.build();
}
}
聚合拍卖逻辑:
@Service
public class CombinedAuctionService {
// 创建组合拍卖
@Transactional
public CombinedAuction createCombinedAuction(
List<Long> artworkIds,
BigDecimal startPrice) {
// 验证所有作品都存在
List<Artwork> artworks = artworkService.listByIds(artworkIds);
if (artworks.size() != artworkIds.size()) {
throw new BusinessException("部分作品不存在");
}
// 创建组合拍卖记录
CombinedAuction auction = new CombinedAuction();
auction.setArtworks(artworks);
auction.setStartPrice(startPrice);
auction.setStatus(AuctionStatus.PENDING);
auction.setCreatedTime(LocalDateTime.now());
combinedAuctionMapper.insert(auction);
// 发送事件
eventPublisher.publish(new AuctionCreatedEvent(auction.getId()));
return auction;
}
// 组合拍卖出价
@Transactional
public void bidOnCombination(Long auctionId,
Long userId,
BigDecimal bidAmount) {
CombinedAuction auction = combinedAuctionMapper
.selectById(auctionId);
// 检查出价金额
if (bidAmount.compareTo(auction.getCurrentPrice()) <= 0) {
throw new BusinessException("出价必须高于当前价格");
}
// 原子操作:更新价格 + 记录出价
// 使用 Lua 脚本
String lua = "redis.call('set', KEYS[1], ARGV[1]) " +
"redis.call('set', KEYS[2], ARGV[2]) " +
"return 1";
redisTemplate.execute(
new DefaultRedisScript<>(lua, Long.class),
Arrays.asList(
"auction:price:" + auctionId,
"auction:bidder:" + auctionId
),
bidAmount.toPlainString(),
userId.toString()
);
// 发布竞价事件
eventPublisher.publish(
new CombinedBidPlacedEvent(auctionId, userId, bidAmount)
);
}
}
订单系统:
@Service
public class OrderService {
// 创建订单(可能来自多种场景:拍卖结束、盲盒、直接购买)
@Transactional
public Order createOrder(OrderCreateRequest request) {
Order order = new Order();
order.setUserId(request.getUserId());
order.setTotalAmount(request.getTotalAmount());
order.setStatus(OrderStatus.CREATED);
order.setItems(request.getItems());
order.setCreatedTime(LocalDateTime.now());
orderMapper.insert(order);
// 发送订单创建事件
eventPublisher.publish(new OrderCreatedEvent(order.getId()));
return order;
}
// 订单支付(通过钱包扣费)
@Transactional
public void payOrder(Long orderId) {
Order order = orderMapper.selectById(orderId);
// 调用钱包服务扣费(Feign RPC)
walletServiceClient.deductBalance(
order.getUserId(),
order.getTotalAmount()
);
// 更新订单状态
order.setStatus(OrderStatus.PAID);
order.setPaidTime(LocalDateTime.now());
orderMapper.updateById(order);
// 发送支付成功事件
eventPublisher.publish(new PaymentSuccessEvent(order.getId()));
}
}
交付物:
- ✅ 盲盒系统完整实现(防重复、概率分层)
- ✅ 聚合拍卖支持
- ✅ 订单系统 CRUD
第 6 周:钱包系统 + 分布式事务
学习目标: Seata 分布式事务、TCC 模式、补偿逻辑
核心任务:
1. 钱包系统
- 用户余额管理
- 充值和提现
- 余额锁定(下单时冻结金额)
2. Seata 分布式事务
- AT 模式:自动回滚
- TCC 模式:业务补偿
- SAGA 模式:长事务编排
3. 交易结算
- 创作者收益分成
- 平台抽成
- 税费计算
钱包服务实现:
@Service
@Slf4j
public class WalletService {
// 用户余额扣费(使用 Redis 实现)
public void deductBalance(Long userId, BigDecimal amount) {
String balanceKey = "wallet:balance:" + userId;
String lockedKey = "wallet:locked:" + userId;
// 使用 Lua 脚本保证原子性
String luaScript =
"local balance = tonumber(redis.call('GET', KEYS[1])) or 0 " +
"local amount = tonumber(ARGV[1]) " +
"if balance >= amount then " +
" redis.call('DECRBY', KEYS[1], amount) " +
" return 1 " +
"else " +
" return 0 " +
"end";
Boolean result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Boolean.class),
Collections.singletonList(balanceKey),
amount.toPlainString()
);
if (!result) {
throw new InsufficientBalanceException("余额不足");
}
}
// 余额锁定(下单时冻结)
public void lockBalance(Long userId, BigDecimal amount) {
String lockedKey = "wallet:locked:" + userId;
redisTemplate.opsForValue()
.increment(lockedKey, amount.doubleValue());
}
// 释放锁定的余额
public void unlockBalance(Long userId, BigDecimal amount) {
String lockedKey = "wallet:locked:" + userId;
redisTemplate.opsForValue()
.decrement(lockedKey, amount.doubleValue());
}
}
Seata 分布式事务示例(TCC 模式):
// 业务接口
public interface OrderTransactionService {
void createOrder(Long userId, Long artworkId, BigDecimal price);
}
// 实现类(带 Seata 注解)
@Service
@Slf4j
public class OrderTransactionServiceImpl
implements OrderTransactionService {
@Autowired
private WalletServiceClient walletServiceClient;
@Autowired
private ArtworkServiceClient artworkServiceClient;
@Autowired
private OrderMapper orderMapper;
// 全局事务开始
@GlobalTransactional(name = "create-order-tx")
@Override
public void createOrder(Long userId, Long artworkId,
BigDecimal price) {
log.info("开始创建订单:userId={}, artworkId={}",
userId, artworkId);
// Step 1: Try - 预留钱包余额
walletServiceClient.lockBalance(userId, price);
// Step 2: 创建订单记录
Order order = new Order();
order.setUserId(userId);
order.setArtworkId(artworkId);
order.setPrice(price);
order.setStatus(OrderStatus.LOCKED);
orderMapper.insert(order);
// Step 3: 更新创意所有者的未结算收入
artworkServiceClient.addUnsettledRevenue(
artworkId, price
);
// 若任何一步失败,Seata 会自动回滚所有操作
log.info("订单创建成功:orderId={}", order.getId());
}
}
// 钱包服务(Seata 参与方)
@FeignClient(name = "wallet-service")
public interface WalletServiceClient {
@PostMapping("/wallet/lock-balance")
void lockBalance(@RequestParam Long userId,
@RequestParam BigDecimal amount);
// Seata 回滚方法
@PostMapping("/wallet/lock-balance-cancel")
void lockBalanceCancel(@RequestParam Long userId,
@RequestParam BigDecimal amount);
}
// 实际钱包服务实现
@Service
public class WalletServiceImpl implements WalletService {
@TwoPhaseBusinessAction(
name = "lockBalance",
commitMethod = "commit",
rollbackMethod = "rollback"
)
public void lockBalance(String xid, Long branchId,
Long userId, BigDecimal amount) {
// Try 阶段:锁定余额
walletRepository.lockBalance(userId, amount);
log.info("锁定余额成功:xid={}, userId={}, amount={}",
xid, userId, amount);
}
// Commit 阶段:确认(无需操作,资金已锁定)
public boolean commit(BusinessActionContext context) {
return true;
}
// Rollback 阶段:回滚(释放锁定的余额)
public boolean rollback(BusinessActionContext context) {
Long userId = (Long) context.getActionContext("userId");
BigDecimal amount =
(BigDecimal) context.getActionContext("amount");
walletRepository.unlockBalance(userId, amount);
log.warn("回滚锁定余额:userId={}, amount={}",
userId, amount);
return true;
}
}
收益结算逻辑:
@Service
@Slf4j
public class RevenueSettlementService {
// 结算流程:创作者收益 = 交易金额 * (1 - 平台抽成)
@Transactional
public void settleRevenue(Long artworkId) {
Artwork artwork = artworkMapper.selectById(artworkId);
BigDecimal unsettledRevenue = artwork.getUnsettledRevenue();
if (unsettledRevenue.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
// 计算创作者收益
BigDecimal platformCommission = unsettledRevenue
.multiply(PLATFORM_COMMISSION_RATE); // 比如 10%
BigDecimal creatorRevenue = unsettledRevenue
.subtract(platformCommission);
// 开始分布式事务:结算
settleToCreator(artwork.getCreatorId(), creatorRevenue);
settleToPlatform(platformCommission);
// 更新创意的收益统计
artwork.setSettledRevenue(
artwork.getSettledRevenue().add(creatorRevenue)
);
artwork.setUnsettledRevenue(BigDecimal.ZERO);
artworkMapper.updateById(artwork);
log.info("结算完成:artworkId={}, creatorRevenue={}, " +
"platformCommission={}", artworkId, creatorRevenue,
platformCommission);
}
private void settleToCreator(Long creatorId,
BigDecimal amount) {
// 发送 MQ 消息:创作者提现
eventPublisher.publish(
new CreatorRevenueReadyEvent(creatorId, amount)
);
}
private void settleToPlatform(BigDecimal commission) {
// 更新平台账户
platformAccountRepository.addCommission(commission);
}
}
交付物:
- ✅ 钱包系统完整(充值、提现、锁定)
- ✅ Seata 分布式事务集成
- ✅ 结算流程验证通过
第 7 周:投票系统 + 定时任务
学习目标: XXL-Job、复杂计算任务、投票算法
核心任务:
1. 投票系统
- 创意投票
- 权重计算(粉丝数、历史评分等)
- 防止刷票
2. XXL-Job 定时任务
- 拍卖自动结算
- 热门创意排行榜更新
- 年度最佳计算
- 数据报表生成
- 过期数据清理
投票系统实现:
@Service
@Slf4j
public class VoteService {
private static final int VOTES_PER_USER = 5; // 每用户每天5票
// 用户投票
@Transactional
public void vote(Long userId, Long artworkId) {
// 检查用户今天是否还有投票次数
String voteKey = "vote:daily:" + userId + ":" +
LocalDate.now();
Long votesUsed = redisTemplate.opsForValue()
.increment(voteKey, 1);
if (votesUsed > VOTES_PER_USER) {
throw new VoteLimitException("每天最多投5票");
}
// 设置过期时间(明天自动清除)
if (votesUsed == 1) {
redisTemplate.expire(voteKey,
Duration.ofDays(1));
}
// 检查用户是否已投过这个作品
String userVoteKey = "vote:user:" + userId +
":artwork:" + artworkId;
Boolean alreadyVoted = redisTemplate.hasKey(
userVoteKey
);
if (alreadyVoted) {
throw new BusinessException("已经投过票了");
}
// 记录投票
redisTemplate.opsForValue()
.set(userVoteKey, "1", Duration.ofDays(365));
// 增加作品的投票数(使用权重)
BigDecimal userWeight = calculateUserWeight(userId);
String artworkVoteKey = "vote:artwork:" + artworkId;
redisTemplate.opsForZSet()
.incrementScore(artworkVoteKey,
userId.toString(),
userWeight.doubleValue());
log.info("投票成功:userId={}, artworkId={}",
userId, artworkId);
}
// 计算用户投票权重
private BigDecimal calculateUserWeight(Long userId) {
User user = userService.getById(userId);
// 权重 = 基础分 + 粉丝数加成 + 历史评分加成
BigDecimal baseScore = BigDecimal.ONE;
BigDecimal followerBonus = BigDecimal.valueOf(
user.getFollowerCount() / 100.0 // 100个粉丝 +0.01
);
BigDecimal ratingBonus = user.getHistoricalRating()
.multiply(BigDecimal.valueOf(0.5));
return baseScore.add(followerBonus).add(ratingBonus);
}
}
XXL-Job 定时任务配置:
@Configuration
public class XxlJobConfig {
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor xxlJobSpringExecutor =
new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(
"${xxl.job.admin.addresses}"
);
xxlJobSpringExecutor.setAppname(
"${xxl.job.executor.appname}"
);
xxlJobSpringExecutor.setIp(
"${xxl.job.executor.ip}"
);
xxlJobSpringExecutor.setPort(
${xxl.job.executor.port}
);
xxlJobSpringExecutor.setAccessToken(
"${xxl.job.accessToken}"
);
xxlJobSpringExecutor.setLogPath(
"${xxl.job.executor.logpath}"
);
xxlJobSpringExecutor.setLogRetentionDays(
${xxl.job.executor.logretentiondays}
);
return xxlJobSpringExecutor;
}
}
具体任务实现:
@Slf4j
@Component
public class AuctionFinalizerJob {
@Autowired
private AuctionService auctionService;
@Autowired
private RabbitTemplate rabbitTemplate;
// 每小时执行一次
@XxlJob("auctionFinalizerJob")
public void executeJob() {
log.info("拍卖结算任务开始...");
try {
// 查询所有已结束的拍卖
List<Auction> endedAuctions =
auctionService.findEndedAuctions();
for (Auction auction : endedAuctions) {
// 发送 MQ 事件进行异步结算
AuctionSettlementEvent event =
new AuctionSettlementEvent();
event.setAuctionId(auction.getId());
event.setHighestBidder(
auction.getHighestBidder()
);
event.setHighestBid(
auction.getHighestBid()
);
rabbitTemplate.convertAndSend(
"auction-settlement-exchange",
"settlement.key",
event
);
}
log.info("拍卖结算任务完成,处理数量:{}",
endedAuctions.size());
} catch (Exception e) {
log.error("拍卖结算任务异常", e);
throw new RuntimeException(e);
}
}
}
@Slf4j
@Component
public class HotArtworkJob {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ArtworkMapper artworkMapper;
// 每 2 小时执行一次
@XxlJob("hotArtworkJob")
public void executeJob() {
log.info("热门创意排行榜更新开始...");
try {
// 从 Redis 获取最近 24 小时的浏览数据
Set hotArtworks =
redisTemplate.opsForZSet()
.reverseRange("artwork:views:24h", 0, 99);
// 保存到数据库(用于展示)
List<HotArtwork> hotList = new ArrayList<>();
int rank = 1;
for (Object artworkId : hotArtworks) {
HotArtwork hot = new HotArtwork();
hot.setArtworkId(Long.parseLong(
artworkId.toString()
));
hot.setRank(rank++);
hot.setUpdatedTime(LocalDateTime.now());
hotList.add(hot);
}
hotArtworkMapper.deleteAll();
hotArtworkMapper.insertBatch(hotList);
// 更新排行榜缓存
redisTemplate.opsForValue()
.set("hot-artworks", hotList,
Duration.ofHours(2));
log.info("热门创意排行榜更新完成");
} catch (Exception e) {
log.error("热门创意排行榜更新异常", e);
}
}
}
@Slf4j
@Component
public class AnnualAwardCalculationJob {
@Autowired
private VoteService voteService;
@Autowired
private ArtworkService artworkService;
// 每年 1 月 1 日凌晨执行
@XxlJob("annualAwardCalculationJob")
public void executeJob() {
log.info("年度最佳创意计算开始...");
try {
LocalDateTime startOfYear =
LocalDateTime.of(
LocalDate.now().getYear(), 1, 1,
0, 0, 0
);
LocalDateTime endOfYear =
LocalDateTime.now();
// 查询去年发布的所有作品
List<Artwork> artworks =
artworkService.listByCreatedBetween(
startOfYear, endOfYear
);
// 计算综合得分(投票 + 销售 + 热度)
Map<Long, BigDecimal> scoreMap = new HashMap<>();
for (Artwork artwork : artworks) {
BigDecimal voteScore =
voteService.getArtworkVoteScore(
artwork.getId()
);
BigDecimal salesScore =
artwork.getTotalSales()
.divide(BigDecimal.valueOf(1000));
BigDecimal viewScore =
BigDecimal.valueOf(
artwork.getViewCount() / 100.0
);
BigDecimal totalScore = voteScore
.multiply(BigDecimal.valueOf(0.4))
.add(salesScore
.multiply(BigDecimal.valueOf(0.35)))
.add(viewScore
.multiply(BigDecimal.valueOf(0.25)));
scoreMap.put(artwork.getId(), totalScore);
}
// 获取前 10 名
List<Map.Entry<Long, BigDecimal>> topTen =
scoreMap.entrySet().stream()
.sorted((a, b) ->
b.getValue().compareTo(a.getValue())
)
.limit(10)
.collect(Collectors.toList());
// 保存年度最佳
AnnualAward award = new AnnualAward();
award.setYear(LocalDate.now().getYear());
award.setTopArtworks(topTen.stream()
.map(Map.Entry::getKey)
.collect(Collectors.toList()));
award.setCreatedTime(LocalDateTime.now());
annualAwardMapper.insert(award);
log.info("年度最佳创意计算完成:{}", topTen);
} catch (Exception e) {
log.error("年度最佳创意计算异常", e);
}
}
}
交付物:
- ✅ 投票系统完整(防刷票、权重计算)
- ✅ XXL-Job 多个任务实现
- ✅ 排行榜和年度最佳计算
第 8 周:监控 + 上线部署
学习目标: Skywalking 链路追踪、Docker 部署、性能优化
核心任务:
1. Skywalking 链路追踪
- 服务间调用可视化
- 性能瓶颈识别
- 日志关联
2. Docker 容器化部署
- 每个微服务编写 Dockerfile
- 编写 docker-compose.yml
- 环境变量管理
3. 性能优化
- 数据库索引优化
- 缓存热点数据
- 异步处理优化
Dockerfile 示例:
# 多阶段构建
FROM maven:3.8-openjdk-11 as builder
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests
# 最终镜像
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
# Skywalking 代理
ADD https://archive.apache.org/dist/skywalking/8.9.0/apache-skywalking-apm-8.9.0.tar.gz .
RUN tar -xzf apache-skywalking-apm-8.9.0.tar.gz
ENV JAVA_OPTS="-javaagent:/app/skywalking/agent/skywalking-agent.jar" \
SW_AGENT_NAME="canvas-chain-service" \
SW_AGENT_COLLECTOR_BACKEND_SERVICES="skywalking-oap:11800"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
docker-compose.yml 完整编排:
version: '3.8'
services:
# MySQL 数据库
mysql:
image: mysql:8.0
container_name: canvas-mysql
environment:
MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: canvas_chain
ports:
- "3306:3306"
volumes:
- ./docs/init.sql:/docker-entrypoint-initdb.d/init.sql
- mysql-data:/var/lib/mysql
networks:
- canvas-network
# Redis 缓存
redis:
image: redis:7-alpine
container_name: canvas-redis
ports:
- "6379:6379"
command: redis-server --appendonly yes --requirepass canvas123456
volumes:
- redis-data:/data
networks:
- canvas-network
# RabbitMQ 消息队列
rabbitmq:
image: rabbitmq:3.12-management-alpine
container_name: canvas-rabbitmq
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
ports:
- "5672:5672"
- "15672:15672"
volumes:
- rabbitmq-data:/var/lib/rabbitmq
networks:
- canvas-network
# Nacos 服务注册中心
nacos:
image: nacos/nacos-server:v2.2.0
container_name: canvas-nacos
environment:
MODE: standalone
NACOS_AUTH_ENABLE: "false"
ports:
- "8848:8848"
- "9848:9848"
volumes:
- nacos-data:/home/nacos/data
networks:
- canvas-network
# ElasticSearch 搜索引擎
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.5.0
container_name: canvas-es
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- es-data:/usr/share/elasticsearch/data
networks:
- canvas-network
# Kibana ES 可视化
kibana:
image: docker.elastic.co/kibana/kibana:8.5.0
container_name: canvas-kibana
environment:
ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"
ports:
- "5601:5601"
depends_on:
- elasticsearch
networks:
- canvas-network
# Skywalking OAP 服务器(链路追踪)
skywalking-oap:
image: apache/skywalking-oap-server:8.9.0
container_name: canvas-skywalking-oap
environment:
SW_STORAGE: elasticsearch
SW_ELASTICSEARCH_REPLICAS: 0
SW_ELASTICSEARCH_PEERS: "elasticsearch:9200"
ports:
- "11800:11800"
- "12800:12800"
depends_on:
- elasticsearch
networks:
- canvas-network
# Skywalking UI
skywalking-ui:
image: apache/skywalking-ui:8.9.0
container_name: canvas-skywalking-ui
ports:
- "8080:8080"
environment:
SW_OAP_ADDRESS: "skywalking-oap:12800"
depends_on:
- skywalking-oap
networks:
- canvas-network
# Seata Server 分布式事务协调器
seata-server:
image: seataio/seata-server:latest
container_name: canvas-seata-server
environment:
SEATA_PORT: 8091
SEATA_CONFIG_NAME: file:/seata-config/registry.conf
ports:
- "8091:8091"
volumes:
- ./config/seata/registry.conf:/seata-config/registry.conf
- seata-data:/seata-server/sessionStore
networks:
- canvas-network
# XXL-Job 定时任务平台
xxl-job-admin:
image: xuxueli/xxl-job-admin:2.4.0
container_name: canvas-xxl-job-admin
environment:
PARAMS: "--server.servlet.context-path=/xxl-job-admin --spring.datasource.url=jdbc:mysql://mysql:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=root123456"
ports:
- "8081:8080"
depends_on:
- mysql
networks:
- canvas-network
# Sentinel 限流控制台
sentinel-dashboard:
image: bladex/sentinel-dashboard:latest
container_name: canvas-sentinel-dashboard
ports:
- "8858:8858"
networks:
- canvas-network
# 网关服务(基于 Spring Cloud Gateway)
gateway:
build:
context: ./canvas-chain-gateway
dockerfile: Dockerfile
container_name: canvas-gateway
environment:
JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
SW_AGENT_NAME: "canvas-gateway"
SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
NACOS_SERVER_ADDR: "nacos:8848"
REDIS_HOST: "redis"
REDIS_PASSWORD: "canvas123456"
ports:
- "9000:8080"
depends_on:
- nacos
- redis
- skywalking-oap
networks:
- canvas-network
# 用户服务
user-service:
build:
context: ./canvas-chain-user-service
dockerfile: Dockerfile
container_name: canvas-user-service
environment:
JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
SW_AGENT_NAME: "canvas-user-service"
SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
NACOS_SERVER_ADDR: "nacos:8848"
MYSQL_HOST: "mysql"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root123456"
REDIS_HOST: "redis"
REDIS_PASSWORD: "canvas123456"
ports:
- "8001:8080"
depends_on:
- mysql
- redis
- nacos
- skywalking-oap
networks:
- canvas-network
# 创意服务
artwork-service:
build:
context: ./canvas-chain-artwork-service
dockerfile: Dockerfile
container_name: canvas-artwork-service
environment:
JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
SW_AGENT_NAME: "canvas-artwork-service"
SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
NACOS_SERVER_ADDR: "nacos:8848"
MYSQL_HOST: "mysql"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root123456"
REDIS_HOST: "redis"
REDIS_PASSWORD: "canvas123456"
ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"
ports:
- "8002:8080"
depends_on:
- mysql
- redis
- nacos
- skywalking-oap
- elasticsearch
networks:
- canvas-network
# 拍卖服务
auction-service:
build:
context: ./canvas-chain-auction-service
dockerfile: Dockerfile
container_name: canvas-auction-service
environment:
JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
SW_AGENT_NAME: "canvas-auction-service"
SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
NACOS_SERVER_ADDR: "nacos:8848"
MYSQL_HOST: "mysql"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root123456"
REDIS_HOST: "redis"
REDIS_PASSWORD: "canvas123456"
RABBITMQ_HOST: "rabbitmq"
RABBITMQ_USER: "guest"
RABBITMQ_PASSWORD: "guest"
ports:
- "8003:8080"
depends_on:
- mysql
- redis
- nacos
- rabbitmq
- skywalking-oap
networks:
- canvas-network
# 盲盒服务
blindbox-service:
build:
context: ./canvas-chain-blindbox-service
dockerfile: Dockerfile
container_name: canvas-blindbox-service
environment:
JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
SW_AGENT_NAME: "canvas-blindbox-service"
SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
NACOS_SERVER_ADDR: "nacos:8848"
MYSQL_HOST: "mysql"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root123456"
REDIS_HOST: "redis"
REDIS_PASSWORD: "canvas123456"
RABBITMQ_HOST: "rabbitmq"
ports:
- "8004:8080"
depends_on:
- mysql
- redis
- nacos
- rabbitmq
- skywalking-oap
networks:
- canvas-network
# 订单服务
order-service:
build:
context: ./canvas-chain-order-service
dockerfile: Dockerfile
container_name: canvas-order-service
environment:
JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
SW_AGENT_NAME: "canvas-order-service"
SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
NACOS_SERVER_ADDR: "nacos:8848"
MYSQL_HOST: "mysql"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root123456"
REDIS_HOST: "redis"
REDIS_PASSWORD: "canvas123456"
RABBITMQ_HOST: "rabbitmq"
SEATA_SERVER: "seata-server:8091"
ports:
- "8005:8080"
depends_on:
- mysql
- redis
- nacos
- rabbitmq
- skywalking-oap
- seata-server
networks:
- canvas-network
# 钱包服务
wallet-service:
build:
context: ./canvas-chain-wallet-service
dockerfile: Dockerfile
container_name: canvas-wallet-service
environment:
JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
SW_AGENT_NAME: "canvas-wallet-service"
SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
NACOS_SERVER_ADDR: "nacos:8848"
MYSQL_HOST: "mysql"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root123456"
REDIS_HOST: "redis"
REDIS_PASSWORD: "canvas123456"
RABBITMQ_HOST: "rabbitmq"
SEATA_SERVER: "seata-server:8091"
ports:
- "8006:8080"
depends_on:
- mysql
- redis
- nacos
- rabbitmq
- skywalking-oap
- seata-server
networks:
- canvas-network
# 投票服务
vote-service:
build:
context: ./canvas-chain-vote-service
dockerfile: Dockerfile
container_name: canvas-vote-service
environment:
JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
SW_AGENT_NAME: "canvas-vote-service"
SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
NACOS_SERVER_ADDR: "nacos:8848"
MYSQL_HOST: "mysql"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root123456"
REDIS_HOST: "redis"
REDIS_PASSWORD: "canvas123456"
RABBITMQ_HOST: "rabbitmq"
ports:
- "8007:8080"
depends_on:
- mysql
- redis
- nacos
- rabbitmq
- skywalking-oap
networks:
- canvas-network
# NFT 服务
nft-service:
build:
context: ./canvas-chain-nft-service
dockerfile: Dockerfile
container_name: canvas-nft-service
environment:
JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
SW_AGENT_NAME: "canvas-nft-service"
SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
NACOS_SERVER_ADDR: "nacos:8848"
MYSQL_HOST: "mysql"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root123456"
REDIS_HOST: "redis"
REDIS_PASSWORD: "canvas123456"
RABBITMQ_HOST: "rabbitmq"
ports:
- "8008:8080"
depends_on:
- mysql
- redis
- nacos
- rabbitmq
- skywalking-oap
networks:
- canvas-network
# 定时任务服务
job-service:
build:
context: ./canvas-chain-job-service
dockerfile: Dockerfile
container_name: canvas-job-service
environment:
JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
SW_AGENT_NAME: "canvas-job-service"
SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
NACOS_SERVER_ADDR: "nacos:8848"
MYSQL_HOST: "mysql"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root123456"
REDIS_HOST: "redis"
REDIS_PASSWORD: "canvas123456"
XXL_JOB_ADMIN_ADDRESSES: "http://xxl-job-admin:8080/xxl-job-admin"
ports:
- "8009:8080"
depends_on:
- mysql
- redis
- nacos
- skywalking-oap
- xxl-job-admin
networks:
- canvas-network
volumes:
mysql-data:
redis-data:
rabbitmq-data:
nacos-data:
es-data:
seata-data:
networks:
canvas-network:
driver: bridge
🚀 快速启动指南
# 1. 克隆项目
git clone https://github.com/your-username/canvas-chain.git
cd canvas-chain
# 2. 启动所有容器
docker-compose up -d
# 3. 查看日志
docker-compose logs -f
# 4. 初始化数据库
docker exec canvas-mysql mysql -uroot -proot123456 < docs/init.sql
# 5. 访问各服务
# API 网关: http://localhost:9000
# Nacos: http://localhost:8848/nacos (user/nacos)
# XXL-Job: http://localhost:8081/xxl-job-admin (admin/123456)
# Skywalking: http://localhost:8080
# Kibana: http://localhost:5601
# RabbitMQ: http://localhost:15672 (guest/guest)
📊 性能优化建议
1. 数据库优化
-- 创意表索引优化
CREATE INDEX idx_creator_id ON artwork(creator_id);
CREATE INDEX idx_category ON artwork(category);
CREATE INDEX idx_created_time ON artwork(created_time);
CREATE INDEX idx_status ON artwork(status);
-- 拍卖表索引
CREATE INDEX idx_auction_status ON auction(status);
CREATE INDEX idx_auction_end_time ON auction(end_time);
-- 交易表索引
CREATE INDEX idx_transaction_user ON transaction(user_id);
CREATE INDEX idx_transaction_created ON transaction(created_time);
2. Redis 热点数据
@Service
public class CacheWarmupService {
@Scheduled(cron = "0 0 * * * *") // 每小时执行
public void warmupHotData() {
// 预热热门创意
List<Artwork> hotArtworks = artworkService
.listHotArtworks();
for (Artwork artwork : hotArtworks) {
String key = "artwork:" + artwork.getId();
redisTemplate.opsForValue()
.set(key, artwork, Duration.ofHours(1));
}
// 预热排行榜
redisTemplate.opsForValue()
.set("ranking:daily", getRankingData(),
Duration.ofHours(1));
}
}
3. 异步处理优化
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("canvas-async-");
executor.initialize();
return executor;
}
}
// 使用
@Service
public class AuctionService {
@Async("taskExecutor")
public void processAuctionSettlement(Long auctionId) {
// 耗时操作
}
}
📈 监控指标
关键指标监控
@Component
@Slf4j
public class MetricsCollector {
@Autowired
private MeterRegistry meterRegistry;
// 记录拍卖竞价
public void recordBid(Long auctionId,
BigDecimal bidAmount) {
meterRegistry.counter(
"auction.bid.count",
"auctionId", auctionId.toString()
).increment();
meterRegistry.gauge(
"auction.bid.amount",
bidAmount.doubleValue()
);
}
// 记录交易
public void recordTransaction(Long userId,
BigDecimal amount) {
meterRegistry.counter(
"transaction.count",
"userId", userId.toString()
).increment();
meterRegistry.timer("transaction.duration")
.record(Duration.ofSeconds(1));
}
// 记录缓存命中率
public void recordCacheHit(String cacheName,
boolean hit) {
meterRegistry.counter(
"cache.hits",
"cache", cacheName,
"hit", String.valueOf(hit)
).increment();
}
}
🎯 最终交付清单
8周后你将得到:
| 阶段 (Week) | 完成内容 (Deliverables) | 技术栈 (Tech Stack) |
|---|---|---|
| W1 | 微服务架构搭建:项目脚手架、服务注册与发现、数据库初始化 | Nacos、Spring Cloud Alibaba、Spring Boot、MySQL |
| W2 | 创意服务开发 + 多级缓存:作品 CRUD、缓存穿透/击穿防护、搜索引擎接入 | Redis、Caffeine、本地缓存、布隆过滤器、ElasticSearch |
| W3 | 消息队列系统:事件驱动设计、死信队列、消息幂等性保证 | RabbitMQ、Spring AMQP、消息模型设计 |
| W4 | 拍卖核心逻辑:实时竞价、限流、原子性出价脚本 | WebSocket、Sentinel、Redis Lua、并发锁 |
| W5 | 盲盒系统 + 订单管理:加权抽奖、库存管理、订单全流程 | 抽奖算法、幂等设计、订单状态机、事务管理 |
| W6 | 钱包系统 + 分布式事务:扣费、锁定、收益结算、跨服务一致性 | Seata、TCC 模式、AT 模式、补偿机制 |
| W7 | 投票系统 + 定时任务:权重投票、排行榜、自动化任务调度 | XXL-Job、投票算法、排行榜计算 |
| W8 | 监控 + 上线部署:链路追踪、容器化、自动化部署流程 | SkyWalking、Docker、docker-compose、CI/CD |
代码统计:
- ✅ 总代码行数:~50,000+ 行
- ✅ 微服务数量:10 个
- ✅ 数据库表:30+ 张
- ✅ API 接口:200+ 个
- ✅ 中间件集成:全栈
简历亮点:
- ✅ 完整的分布式系统设计经验
- ✅ 中间件全栈掌握(MQ、缓存、注册中心、限流、事务、任务调度)
- ✅ 高并发场景实战经验
- ✅ 企业级代码规范和架构设计能力
- ✅ 可部署的生产级系统
VibeCoding 导航:⬅️ 02-CanvasChain 第1周 - 基础架构搭建详细设计文档 | 01-计划 | ➡️ 02-CanvasChain 项目 8 周计划可行性分析
💬 评论