🍺 酒吧德州扑克娱乐小程序 — Java全栈开发源码案例

2026年5月最新实战方案 · SpringBoot + UniApp + WebSocket + 微信支付 完整源码级解析


📋 一、项目全景总览


┌─────────────────────────────────────────────────────────┐
│           酒吧德州扑克 + 小酒馆点餐娱乐小程序               │
├──────────────┬──────────────┬───────────────────────────┤
│   🃏 扑克模块  │  🍺 酒馆模块  │   👤 用户模块              │
├──────────────┼──────────────┼───────────────────────────┤
│ • 快速匹配     │ • 扫码点餐    │ • 微信一键登录             │
│ • 私密房间     │ • 桌台绑定    │ • 积分/存酒管理            │
│ • 牌局实时同步  │ • 存酒/取酒   │ • 战绩/盈亏统计           │
│ • 牌型自动判定  │ • 优惠券      │ • 充值/提现               │
│ • 记分牌工具   │ • 订单管理    │ • 会员等级                │
│ • 胜率计算器   │ • 微信支付    │ • 抽位置功能              │
└──────────────┴──────────────┴───────────────────────────┘

🏗️ 二、技术架构选型(2026年主流方案)

层级 技术选型 说明
后端框架 SpringBoot 3.2 + JDK 21 当前企业级主流,性能最优
数据库 MySQL 8.0 核心业务数据
缓存/实时 Redis 7 房间状态 + 在线用户 + WebSocket
实时通信 Spring WebSocket 牌局状态毫秒级推送
前端小程序 UniApp (Vue3) → 微信小程序 一套代码多端运行
管理后台 Vue3 + Element Plus 运营管理系统
微信支付 wechatpay-java 0.2.12 充值 + 点餐支付
部署 Docker + Nginx 一键部署,生产级

💡 JDK版本选择:根据2026年企业使用占比,JDK 17(48%) 和 JDK 21(32%) 为主流。推荐使用 JDK 21,商业支持到2031年。


💾 三、数据库设计(12张核心表)


sql

-- ============================================
-- 酒吧德州扑克小酒馆 · 数据库初始化脚本
-- ============================================
CREATE DATABASE poker_tavern DEFAULT CHARSET utf8mb4;
USE poker_tavern;

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

-- 2. 游戏房间表
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 NOT NULL,
  `pot` INT DEFAULT 0 COMMENT '底池',
  `phase` TINYINT DEFAULT 0 COMMENT '0翻前 1翻牌 2转牌 3河牌',
  `community_cards` JSON,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 3. 牌局记录表
