盲盒番赏小程序怎么开发?一番赏+赛博赏+大转盘完整方案

项目背景: 一款支持实物奖品(手表、酒水、数码等)的盲盒翻赏小程序,包含一番赏、赛博赏、大转盘、房间系统等多种玩法,Uniapp开发,小程序/APP/H5三端同步上线。本文从技术实现角度解析核心功能的开发要点。


一、系统功能结构

基于实际项目整理的核心功能模块:

模块 包含功能
首页 模式切换(ALL/一番赏/赛博赏/天选赏)、分类标签、公告栏、新人入口...等
抽奖详情 库存进度条、奖品列表(A/B/C/D/E赏)、中奖记录、换箱、一发/三发/五发/十发...等
赛博赏 9宫格奖品墙、颜色区分稀有度、光标下落动画、跳过按钮...等
大转盘 九宫格转盘、折扣券/免费开盒、提现规则...等
房间系统 房间列表、魂力机制、创建房间、加入房间...等
用户端 赏袋管理、订单记录、收货地址...等
管理后台 奖品配置、库存管理、订单管理、概率设置...等

二、首页模式切换的技术实现

首页支持四种模式(ALL/一番赏/赛博赏/天选赏)并行切换,用户点击Tab切换当前显示的奖池数据。

前端实现:

javascript复制

复制代码
// 模式切换的核心逻辑
data() {
  return {
    currentMode: 'ALL', // 默认显示全部
    prizeList: []       // 当前模式下的奖品列表
  }
},
methods: {
  switchMode(mode) {
    this.currentMode = mode
    this.loadPrizeList(mode)
  },
  async loadPrizeList(mode) {
    // 调用后端接口,根据模式筛选数据
    const res = await this.$api.getPrizeList({ mode })
    this.prizeList = res.data
  }
}

模式切换时重新请求后端接口,按mode参数筛选对应奖池。分类标签(高达、初音未来、卡牌等)的实现方式相同,只是筛选维度从mode换成category。

数据表设计:

sql复制

复制代码
-- 奖池表,mode字段区分玩法
CREATE TABLE prize_pool (
  id BIGINT PRIMARY KEY,
  name VARCHAR(100),
  mode ENUM('ALL', 'YIFAN', 'CYBER', 'TIANXUAN'),
  category VARCHAR(50),
  total_stock INT,
  remain_stock INT,
  price DECIMAL(10,2),
  rare_level ENUM('A','B','C','D','E'),
  probability DECIMAL(5,4),  -- 抽取概率
  create_time DATETIME
);

三、一番赏模式开发要点

一番赏的核心逻辑是固定总数、递减库存、概率按位置分布。详情页包含几个关键组件:

3.1 库存进度条

html复制

复制代码
<!-- 进度条显示 -->
<view class="progress-bar">
  <view class="progress-fill" :style="{width: remainPercent + '%'}"></view>
  <text class="progress-text">剩余{{remain}}/{{total}}发</text>
</view>

进度条低于20%时加红色闪烁效果提醒用户"快没了",通过CSS动画实现:

css复制

复制代码
@keyframes blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}
.low-stock .progress-fill {
  background: #ff4d4f;
  animation: blink 1s infinite;
}

3.2 换箱功能

当一个番赏的库存抽完后,后端自动开启下一箱。用户可手动切换"上一箱/下一箱":

javascript复制

复制代码
// 换箱逻辑
async changeBox(direction) {
  const currentPool = this.currentPool
  const targetIndex = direction === 'next' 
    ? currentPool.boxIndex + 1 
    : currentPool.boxIndex - 1
  
  // 检查目标箱是否存在
  const res = await this.$api.checkBox({
    poolId: currentPool.id,
    boxIndex: targetIndex
  })
  
  if (res.data.available) {
    this.currentPool = res.data.pool
  }
}

3.3 高并发防超卖

一番赏在高并发下最大的风险是超卖------库存显示还有但实际已经被别人抽走了。解决方案:

java复制

复制代码
// Redis Lua脚本,原子性扣减库存
String luaScript = 
  "if redis.call('exists', KEYS[1]) == 1 then " +
  "  local stock = tonumber(redis.call('get', KEYS[1])) " +
  "  if stock > 0 then " +
  "    redis.call('decr', KEYS[1]) " +
  "    return 1 " +
  "  else " +
  "    return 0 " +
  "  end " +
  "else return -1 end";

RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(stockKey));

先扣Redis库存,扣成功后再写MySQL订单记录。如果MySQL写入失败,Redis库存加回去并返回"库存不足"。这个流程保证了Redis和MySQL的一致性。


四、赛博赏模式实现

赛博赏的交互是"光标在9宫格奖品墙上逐行下落,停在哪格就中哪个奖品"。

4.1 奖品墙布局

html复制

复制代码
<view class="prize-grid">
  <view 
    v-for="(prize, index) in prizeList" 
    :key="index"
    :class="['prize-cell', 'level-' + prize.rareLevel]">
    <image :src="prize.image" mode="aspectFit" />
    <!-- 稀有度用边框颜色区分 -->
  </view>
