🃏 德州扑克小酒馆小程序|Java后端开发全流程解析

2026年5月最新实战方案 · SpringBoot + UniApp + WebSocket 全栈实现


📋 全流程总览(7大阶段)

阶段 内容 工期 核心产出
P1 需求分析 + 架构设计 功能拆解、技术选型、接口定义 2天 需求文档 + 架构图
P2 数据库设计 + 环境搭建 建表、实体类、项目初始化 1天 12张核心表 + 启动项目
P3 核心业务开发 牌局引擎、WebSocket、点餐系统 5天 完整后端API
P4 小程序前端开发 UniApp页面 + 实时通信 4天 可运行小程序
P5 管理后台开发 Vue3 + Element Plus 3天 运营管理系统
P6 联调测试 功能测试 + 性能测试 2天 测试报告
P7 部署上线 Docker + Nginx + 微信审核 1天 正式上线
合计 约18个工作日

🏗️ P1:需求分析 + 架构设计

1.1 核心功能矩阵


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

1.2 运营亮点(参考市场需求)

根据2026年酒馆运营趋势,重点实现:

运营功能 说明 价值
✅ 智能预约 线上一键订桌,时段/人数精准锁定 前台效率翻3倍
✅ 专属存分 牌局积分自动存储,跨场次累计,兑换酒水/免单 锁客留客
✅ 桌边点餐 扫码点单同步后厨,不中断牌局 翻台率↑ 体验↑
✅ 抽位置 线上随机抽座位,解决等人烦恼 便捷性↑
✅ 胜率计算器 输入手牌实时计算胜率 辅助决策

1.3 技术架构


                    ┌──────────────┐
                    │  微信小程序   │ ← UniApp (Vue3)
                    │  (用户端)    │
                    └──────┬───────┘
                           │ HTTPS / WSS
                    ┌──────▼───────┐
                    │   Nginx      │ ← 反向代理 + SSL + WebSocket
                    └──────┬───────┘
                           │
          ┌────────────────┼────────────────┐
          │                │                │
   ┌──────▼──────┐  ┌──────▼──────┐  ┌──────▼──────┐
   │ Game Server │  │ Order Server│  │ User Server │
   │  (牌局引擎)  │  │  (点餐消费)  │  │  (用户管理)  │
   │  :8080      │  │  :8080      │  │  :8080      │
   └──────┬──────┘  └──────┬──────┘  └──────┬──────┘
          │                │                │
   ┌──────▼────────────────▼────────────────▼──────┐
   │              SpringBoot 2.7+                    │
   │   MyBatis-Plus + Redis + WebSocket + JWT       │
   └──────────────────────┬─────────────────────────┘
                          │
          ┌───────────────┼───────────────┐
          │               │               │
   ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
   │   MySQL 8   │ │  Redis 7    │ │  微信支付SDK │
   │  (业务数据)  │ │ (缓存/实时)  │ │  (充值/点餐) │
   └─────────────┘ └─────────────┘ └─────────────┘

💾 P2:数据库设计(12张核心表)

2.1 完整建表SQL


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 '' COMMENT '昵称',
  `avatar` VARCHAR(255) DEFAULT '' COMMENT '头像URL',
  `phone` VARCHAR(20) DEFAULT '' COMMENT '手机号',
  `balance` DECIMAL(10,2) DEFAULT 0.00 COMMENT '余额',
  `chips` INT DEFAULT 10000 COMMENT '游戏筹码',
  `level` INT DEFAULT 1 COMMENT '会员等级 1-10',
  `total_profit` DECIMAL(10,2) DEFAULT 0.00 COMMENT '总盈亏',
  `total_hands` INT DEFAULT 0 COMMENT '总局数',
  `win_count` INT DEFAULT 0 COMMENT '胜局数',
  `status` TINYINT DEFAULT 1 COMMENT '0禁用 1正常',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX `idx_openid` (`openid`)
) ENGINE=InnoDB COMMENT='用户表';

