德州扑克小酒馆小程序定制|Java后端开发全流程解析
·
🃏 德州扑克小酒馆小程序|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更多推荐

所有评论(0)