CREATE TABLE `poker_hand` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `room_id` BIGINT NOT NULL,
  `player_id` BIGINT NOT NULL,
  `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;

-- 4. 菜单/商品表
CREATE TABLE `menu_item` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `name` VARCHAR(100) NOT NULL,
  `category` VARCHAR(20) DEFAULT '酒水',
  `price` DECIMAL(10,2) NOT NULL,
  `image` VARCHAR(255) DEFAULT '',
  `stock` INT DEFAULT 999,
  `status` TINYINT DEFAULT 1,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 5. 点餐订单表
CREATE TABLE `dinner_order` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `order_no` VARCHAR(32) UNIQUE,
  `user_id` BIGINT NOT NULL,
  `table_no` VARCHAR(10) COMMENT '桌台号',
  `items` JSON NOT NULL,
  `total_amount` DECIMAL(10,2) NOT NULL,
  `status` TINYINT DEFAULT 0,
  `pay_time` DATETIME DEFAULT NULL,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 6. 存酒记录表
CREATE TABLE `wine_storage` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `user_id` BIGINT NOT NULL,
  `wine_name` VARCHAR(100) NOT NULL,
  `quantity` INT DEFAULT 1,
  `store_date` DATE NOT NULL,
  `status` TINYINT DEFAULT 0,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 7. 充值记录表
CREATE TABLE `recharge_log` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `user_id` BIGINT NOT NULL,
  `amount` DECIMAL(10,2) NOT NULL,
  `chips_add` INT NOT NULL,
  `pay_type` VARCHAR(20) DEFAULT 'wechat',
  `trans_id` VARCHAR(64),
  `status` TINYINT DEFAULT 0,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 初始化测试数据
INSERT INTO `user` (`openid`, `nickname`, `chips`) VALUES
('test_001', '扑克王', 50000),
('test_002', '斗地主', 30000);

🎮 四、核心后端源码(Java)

4.1 扑克牌核心类


java

// ========== Card.java ==========
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Card {
    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; }
    }
    private Suit suit;
    private Rank rank;

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

// ========== 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);
    }
}

4.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() - 5; 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);
        }
    }
}

4.3 ⭐⭐⭐ WebSocket 牌局实时同步


java

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

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

    @OnOpen
    public void onOpen(Session session, @PathParam String roomId) {
        ROOM_SESSIONS.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(session);
        log.info("玩家加入房间: {}, 在线: {}", roomId, ROOM_SESSIONS.get(roomId).size());
    }

    @OnMessage
    public void onMessage(String message, @PathParam String roomId) {
        try {
            GameMessage msg = JSON.parseObject(message, GameMessage.class);
            PokerRoomState state = ROOM_STATES.computeIfAbsent(roomId, k -> new PokerRoomState());

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

            broadcast(roomId, state.toMessage());
        } catch (Exception e) {
            log.error("处理消息失败: {}", e.getMessage());
        }
    }

    @OnClose
    public void onClose(Session session, @PathParam String roomId) {
        Set<Session> sessions = ROOM_SESSIONS.get(roomId);
        if (sessions != null) sessions.remove(session);
        if (sessions != null && sessions.isEmpty()) {
            ROOM_SESSIONS.remove(roomId);
            ROOM_STATES.remove(roomId);
            log.info("房间解散: {}", roomId);
        }
    }

    private void processBet(String roomId, PokerRoomState state, GameMessage msg) {
        PokerPlayer player = state.getPlayer(msg.getPlayerId());
        player.setChips(player.getChips() - msg.getAmount());
        state.setCurrentBet(Math.max(state.getCurrentBet(), msg.getAmount()));
        state.setPot(state.getPot() + msg.getAmount());
    }

    private void processFold(String roomId, PokerRoomState state, GameMessage msg) {
        state.getPlayer(msg.getPlayerId()).setFolded(true);
        checkWinner(roomId, state);
    }

    private void checkWinner(String roomId, PokerRoomState state) {
        List<PokerPlayer> activePlayers = state.getPlayers().stream()
            .filter(p -> !p.isFolded()).toList();

        if (activePlayers.size() == 1) {
            PokerPlayer winner = activePlayers.get(0);
            winner.setChips(winner.getChips() + state.getPot());
            state.setStatus(RoomStatus.ENDED);
            log.info("[{}] 玩家{} 独赢底池 {}", roomId, winner.getId(), state.getPot());
        } else if (activePlayers.size() > 1 && state.getPhase() >= 4) {
            compareHands(roomId, state, activePlayers);
        }
    }

    /** 比牌判定赢家 */
    private void compareHands(String roomId, PokerRoomState state, List<PokerPlayer> players) {
        PokerHandEvaluator evaluator = new PokerHandEvaluator();
        PokerPlayer bestPlayer = null;
        int bestRank = -1;

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

        bestPlayer.setChips(bestPlayer.getChips() + state.getPot());
        state.setStatus(RoomStatus.ENDED);
        state.setWinner(bestPlayer.getId());
        log.info("[{}] 玩家{} 以牌型{}赢得 {}", roomId, bestPlayer.getId(), bestRank, state.getPot());
    }

    private void broadcast(String roomId, GameMessage msg) {
        ROOM_SESSIONS.get(roomId).forEach(s -> {
            try {
                s.getAsyncRemote().sendText(JSON.toJSONString(msg));
            } catch (Exception e) { /* ignore */ }
        });
    }

    // ===== 房间状态内部类 =====
    @Data
    static class PokerRoomState {
        private String roomId;
        private List<PokerPlayer> players = new ArrayList<>();
        private List<Card> communityCards = new ArrayList<>();
        private int pot = 0;
        private int currentBet = 0;
        private int phase = 0;
        private RoomStatus status = RoomStatus.WAITING;
        private Long winner = null;

        public PokerPlayer getPlayer(Long playerId) {
            return players.stream().filter(p -> p.getId().equals(playerId)).findFirst().orElse(null);
        }

        public GameMessage toMessage() {
            GameMessage msg = new GameMessage();
            msg.setAction("state_update");
            msg.setPlayers(players);
            msg.setCommunityCards(communityCards.stream().map(Card::display).toList());
            msg.setPot(pot);
            msg.setCurrentBet(currentBet);
            msg.setPhase(phase);
            msg.setStatus(status.ordinal());
            return msg;
        }
    }

    @Data
    static class PokerPlayer {
        private Long id;
        private String nickname;
        private int chips;
        private boolean isDealer;
        private boolean isFolded;
        private boolean isAllIn;
        private List<Card> holeCards;
    }

    enum RoomStatus { WAITING, PLAYING, ENDED }
}

// ========== GameMessage.java ==========
@Data
public class GameMessage {
    private String action;
    private Long playerId;
    private Integer amount;
    private List<String> cards;
    private Integer pot;
    private Integer phase;
    private Integer status;
    private List<PokerWebSocket.PokerPlayer> players;
    private Long winner;
}

4.4 REST API 控制器


java

@RestController
@RequestMapping("/api")
@CrossOrigin
@Slf4j
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 = "R" + System.currentTimeMillis() % 100000;
        roomService.createRoom(roomNo, req.getBlindLevel(), req.getCreatorId());
        return R.ok(roomNo);
    }

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

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

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

    @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("存酒成功");
    }

    // ====== 战绩 ======
    @GetMapping("/user/stats")
    public R<UserStatsVO> getStats(@RequestHeader("Authorization") String token) {
        return R.ok(userService.getStats(JwtUtil.getUserId(token)));
    }
}

4.5 微信支付对接


java

@Service
@Slf4j
public class PayService {

    @Autowired private WeChatPayClient weChatPayClient;

    public String createRechargeOrder(Long userId, BigDecimal amount) {
        JSONObject params = new JSONObject();
        params.put("appid", "wx1234567890abcdef");
        params.put("mchid", "1234567890");
        params.put("description", "德州扑克充值-" + userId);
        params.put("out_trade_no", "RC" + 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) {
        try {
            JSONObject data = JSON.parseObject(body);
            String outTradeNo = data.getString("out_trade_no");
            String transId = data.getString("transaction_id");

            log.info("支付回调: {} {}", outTradeNo, transId);

            // 更新订单状态 + 增加筹码
            orderService.updateOrderStatus(outTradeNo, 1);
            userService.addChips(data.getString("openid"),
                data.getInteger("total_fee") / 100);

            return "{\"code\":\"SUCCESS\",\"message\":\"成功\"}";
        } catch (Exception e) {
            log.error("支付回调失败: {}", e.getMessage());
            return "{\"code\":\"FAIL\",\"message\":\"\"}";
        }
    }
}

📱 五、UniApp 小程序前端源码

5.1 ⭐ 牌桌页面(核心)


vue

<!-- pages/table/table.vue -->
<template>
  <view class="poker-table">
    <!-- 公共牌区域 -->
    <view class="community-area">
      <view class="pot">💰 底池: {{ potChips }}</view>
      <view class="cards-row">
        <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 + 'rpx', top: player.y + 'rpx' }">
      <image class="avatar" :src="player.avatar" />
      <text class="name">{{ player.nickname }}</text>
      <text class="chips">💰{{ player.chips }}</text>
      <view v-if="player.isMe" class="me-badge">我</view>
    </view>

    <!-- 我的手牌 -->
    <view class="my-cards" v-if="isMyTurn">
      <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, computed } from 'vue'
import { connectWS, sendWS } from '@/utils/ws'

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)
const isMyTurn = ref(false)

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

onMounted(() => {
  const pages = getCurrentPages()
  const cur = pages[pages.length - 1]
  roomId.value = cur.options?.roomId || ''

  ws.value = connectWS(`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 || []; isMyTurn.value = true }
    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; isMyTurn.value = msg.isMyTurn || false }
    if (msg.action === 'bet_update') { currentBet.value = msg.currentBet || 0 }
    if (msg.action === 'winner') { uni.showToast({ title: `玩家${msg.winnerName}获胜!`, icon: 'none' }) }
  }
})