</view>

稀有度颜色对应关系:A赏红色、B赏粉色、C赏青色、D/E赏橙色,用CSS边框实现,不需要额外的图标资源。

4.2 光标动画

javascript复制

复制代码
// 光标下落动画核心逻辑
async startDrop() {
  // 1. 预加载所有奖品图片
  await this.preloadImages()
  
  // 2. 服务端计算中奖结果
  const result = await this.$api.drawLottery({ poolId: this.poolId })
  const targetIndex = result.data.prizeIndex
  
  // 3. 计算光标路径(先快后慢停在目标位置)
  const totalRows = 3
  const targetRow = Math.floor(targetIndex / 3)
  
  // 动画分三阶段:快速下落 -> 减速 -> 停在目标
  await this.animateDrop(targetRow, 800, 400)  // 快速阶段
  await this.animateSlow(targetRow, 600)        // 减速阶段
}

动画的减速曲线调了很久------太快显得突兀,太慢拖沓。最后用的是二次贝塞尔曲线cubic-bezier(0.2, 0.8, 0.2, 1),实测视觉效果最自然。

4.3 服务端校验

前端动画只是视觉效果,真实中奖结果必须由服务端决定:

java复制

复制代码
@PostMapping("/draw")
public AjaxResult drawLottery(@RequestBody DrawDTO dto) {
    // 1. 验证用户状态
    // 2. 检查库存
    // 3. 按概率算法计算中奖结果(与前端动画无关)
    // 4. 扣减库存
    // 5. 返回中奖信息给前端
    
    PrizeDTO result = prizeService.draw(dto.getPoolId());
    return AjaxResult.success(result);
}

五、大转盘与房间系统

5.1 大转盘九宫格

大转盘不是圆形转盘而是九宫格布局,中心位是"免费开盒",周围8格是折扣券。抽奖结果同样由服务端生成:

java复制

复制代码
// 转盘概率配置(后台可调)
Map<Integer, BigDecimal> probability = new LinkedHashMap<>();
probability.put(0, new BigDecimal("0.01"));   // 免费开盒 1%
probability.put(1, new BigDecimal("0.05"));  // 9.9折 5%
// ...

// 根据概率随机选中
int result = weightedRandom(probability);

转盘每天约40%活跃用户会参与一次,是个有效的日活钩子。用户来转一次顺手可能就进赏袋看有没有新的可提现奖品,复购动机就产生了。

5.2 房间系统设计

房间系统的核心是魂力机制------用户投入件数/金额累积魂力,魂力越高在房间内中奖概率越大。

sql复制

复制代码
-- 房间表
CREATE TABLE room (
  id BIGINT PRIMARY KEY,
  name VARCHAR(100),
  creator_id BIGINT,
  total_soul INT DEFAULT 0,
  status TINYINT,  -- 0正常 1已满 2已结束
  create_time DATETIME
);

-- 用户魂力表(用Redis zset实时排序)
ZINCRBY room:{roomId}:souls {increment} {userId}
ZREVRANGE room:{roomId}:souls 0 -1 WITHSCORES

魂力用Redis zset存储,按分数倒序排列,实时显示用户在房间内的排名。排名越高越有竞争感,驱动用户持续投入。

踩过的坑: 上线第一个月大量空房间没人加入白白占用资源。后来加了"创建房间消耗房卡"机制,空房间48小时无新用户自动解散。


六、实物奖品的库存管理

实物奖品和虚拟奖品不同,抽中后需要发货。库存管理多了几步:

sql复制

复制代码
-- 实物库存表
CREATE TABLE physical_stock (
  id BIGINT PRIMARY KEY,
  prize_id BIGINT,
  warehouse VARCHAR(50),      -- 仓库
  stock INT,                   -- 库存数
  lock_stock INT DEFAULT 0,    -- 已锁库存(下单未发货)
  update_time DATETIME
);

-- 退款时回滚库存
@Transactional
public void refund(Long orderId) {
    Order order = orderMapper.selectById(orderId);
    if (order.getStatus() == 0) {  // 未发货才能退款
        physicalStockMapper.incrStock(order.getPrizeId());
        orderMapper.updateStatus(orderId, -1);  // -1表示已退款
    }
}

实物库存和虚拟库存逻辑分开写,混在一起会导致代码耦合严重,出问题排查困难。


七、总结

这个项目用Uniapp做三端开发,Java做后端,MySQL+Redis做数据层。几个技术要点:

  1. 库存超卖用Redis Lua脚本解决,原子性扣减是核心
  2. 赛博赏动画要做好图片预加载和减速曲线调参
  3. 房间系统用Redis zset做魂力排行,性能好且支持实时排序
  4. 实物奖品的库存和虚拟奖品逻辑要分开设计
  5. Uniapp多端要注意支付回调、推送等API的兼容性

开发周期约三个月,前后端加联调测试一共12周。需求冻结、并发测试、兼容测试这几件事做在前面,上线后基本稳定。

有技术方案选型或开发需求可以来聊。