-- ------------------------------------------------
-- 2. 游戏房间表
-- ------------------------------------------------
CREATE TABLE `poker_room` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `room_no` VARCHAR(20) UNIQUE COMMENT '房间号 如 ROOM2026051501',
  `blind_level` INT DEFAULT 1 COMMENT '盲注等级 1/2 2/4 5/10',
  `small_blind` INT DEFAULT 1 COMMENT '小盲注',
  `big_blind` INT DEFAULT 2 COMMENT '大盲注',
  `max_players` INT DEFAULT 9 COMMENT '最大人数',
  `status` TINYINT DEFAULT 0 COMMENT '0等待中 1游戏中 2已结束',
  `creator_id` BIGINT NOT NULL COMMENT '创建者ID',
  `current_bet` INT DEFAULT 0 COMMENT '当前最高注',
  `pot` INT DEFAULT 0 COMMENT '底池',
  `phase` TINYINT DEFAULT 0 COMMENT '0翻前 1翻牌 2转牌 3河牌',
  `community_cards` JSON COMMENT '公共牌 JSON数组',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `started_at` DATETIME DEFAULT NULL,
  `ended_at` DATETIME DEFAULT NULL,
  INDEX `idx_status` (`status`),
  INDEX `idx_creator` (`creator_id`)
) ENGINE=InnoDB COMMENT='游戏房间表';

-- ------------------------------------------------
-- 3. 房间玩家关联表
-- ------------------------------------------------
CREATE TABLE `room_player` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `room_id` BIGINT NOT NULL,
  `user_id` BIGINT NOT NULL,
  `seat_no` INT DEFAULT 0 COMMENT '座位号 0-8',
  `chips` INT DEFAULT 10000 COMMENT '入座筹码',
  `is_dealer` TINYINT DEFAULT 0 COMMENT '是否庄家',
  `is_folded` TINYINT DEFAULT 0 COMMENT '是否弃牌',
  `is_all_in` TINYINT DEFAULT 0 COMMENT '是否全押',
  `hole_cards` JSON COMMENT '底牌 ["AH","KH"]',
  `joined_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY `uk_room_seat` (`room_id`, `seat_no`),
  INDEX `idx_user` (`user_id`)
) ENGINE=InnoDB COMMENT='房间玩家表';

-- ------------------------------------------------
-- 4. 牌局记录表
-- ------------------------------------------------
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 COMMENT '赢得筹码',
  `is_winner` TINYINT DEFAULT 0 COMMENT '是否赢家',
  `hand_no` INT DEFAULT 0 COMMENT '第几手牌',
  `played_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX `idx_room` (`room_id`),
  INDEX `idx_player` (`player_id`)
) ENGINE=InnoDB COMMENT='牌局记录表';

-- ------------------------------------------------
-- 5. 菜单/商品表(小酒馆消费)
-- ------------------------------------------------
CREATE TABLE `menu_item` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `name` VARCHAR(100) NOT NULL COMMENT '商品名',
  `category` VARCHAR(20) DEFAULT '酒水' COMMENT '分类:酒水/小吃/套餐',
  `price` DECIMAL(10,2) NOT NULL COMMENT '价格',
  `image` VARCHAR(255) DEFAULT '' COMMENT '图片URL',
  `stock` INT DEFAULT 999 COMMENT '库存',
  `status` TINYINT DEFAULT 1 COMMENT '0下架 1上架',
  `sort_order` INT DEFAULT 0 COMMENT '排序',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB COMMENT='菜单表';

-- ------------------------------------------------
-- 6. 点餐订单表
-- ------------------------------------------------
CREATE TABLE `dinner_order` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `order_no` VARCHAR(32) UNIQUE COMMENT '订单号',
  `user_id` BIGINT NOT NULL,
  `table_no` VARCHAR(10) COMMENT '桌台号',
  `items` JSON NOT NULL COMMENT '菜品列表 [{"id":1,"name":"啤酒","qty":2,"price":30}]',
  `total_amount` DECIMAL(10,2) NOT NULL COMMENT '总金额',
  `status` TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已完成 3已取消',
  `pay_time` DATETIME DEFAULT NULL,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX `idx_user` (`user_id`),
  INDEX `idx_status` (`status`)
) ENGINE=InnoDB COMMENT='点餐订单表';

