🔥 完整部署教程:Java德州扑克小酒馆小程序(从零到上线)

2026年5月最新版 · SpringBoot + UniApp + 微信小程序 + Docker一键部署


📋 目录导航

章节 内容 预计耗时
阶段一 环境搭建 + 项目初始化 2h
阶段二 后端核心:牌局引擎 + WebSocket 4h
阶段三 小酒馆消费模块(点餐/存酒) 3h
阶段四 UniApp小程序前端开发 4h
阶段五 联调测试 + 部署上线 2h
合计 约15h(1-2天可搞定)

🏗️ 阶段一:环境搭建 + 项目初始化

1.1 安装基础环境


bash

# Java 17+
java -version   # 需 >= 17

# Maven 3.8+
mvn -v

# MySQL 8.0
mysql --version

# Redis 7
redis-cli --version

# Node.js 18+ (前端用)
node -v

# 微信开发者工具 → 下载地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html

1.2 创建项目骨架


bash

# 后端
mvn archetype:generate \
  -DgroupId=com.poker.tavern \
  -DartifactId=poker-server \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DinteractiveMode=false

cd poker-server

# pom.xml 核心依赖

xml

<dependencies>
    <!-- SpringBoot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- WebSocket 实时通信 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>

    <!-- MySQL -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>

    <!-- MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>3.5.5</version>
    </dependency>

    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- 微信支付SDK -->
    <dependency>
        <groupId>com.github.wechatpay-apiv3</groupId>
        <artifactId>wechatpay-java</artifactId>
        <version>0.2.12</version>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

1.3 数据库初始化


sql

-- 创建数据库
CREATE DATABASE poker_tavern DEFAULT CHARSET utf8mb4;
USE poker_tavern;

-- ===== 用户表 =====
CREATE TABLE `user` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `openid` VARCHAR(64) UNIQUE COMMENT '微信openid',
  `nickname` VARCHAR(50) DEFAULT '',
  `avatar` VARCHAR(255) DEFAULT '',
  `phone` VARCHAR(20) DEFAULT '',
  `balance` DECIMAL(10,2) DEFAULT 0.00 COMMENT '余额',
  `chips` INT DEFAULT 10000 COMMENT '游戏筹码',
  `level` INT DEFAULT 1,
  `total_profit` DECIMAL(10,2) DEFAULT 0.00,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- ===== 游戏房间表 =====
CREATE TABLE `poker_room` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `room_no` VARCHAR(20) UNIQUE COMMENT '房间号',
  `blind_level` INT DEFAULT 1 COMMENT '盲注等级 1/2 2/4 5/10...',
  `max_players` INT DEFAULT 9,
  `status` TINYINT DEFAULT 0 COMMENT '0等待 1游戏中 2已结束',
  `creator_id` BIGINT,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- ===== 牌局记录表 =====
