酒吧德州扑克娱乐小程序开发Java技术搭建源码案例
🍺 酒吧德州扑克娱乐小程序 — 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