-- ------------------------------------------------
-- 7. 存酒记录表
-- ------------------------------------------------
CREATE TABLE `wine_storage` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `user_id` BIGINT NOT NULL,
  `wine_name` VARCHAR(100) NOT NULL COMMENT '酒名',
  `quantity` INT DEFAULT 1 COMMENT '瓶数',
  `store_date` DATE NOT NULL COMMENT '存入日期',
  `status` TINYINT DEFAULT 0 COMMENT '0存放中 1已取走',
  `pickup_date` DATE DEFAULT NULL COMMENT '取走日期',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX `idx_user` (`user_id`)
) ENGINE=InnoDB COMMENT='存酒记录表';

-- ------------------------------------------------
-- 8. 充值记录表
-- ------------------------------------------------
CREATE TABLE `recharge_log` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `user_id` BIGINT NOT NULL,
  `amount` DECIMAL(10,2) NOT NULL COMMENT '充值金额',
  `chips_add` INT NOT NULL COMMENT '增加筹码数',
  `pay_type` VARCHAR(20) DEFAULT 'wechat' COMMENT '支付方式',
  `trans_id` VARCHAR(64) COMMENT '微信支付单号',
  `status` TINYINT DEFAULT 0 COMMENT '0待支付 1成功 2失败',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX `idx_user` (`user_id`)
) ENGINE=InnoDB COMMENT='充值记录表';