CREATE TABLE `poker_hand` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `room_id` BIGINT,
  `player_id` BIGINT,
  `hole_cards` VARCHAR(20) COMMENT '底牌 AH-KH',
  `final_rank` INT COMMENT '0高牌~9皇家同花顺',
  `win_chips` INT DEFAULT 0,
  `is_winner` TINYINT DEFAULT 0,
  `played_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- ===== 点餐订单表 =====
CREATE TABLE `dinner_order` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `user_id` BIGINT,
  `table_no` VARCHAR(10) COMMENT '桌台号',
  `items` JSON COMMENT '菜品列表',
  `total_amount` DECIMAL(10,2),
  `status` TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2完成 3取消',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- ===== 存酒记录表 =====
CREATE TABLE `wine_storage` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `user_id` BIGINT,
  `wine_name` VARCHAR(100),
  `quantity` INT DEFAULT 1,
  `store_date` DATE,
  `status` TINYINT DEFAULT 0 COMMENT '0存放中 1已取走',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 插入测试数据
INSERT INTO user (openid, nickname, chips) VALUES 
('test_openid_001', '扑克王', 50000),
('test_openid_002', '斗地主', 30000);

1.4 配置文件 application.yml


yaml

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/poker_tavern?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root123
    driver-class-name: com.mysql.cj.jdbc.Driver

  redis:
    host: localhost
    port: 6379
    password: 
    database: 0

  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
  global-config:
    db-config:
      id-type: auto

# 微信小程序配置
wechat:
  miniapp:
    appid: wx1234567890abcdef    # 替换你的AppID
    secret: your_secret_here      # 替换你的Secret

# 微信支付配置
wechat:
  pay:
    mch-id: 1234567890
    api-key: your_api_key
    notify-url: https://your-domain.com/api/pay/notify

🎮 阶段二:后端核心代码实现

2.1 扑克牌核心类


java

// ========== Card.java ==========
@Data
@AllArgsConstructor
public class Card {
    private Suit suit;    // 花色
    private Rank rank;    // 点数

    public enum Suit { HEARTS, DIAMONDS, CLUBS, SPADES }
    public enum Rank {
        TWO(2), THREE(3), FOUR(4), FIVE(5), SIX(6), SEVEN(7), EIGHT(8),
        NINE(9), TEN(10), JACK(11), QUEEN(12), KING(13), ACE(14);

        private final int value;
        Rank(int value) { this.value = value; }
        public int getValue() { return value; }
    }

    @Override
    public String toString() {
        return suit.name().charAt(0) + rank.name().charAt(0);
    }
}

java

// ========== Deck.java 牌组 ==========
@Component
public class Deck {

    public List<Card> createDeck() {
        List<Card> deck = new ArrayList<>(52);
        for (Card.Suit suit : Card.Suit.values()) {
            for (Card.Rank rank : Card.Rank.values()) {
                deck.add(new Card(suit, rank));
            }
        }
        return deck;
    }

    public void shuffle(List<Card> deck) {
        Collections.shuffle(deck);
    }
}

2.2 ⭐ 牌型判定引擎(核心算法)


java

@Component
public class PokerHandEvaluator {

    /**
     * 从7张牌中选出最好的5张,返回牌型等级
     * 0=高牌 1=一对 2=两对 3=三条 4=顺子 5=同花
     * 6=葫芦 7=四条 8=同花顺 9=皇家同花顺
     */
    public int evaluate(List<Card> sevenCards) {
        List<List<Card>> combos = combinations(sevenCards, 5);
        int bestRank = -1;
        for (List<Card> combo : combos) {
            bestRank = Math.max(bestRank, rankHand(combo));
        }
        return bestRank;
    }

    private int rankHand(List<Card> cards) {
        Collections.sort(cards, Comparator.comparingInt(c -> c.getRank().getValue()).reversed());

        boolean flush = cards.stream().map(Card::getSuit).distinct().count() == 1;
        boolean straight = checkStraight(cards);

        Map<Integer, Long> freq = cards.stream()
            .collect(Collectors.groupingBy(c -> c.getRank().getValue(), Collectors.counting()));
        List<Long> counts = new ArrayList<>(freq.values());
        Collections.sort(counts, Collections.reverseOrder());

        if (flush && straight && cards.get(0).getRank().getValue() == 14) return 9; // 皇家同花顺
        if (flush && straight) return 8; // 同花顺
        if (counts.get(0) == 4) return 7; // 四条
        if (counts.get(0) == 3 && counts.get(1) == 2) return 6; // 葫芦
        if (flush) return 5; // 同花
        if (straight) return 4; // 顺子
        if (counts.get(0) == 3) return 3; // 三条
        if (counts.get(0) == 2 && counts.get(1) == 2) return 2; // 两对
        if (counts.get(0) == 2) return 1; // 一对
        return 0; // 高牌
    }

    private boolean checkStraight(List<Card> cards) {
        List<Integer> vals = cards.stream()
            .map(c -> c.getRank().getValue()).distinct().sorted().toList();
        if (vals.size() < 5) return false;
        // A-2-3-4-5 特殊顺子
        if (vals.contains(14) && vals.contains(2) && vals.contains(3) 
            && vals.contains(4) && vals.contains(5)) return true;
        for (int i = 0; i < vals.size() - 4; i++) {
            if (vals.get(i + 4) - vals.get(i) == 4) return true;
        }
        return false;
    }

    private List<List<Card>> combinations(List<Card> cards, int k) {
        List<List<Card>> result = new ArrayList<>();
        combine(cards, k, 0, new ArrayList<>(), result);
        return result;
    }

    private void combine(List<Card> cards, int k, int start,
                         List<Card> current, List<List<Card>> result) {
        if (current.size() == k) {
            result.add(new ArrayList<>(current));
            return;
        }
        for (int i = start; i < cards.size(); i++) {
            current.add(cards.get(i));
            combine(cards, k, i + 1, current, result);
            current.remove(current.size() - 1);
        }
    }
}

2.3 ⭐ WebSocket 实时牌局同步


java

@ServerEndpoint("/ws/room/{roomId}")
@Component
public class PokerWebSocket {

    private static final Map<String, Set<Session>> ROOM_SESSIONS = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session, @PathParam String roomId) {
        ROOM_SESSIONS.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet())
                     .add(session);
        System.out.println("玩家加入房间: " + roomId);
    }

    @OnMessage
    public void onMessage(String message, @PathParam String roomId) {
        GameMessage msg = JSON.parseObject(message, GameMessage.class);

        switch (msg.getAction()) {
            case "bet" -> processBet(roomId, msg);
            case "fold" -> processFold(roomId, msg);
            case "call"  -> processCall(roomId, msg);
            case "all_in" -> processAllIn(roomId, msg);
        }

        broadcast(roomId, msg);
    }

    @OnClose
    public void onClose(Session session, @PathParam String roomId) {
        Set<Session> sessions = ROOM_SESSIONS.get(roomId);
        if (sessions != null) sessions.remove(session);
    }

    private void broadcast(String roomId, GameMessage msg) {
        Set<Session> sessions = ROOM_SESSIONS.get(roomId);
        if (sessions == null) return;
        sessions.forEach(s -> {
            try {
                s.getAsyncRemote().sendText(JSON.toJSONString(msg));
            } catch (Exception e) { e.printStackTrace(); }
        });
    }

    // ===== 牌局动作处理 =====
    private void processBet(String roomId, GameMessage msg) {
        PokerRoom room = roomService.getRoom(roomId);
        Player player = room.getPlayer(msg.getPlayerId());
        player.setChips(player.getChips() - msg.getAmount());
        room.setCurrentBet(Math.max(room.getCurrentBet(), msg.getAmount()));
        room.setPot(room.getPot() + msg.getAmount());
    }

    private void processFold(String roomId, GameMessage msg) {
        PokerRoom room = roomService.getRoom(roomId);
        room.getPlayer(msg.getPlayerId()).setFolded(true);
        checkWinner(room);
    }

    private void checkWinner(PokerRoom room) {
        List<Player> activePlayers = room.getPlayers().stream()
            .filter(p -> !p.isFolded()).toList();

        if (activePlayers.size() == 1) {
            // 只剩一人,直接获胜
            Player winner = activePlayers.get(0);
            winner.setChips(winner.getChips() + room.getPot());
            room.setStatus(2); // 结束
        } else if (activePlayers.size() > 1 && room.getPhase() >= 4) {
            // 河牌阶段,比牌
            compareHands(room, activePlayers);
        }
    }

    private void compareHands(PokerRoom room, List<Player> players) {
        PokerHandEvaluator evaluator = new PokerHandEvaluator();
        Player bestPlayer = null;
        int bestRank = -1;

        for (Player p : players) {
            List<Card> allCards = new ArrayList<>(p.getHoleCards());
            allCards.addAll(room.getCommunityCards());
            int rank = evaluator.evaluate(allCards);
            if (rank > bestRank) {
                bestRank = rank;
                bestPlayer = p;
            }
        }

        bestPlayer.setChips(bestPlayer.getChips() + room.getPot());
        room.setStatus(2);
    }
}

java

// ========== GameMessage.java ==========
@Data
public class GameMessage {
    private String action;      // bet/fold/call/all_in/deal
    private Long playerId;
    private Integer amount;      // 下注金额
    private List<String> cards;  // 公共牌
    private Integer pot;         // 底池
    private Integer phase;       // 0翻前 1翻牌 2转牌 3河牌
}

2.4 REST API 控制器


java

@RestController
@RequestMapping("/api")
@CrossOrigin
public class GameController {

    @Autowired private PokerRoomService roomService;
    @Autowired private UserService userService;
    @Autowired private OrderService orderService;

    // ---- 房间管理 ----
    @PostMapping("/room/create")
    public R<String> createRoom(@RequestBody CreateRoomReq req) {
        String roomNo = "ROOM" + System.currentTimeMillis() % 10000;
        roomService.createRoom(roomNo, req.getBlindLevel(), req.getCreatorId());
        return R.ok(roomNo);
    }

    @GetMapping("/room/list")
    public R<List<PokerRoom>> roomList() {
        return R.ok(roomService.getWaitingRooms());
    }

    @PostMapping("/room/{roomNo}/join")
    public R<String> joinRoom(@PathVariable String roomNo, @RequestBody Long userId) {
        roomService.joinRoom(roomNo, userId);
        return R.ok("加入成功");
    }

    // ---- 用户 ----
    @PostMapping("/user/login")
    public R<User> login(@RequestBody WxLoginReq req) {
        User user = userService.wxLogin(req.getCode());
        return R.ok(user);
    }

    @PostMapping("/user/recharge")
    public R<String> recharge(@RequestBody RechargeReq req) {
        userService.recharge(req.getUserId(), req.getAmount());
        return R.ok("充值成功");
    }

    // ---- 点餐 ----
    @PostMapping("/order/create")
    public R<String> createOrder(@RequestBody OrderReq req) {
        orderService.createOrder(req);
        return R.ok("下单成功");
    }

    @PostMapping("/order/pay")
    public R<String> payOrder(@RequestBody PayReq req) {
        orderService.payOrder(req.getOrderId(), req.getUserId());
        return R.ok("支付成功");
    }

    // ---- 存酒 ----
    @PostMapping("/wine/store")
    public R<String> storeWine(@RequestBody WineReq req) {
        orderService.storeWine(req);
        return R.ok("存酒成功");
    }
}

🍺 阶段三:小酒馆消费模块

3.1 菜品管理


java

@Data
@TableName("menu_item")
public class MenuItem {
    private Long id;
    private String name;
    private String category;  // 酒水/小吃/套餐
    private BigDecimal price;
    private String image;
    private Integer stock;
}

java

@RestController
@RequestMapping("/api/menu")
public class MenuController {

    @Autowired private MenuItemService menuService;

    @GetMapping("/list")
    public R<List<MenuItem>> list(@RequestParam String category) {
        return R.ok(menuService.listByCategory(category));
    }

    @PostMapping("/cart/add")
    public R<String> addToCart(@RequestBody CartReq req) {
        // Redis 存储购物车 key: cart:{userId}
        return R.ok("已加入购物车");
    }
}

3.2 微信支付对接


java

@Service
public class PayService {

    @Autowired private WeChatPayClient weChatPayClient;

    public String createOrder(Long userId, BigDecimal amount, String description) {
        // 1. 调用微信统一下单
        JSONObject params = new JSONObject();
        params.put("appid", "wx1234567890abcdef");
        params.put("mchid", "1234567890");
        params.put("description", description);
        params.put("out_trade_no", "ORDER" + System.currentTimeMillis());
        params.put("notify_url", "https://your-domain.com/api/pay/notify");
        params.put("amount", amount.multiply(new BigDecimal(100)).intValue());

        JSONObject result = weChatPayClient.post("v3/pay/transactions/jsapi", params, 
            JSONObject.class, 
            AutoCertificateExtension.class,
            new HostNameCertificateVerifier());

        return result.getString("prepay_id");
    }

    // 支付回调
    @PostMapping("/api/pay/notify")
    public String payNotify(@RequestBody String body) {
        // 验证签名 → 更新订单状态 → 返回成功
        return "{\"code\":\"SUCCESS\",\"message\":\"成功\"}";
    }
}

📱 阶段四:UniApp 小程序前端

4.1 项目创建


bash

# 使用 HBuilderX 创建 UniApp 项目
# 或命令行:
npx degit dcloudio/uni-preset-vue#vite-ts my-poker-tavern
cd my-poker-tavern
npm install

4.2 项目结构


my-poker-tavern/
├── pages/
│   ├── index/          # 首页(酒馆入口)
│   ├── lobby/          # 游戏大厅
│   ├── table/          # 牌桌(核心)
│   ├── order/          # 点餐
│   └── mine/           # 我的
├── components/
│   ├── poker-card.vue  # 扑克牌组件
│   └── player-seat.vue # 玩家座位
├── store/              # Pinia状态管理
├── App.vue
└── manifest.json

4.3 ⭐ 牌桌页面(核心)


vue

<!-- pages/table/table.vue -->
<template>
  <view class="poker-table">
    <!-- 公共牌区域 -->
    <view class="community-area">
      <view class="pot">底池: {{ potChips }} 💰</view>
      <view class="cards">
        <view v-for="(card, i) in communityCards" :key="i" class="card">
          {{ card }}
        </view>
        <view v-if="communityCards.length < 5" class="card empty">?</view>
      </view>
      <view class="phase">{{ phaseText }}</view>
    </view>

    <!-- 玩家座位 -->
    <view v-for="player in players" :key="player.id"
          class="seat" :class="{ folded: player.folded }"
          :style="{ left: player.x + 'px', top: player.y + 'px' }">
      <image class="avatar" :src="player.avatar" />
      <text class="name">{{ player.nickname }}</text>
      <text class="chips">💰{{ player.chips }}</text>
      <view v-if="player.isDealer" class="dealer-badge">D</view>
      <view v-if="player.isMe" class="me-badge">我</view>
    </view>

    <!-- 我的手牌 -->
    <view class="my-cards">
      <view v-for="card in myCards" :key="card" class="card my-card">
        {{ card }}
      </view>
    </view>

    <!-- 操作按钮 -->
    <view class="actions">
      <button class="btn-fold" @click="doAction('fold')">弃牌</button>
      <button class="btn-call" @click="doAction('call')">
        跟注 {{ currentBet }}
      </button>
      <button class="btn-raise" @click="doAction('raise')">加注</button>
      <button class="btn-allin" @click="doAction('all_in')">ALL IN 🔥</button>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

const roomId = ref('')
const ws = ref<WebSocket | null>(null)

const potChips = ref(0)
const communityCards = ref<string[]>([])
const players = ref<any[]>([])
const myCards = ref<string[]>([])
const currentBet = ref(0)
const phase = ref(0) // 0翻前 1翻牌 2转牌 3河牌

const phaseText = computed(() => {
  const map = ['翻牌前', '翻牌', '转牌', '河牌']
  return map[phase.value] || ''
})

onMounted(() => {
  roomId.value = getCurrentInstance()?.proxy?.$route?.params?.roomId || ''

  ws.value = uni.connectSocket({
    url: `wss://your-domain.com/ws/room/${roomId.value}`
  })

  ws.value.onMessage((res) => {
    const msg = JSON.parse(res.data)

    if (msg.action === 'deal') {
      myCards.value = msg.myCards || []
    }
    if (msg.action === 'community') {
      communityCards.value = msg.cards || []
    }
    if (msg.action === 'pot_update') {
      potChips.value = msg.pot || 0
    }
    if (msg.action === 'player_update') {
      players.value = msg.players || []
    }
    if (msg.action === 'phase') {
      phase.value = msg.phase || 0
    }
    if (msg.action === 'bet_update') {
      currentBet.value = msg.currentBet || 0
    }
  })
})