const doAction = (action: string) => {
  sendWS(ws.value, {
    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: 160rpx; text-align: center; color: #fff; }
.card {
  width: 80rpx; height: 112rpx;
  background: #fff; border-radius: 12rpx;
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: bold; margin: 4rpx;
  box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.4);
}
.btn-allin {
  background: linear-gradient(135deg, #ff4444, #cc0000) !important;
  color: #fff !important; font-size: 32rpx; font-weight: bold;
}
</style>

5.2 WebSocket 封装


javascript

// utils/ws.js
let wsInstance = null

export function connectWS(url) {
  wsInstance = uni.connectSocket({ url })
  wsInstance.onOpen(() => console.log('WebSocket 已连接'))
  wsInstance.onMessage((res) => {
    if (wsInstance._onMessage) wsInstance._onMessage(res)
  })
  wsInstance.onClose(() => {
    console.log('WebSocket 已断开')
    setTimeout(() => connectWS(url), 5000) // 5秒重连
  })
  return wsInstance
}

export function sendWS(ws, data) {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send({ data: JSON.stringify(data) })
  }
}

🐳 六、Docker 一键部署

docker-compose.yml


yaml

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]

  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:

nginx.conf


nginx

server {
    listen 80;
    server_name your-domain.com;

    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://poker-server:8080/api/;
        proxy_set_header Host $host;
    }

    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";
    }
}

部署命令


bash

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

# 2. 打包小程序
cd my-poker-tavern && npm run build:mp-weixin

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

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

更多推荐