-- ------------------------------------------------
-- 9. 系统配置表
-- ------------------------------------------------
CREATE TABLE `sys_config` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `config_key` VARCHAR(50) UNIQUE NOT NULL,
  `config_value` TEXT,
  `remark` VARCHAR(200) DEFAULT '',
  `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB COMMENT='系统配置表';

-- 初始化配置
INSERT INTO `sys_config` (`config_key`, `config_value`, `remark`) VALUES
('appid', 'wx1234567890abcdef', '微信小程序AppID'),
('mch_id', '1234567890', '微信支付商户号'),
('min_recharge', '10.00', '最低充值金额'),
('max_recharge', '10000.00', '最高充值金额');

🎮 P3:核心业务开发(Java后端)

3.1 项目骨架


bash

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

cd poker-server

3.2 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 + MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>3.5.5</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </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>

    <!-- JWT认证 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.3</version>
    </dependency>

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

3.3 ⭐ 扑克牌核心类


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

    // 转为显示字符串
    public String display() {
        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);
    }

    // 发n张牌
    public List<Card> deal(List<Card> deck, int n) {
        return deck.subList(0, n);
    }
}

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


java

@Component
public class PokerHandEvaluator {

    /**
     * 从7张牌(2底牌+5公共牌)中选出最好的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;
    }

    /** 判定5张牌的牌型 */
    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;
    }

    /** 检查是否为顺子(含A-2-3-4-5特殊顺子) */
    private boolean checkStraight(List<Card> cards) {
        List<Integer> vals = cards.stream()
            .map(Card::getRank).map(Card.Rank::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;
    }

    /** 从n张牌中选k张的所有组合 */
    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);
        }
    }
}

3.5 ⭐⭐⭐ WebSocket 牌局实时同步


java

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

    // 房间ID → 在线Session映射
    private static final Map<String, Set<Session>> ROOM_SESSIONS = new ConcurrentHashMap<>();
    // 房间ID → 牌局状态
    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());
        log.info("[{}] 玩家{} 下注 {}", roomId, msg.getPlayerId(), msg.getAmount());
    }

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

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

        // 只剩1人 → 直接获胜
        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());

        // 落库牌局记录
        saveHandRecord(roomId, state);
    }

    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) { /* 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; // 0翻前 1翻牌 2转牌 3河牌
        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 }
}

java

// ========== GameMessage.java 统一消息格式 ==========
@Data
public class GameMessage {
    private String action;        // bet/fold/call/raise/all_in/deal/state_update
    private Long playerId;
    private Integer amount;       // 下注金额
    private List<String> cards;   // 公共牌显示
    private Integer pot;          // 底池
    private Integer phase;        // 0翻前 1翻牌 2转牌 3河牌
    private Integer status;       // 0等待 1游戏中 2结束
    private List<PokerWebSocket.PokerPlayer> players;
    private Long winner;
}

3.6 REST API 控制器


java

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

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

    // ====== 房间管理 ======

    @PostMapping("/room/create")
    public R<String> createRoom(@RequestBody CreateRoomReq req, @RequestHeader("Authorization") String token) {
        Long userId = JwtUtil.getUserId(token);
        String roomNo = "R" + System.currentTimeMillis() % 100000;
        roomService.createRoom(roomNo, req.getBlindLevel(), userId);
        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,
                              @RequestHeader("Authorization") String token) {
        Long userId = JwtUtil.getUserId(token);
        roomService.joinRoom(roomNo, userId, req.getChips());
        return R.ok("加入成功");
    }

    // ====== 用户 ======

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

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

    // ====== 点餐 ======

    @PostMapping("/order/create")
    public R<String> createOrder(@RequestBody OrderReq req,
                                 @RequestHeader("Authorization") String token) {
        Long userId = JwtUtil.getUserId(token);
        orderService.createOrder(userId, req);
        return R.ok("下单成功");
    }

    @PostMapping("/order/pay")
    public R<String> payOrder(@RequestBody PayReq req,
                              @RequestHeader("Authorization") String token) {
        Long userId = JwtUtil.getUserId(token);
        payService.payOrder(userId, req.getOrderId());
        return R.ok("支付成功");
    }

    // ====== 存酒 ======

    @PostMapping("/wine/store")
    public R<String> storeWine(@RequestBody WineReq req,
                               @RequestHeader("Authorization") String token) {
        Long userId = JwtUtil.getUserId(token);
        orderService.storeWine(userId, req);
        return R.ok("存酒成功");
    }

    // ====== 战绩 ======

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

3.7 微信支付对接


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 {
            // 1. 验证签名
            // 2. 更新订单状态
            // 3. 增加用户筹码
            JSONObject data = JSON.parseObject(body);
            String outTradeNo = data.getString("out_trade_no");
            String transId = data.getString("transaction_id");

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

            // TODO: 更新数据库

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

3.8 AOP + 拦截器(安全层)


java

/** JWT拦截器 */
@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("Authorization");
        if (StringUtils.isBlank(token) || !JwtUtil.verify(token)) {
            throw new UnauthorizedException("未登录或Token已过期");
        }
        return true;
    }
}

/** 全局异常处理 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public R<Void> handleBusiness(BusinessException e) {
        return R.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public R<Void> handleException(Exception e) {
        log.error("系统异常: ", e);
        return R.error(500, "系统繁忙,请稍后重试");
    }
}

/** 请求日志AOP */
@Aspect
@Component
public class LogAspect {
    @Around("execution(* com.poker.tavern.controller.*.*(..))")
    public Object logTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        log.info("[{}] 耗时: {}ms", pjp.getSignature().getName(),
            System.currentTimeMillis() - start);
        return result;
    }
}

📱 P4: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状态管理
├── utils/
│   └── ws.js           # WebSocket封装
├── 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-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-tag">{{ 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.isDealer" class="dealer-badge">D</view>
      <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>

4.4 WebSocket 封装


javascript

// utils/ws.js
let wsInstance = null

export function connectWS(url) {
  wsInstance = uni.connectSocket({ url, success: () => console.log('WS连接中...') })

  wsInstance.onOpen(() => {
    console.log('WebSocket 已连接')
  })

  wsInstance.onMessage((res) => {
    // 交给页面处理
    if (wsInstance._onMessage) wsInstance._onMessage(res)
  })

  wsInstance.onClose(() => {
    console.log('WebSocket 已断开')
    // 5秒重连
    setTimeout(() => connectWS(url), 5000)
  })

  return wsInstance
}

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

🐳 P7: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]
    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:

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;
        proxy_set_header X-Real-IP $remote_addr;
    }

    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

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

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

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

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

更多推荐