const doAction = (action: string) => {
  ws.value?.send({
    data: JSON.stringify({
      action,
      playerId: getApp().globalData.userId,
      amount: action === 'all_in' ? 999999 : currentBet.value
    })
  })
}

onUnmounted(() => {
  ws.value?.close()
})
</script>

<style scoped>
.poker-table {
  width: 100vw;
  height: 100vh;
  background: radial-gradient(ellipse, #1a5c2a, #0d3318);
  position: relative;
  overflow: hidden;
}
.seat {
  position: absolute;
  width: 80px;
  text-align: center;
  color: #fff;
}
.card {
  width: 40px;
  height: 56px;
  background: #fff;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  margin: 2px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.btn-allin {
  background: linear-gradient(135deg, #ff4444, #cc0000) !important;
  color: #fff !important;
  font-size: 18px;
  font-weight: bold;
}
</style>

4.4 首页(酒馆入口)


vue

<!-- pages/index/index.vue -->
<template>
  <view class="index">
    <!-- 顶部Banner -->
    <swiper class="banner" autoplay circular>
      <swiper-item v-for="img in banners" :key="img">
        <image :src="img" mode="aspectFill" />
      </swiper-item>
    </swiper>

    <!-- 功能入口 -->
    <view class="menu-grid">
      <view class="menu-item" @click="goLobby">
        <text class="icon">🃏</text>
        <text>开始游戏</text>
      </view>
      <view class="menu-item" @click="goOrder">
        <text class="icon">🍺</text>
        <text>酒馆点餐</text>
      </view>
      <view class="menu-item" @click="goWine">
        <text class="icon">🍷</text>
        <text>存酒取酒</text>
      </view>
      <view class="menu-item" @click="goMine">
        <text class="icon">👤</text>
        <text>个人中心</text>
      </view>
    </view>

    <!-- 热门房间 -->
    <view class="hot-rooms">
      <text class="title">🔥 热门牌局</text>
      <view v-for="room in rooms" :key="room.id"
            class="room-card" @click="joinRoom(room.roomNo)">
        <text class="room-no">{{ room.roomNo }}</text>
        <text class="blind">盲注 {{ room.blindLevel }}/{{ room.blindLevel * 2 }}</text>
        <text class="players">{{ room.currentPlayers }}/{{ room.maxPlayers }}人</text>
      </view>
    </view>
  </view>
</template>

🚀 阶段五:Docker 部署上线

5.1 Docker Compose 一键部署


yaml

# docker-compose.yml
version: '3.8'
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: poker_tavern
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

  poker-server:
    build: ./poker-server
    ports:
      - "8080:8080"
    depends_on:
      - mysql
      - redis
    environment:
      SPRING_PROFILES_ACTIVE: prod

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./dist:/usr/share/nginx/html   # 小程序静态资源

volumes:
  mysql_data:
  redis_data:

5.2 Nginx 配置


nginx

# nginx.conf
server {
    listen 80;
    server_name your-domain.com;

    # 小程序前端
    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    # 后端API代理
    location /api/ {
        proxy_pass http://poker-server:8080/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # WebSocket代理
    location /ws/ {
        proxy_pass http://poker-server:8080/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

5.3 部署命令


bash

# 1. 打包后端
cd poker-server
mvn clean package -DskipTests

# 2. 打包小程序
cd my-poker-tavern
npm run build:mp-weixin   # 编译为微信小程序

# 3. 一键启动
cd ..
docker-compose up -d --build

# 4. 查看日志
docker-compose logs -f poker-server

5.4 微信小程序上线流程


微信公众平台 → 小程序管理 → 版本管理 → 上传代码

上传 dist/build/mp-weixin 目录下的所有文件

→ 提交审核(1-3个工作日)
→ 审核通过 → 发布上线 ✅

📊 管理后台(Vue3)


管理后台地址:https://your-domain.com/admin

功能:
├── 用户管理(封号/充值/查战绩)
├── 房间管理(设置盲注/最大人数)
├── 订单管理(点餐/充值/退款)
├── 商品管理(酒水/菜品/库存)
├── 数据统计(DAU/收入/胜率分布)
└── 系统配置(轮播图/公告/微信支付)

⚡ 性能优化清单

优化项 方案 效果
牌局状态 Redis 存储(而非DB) 响应 < 50ms
房间列表 Redis Sorted Set 毫秒级排序
消息推送 WebSocket 长连接 实时无延迟
图片资源 CDN + WebP 加载提速60%
数据库 读写分离 + 索引 QPS 提升3倍

🎯 快速启动命令(一键运行版)


bash

# 克隆项目后执行:
git clone https://github.com/xxx/poker-tavern.git
cd poker-tavern

# 启动所有服务
docker-compose up -d

# 访问:
# 后端API: http://localhost:8080/api/room/list
# 管理后台: http://localhost:8081
# 小程序: 微信开发者工具导入 my-poker-tavern/dist/build/mp-weixin

更多推荐