news 2026/9/30 3:19:37

Python pygame飞机大战:游戏循环与碰撞检测实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python pygame飞机大战:游戏循环与碰撞检测实战

1. 从一条弹幕说起:为什么我劝你先用 Python 写个飞机大战

说个真事儿。前段时间帮一个学弟看简历,他说自己"精通 Python",结果面试官让他现场写个对象在屏幕上动起来,他憋了二十分钟没写出来。问题出在哪?不是语法不会,是没做过能把语法串起来的完整项目。for循环、class、if判断这些他都懂,但一到"怎么让一架飞机从下往上飞、还能打子弹、还能撞爆炸"就卡壳了。

飞机大战这个题材,在 Python 入门圈里几乎是标配练手项目,原因很简单:它把面向对象、事件循环、坐标计算、碰撞检测、状态管理这几个核心概念全塞进了一个你能看见、能玩、有反馈的小框里。你写完它,再看 Python 基础语法,感觉完全不一样——以前是背知识点,现在是知道这些知识点在什么场景下用。

我打算用这篇文章,把这几年带人写飞机大战踩过的坑、绕过的弯、以及那些教程里从来不说的细节,一次性讲清楚。你会看到完整的实现思路、可以直接跑的代码骨架、参数怎么调、性能为什么掉帧、以及最关键的——哪些地方新手一定会写错。适合谁看?如果你能看懂def和class的区别,装好了 Python 却不知道该拿它干嘛,那这篇就是给你准备的。如果你已经写过一两个小游戏,也可以从第四节的问题排查里挑点东西走。

先说清楚技术选型。网上搜"飞机大战 Python",你会看到两个主流方向:一个是pygame,一个是tkinter。还有一部分人用cv2(OpenCV)配合鼠标操作做人机交互,但那是另一个流派,本文不做展开。我选pygame,理由是它专为 2D 游戏设计,精灵组、碰撞检测、音效播放、帧率控制这几样都是现成的,你不需要自己造轮子。用 tkinter 写也不是不行,但你得自己用after()手动跑主循环,自己画矩形当飞机,等写到碰撞检测和帧同步的时候会非常痛苦。这个选择背后的逻辑是:入门项目要让你把精力花在"游戏结构"上,而不是"怎么实现一个游戏引擎"上。

2. 动手之前:环境、目录和素材,别一上来就写代码

2.1 安装 Python 与 pygame 的正确姿势

很多人在这儿就翻车了。流程是:装 Python → 装 pygame → 建项目。Python 建议用 3.8 以上版本,3.10、3.11 都行,实际测试下来对 pygame 支持都没问题。安装时那个"Add Python to PATH"的勾,一定要打上,不然后面pip命令在终端里根本找不到,你会对着"不是内部或外部命令"发呆半小时。这是热词里"python安装教程"出现这么高频的原因,真心是第一个大坑。

装完 Python 后打开终端,敲python --version确认能输出版本号。然后装 pygame:

pip install pygame

如果你在国内,下载慢或者卡住,可以换镜像源,这个操作是常规做法:

pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple

装完验证一下,跑这段:

import pygame print(pygame.ver)

能打印出版本号就成了。如果报ModuleNotFoundError,八成是你电脑上有多个 Python,pip装到了另一个版本里。这时候用python -m pip install pygame更保险,它保证 pip 和当前 python 是同一个。

编辑器方面,VSCode 和 PyCharm 都行。VSCode 轻快,但环境配置要多一步——你得在左下角选对解释器(Python Interpreter),不然会出现"终端能跑但编辑器提示 import 报红"的情况,这也是热词里"vscode python环境配置"的由来。PyCharm 开箱体验更顺,新建项目时它会自动建虚拟环境,缺点是启动慢、占内存。我个人建议新手先 PyCharm,把环境这关过了再说。

2.2 项目目录与素材准备

目录别乱堆,按下面这样分,后面加功能的时候你会感激自己:

plane_war/ ├── main.py 主程序入口 ├── game.py 游戏逻辑 ├── settings.py 所有常量配置 └── images/ 图片素材 ├── hero.png ├── enemy.png ├── bullet.png └── background.png

为什么把常量单独抽到settings.py?因为你一定会反复调参数——敌机速度、子弹冷却、屏幕尺寸。如果这些数字散落在各个文件里,改一次你就要全局搜索一遍。集中管理之后,调难度就是改一个文件,五分钟搞定。

素材方面,不求精美,求能用。尺寸建议:主角机 60×60 像素左右,敌机 50×50,子弹 8×20,背景 480×700。你要是懒得找图,用纯色矩形也完全可以,pygame.Surface填充个颜色就能当飞机,效果丑但逻辑一点不差。我见过太多人卡在"找素材"上,找了两小时图,代码一行没写。先用方块跑通逻辑,最后再换图,这个顺序能让你少走一大半弯路。

注意:图片建议统一用 PNG 格式,它支持透明通道,不然你的飞机后面会顶着一个白方块,看起来极其廉价。JPG 不支持透明,别用。

3. 核心架构拆解:一个游戏循环加上三类对象

3.1 游戏循环到底在循环什么

几乎所有游戏的心脏都是同一个结构,飞机大战也不例外:

while True: events = pygame.event.get() # 1. 收事件 update() # 2. 更新状态 draw() # 3. 绘制画面 pygame.display.flip() # 4. 刷新屏幕

这四步理解透了,你就理解了游戏编程的本质。收事件是看玩家按了什么键、点没点关闭按钮;更新状态是让所有物体按各自速度移动一格、判断有没有撞上;绘制画面是把每个物体画到它当前坐标;刷新屏幕是把画好的这帧显示出来。每秒重复 60 次,物体就动起来了。

这里有个新手最容易忽略的点:pygame.display.flip()缺了,你会看到一片黑屏。因为所有绘制都画在内存里的一个"后台缓冲区"上,flip才是把它推到显示器上的那一下。我最早写的时候删过这一行,对着黑屏debug了四十分钟,一直以为是我图片路径写错了。

帧率控制用clock.tick(60),它有两个作用:一是锁住 60 帧,二是返回上一帧耗时。没有它,循环会以 CPU 极限速度狂跑,你的飞机可能一秒钟飞过整个屏幕。为什么是 60?因为大多数显示器刷新率就是 60Hz,超过这个数字肉眼也看不出来,白白浪费 CPU。

3.2 三类对象:我方、敌方、子弹

用面向对象来组织,是因为这三类东西有共性——都有坐标、都有图、都要移动、都要能碰撞。自然的做法是抽一个基类:

import pygame class GameObject(pygame.sprite.Sprite): def __init__(self, image_path, x, y): super().__init__() self.image = pygame.image.load(image_path) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.speed = 0 def update(self): self.rect.y += self.speed

Sprite这个基类提供了image和rect两个属性,看起来不起眼,但pygame.sprite.groupcollide()全靠这两个属性做碰撞检测,所以命名不能改。这是个隐式约定,很多教程不说,新手自己改成self.pic就发现碰撞检测永远返回空,怎么找都找不到原因。

敌机直接继承,把speed设成正数,update里 y 坐标加,它就往下飞了:

class Enemy(GameObject): def __init__(self, x, y): super().__init__('images/enemy.png', x, y) self.speed = 3

主角机要复杂点,它得响应键盘、得有开火冷却:

class Hero(GameObject): def __init__(self, x, y): super().__init__('images/hero.png', x, y) self.speed = 5 self.last_shot = 0 self.shot_interval = 200 # 毫秒 def shoot(self, bullet_group): now = pygame.time.get_ticks() if now - self.last_shot > self.shot_interval: self.last_shot = now bullet = Bullet(self.rect.centerx, self.rect.top) bullet_group.add(bullet)

这里pygame.time.get_ticks()返回的是程序启动至今的毫秒数,用它来控制射速,比用帧计数器更稳。为什么?因为如果哪天你为了让画面更流畅改成 120 帧,用帧数当冷却的话射速会直接翻倍,而用毫秒数就不会。这个细节我觉得挺重要,很多教程用frame_count % 10 == 0那种写法,一改帧率就全乱了。

3.3 用精灵组管理,而不是一堆列表

不用精灵组行不行?行,你可以把所有敌机塞进一个 list,然后自己写双层 for 循环两两判断碰撞。但坏处很直接:当你有 100 个子弹、30 个敌机的时候,你要比较 3000 次;而且遍历时删除元素还要小心index越界问题。

用精灵组就是:

enemies = pygame.sprite.Group() bullets = pygame.sprite.Group() # 子弹打到敌机,两两碰撞 hits = pygame.sprite.groupcollide(enemies, bullets, True, True)

一行代码,命中即双方移除。groupcollide内部做了矩形相交判断,还帮你处理了删除。我在带人写的时候,最常听到的感叹就是"原来还能这样"。这不是偷懒,这叫站在库的肩膀上。你该花时间理解的是游戏逻辑,而不是优化一个碰撞检测算法。

4. 完整实操:从零到能玩,每个环节的参数怎么定

4.1 搭建入口和主循环骨架

先写settings.py:

SCREEN_WIDTH = 480 SCREEN_HEIGHT = 700 FPS = 60 ENEMY_SPAWN_INTERVAL = 500 # 敌机生成间隔,毫秒 HERO_SPEED = 5 ENEMY_SPEED = 3 BULLET_SPEED = -8

屏幕用 480×700 而不是全屏,是因为这个竖版比例接近老式街机,玩起来手感最好,而且测试时窗口小,方便调试截图。敌机生成间隔 500 毫秒是个起点值,下面会讲怎么调。

主程序入口:

import pygame import sys from settings import * from game import Game def main(): pygame.init() screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption('飞机大战') clock = pygame.time.Clock() game = Game(screen) while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() game.update() game.draw() pygame.display.flip() clock.tick(FPS) if __name__ == '__main__': main()

这份代码你直接抄没问题。pygame.init()负责初始化所有子模块,忘记它会出现"字体未初始化"之类莫名其妙的报错。sys.exit()在退出前调用pygame.quit(),这是标准做法,避免进程残留。

4.2 键盘控制:为什么用 pressed 而不是按下事件

主角持续移动,靠的是按住状态检测,不是单次按键事件:

keys = pygame.key.get_pressed() if keys[pygame.K_LEFT] and self.rect.left > 0: self.rect.x -= self.speed if keys[pygame.K_RIGHT] and self.rect.right < SCREEN_WIDTH: self.rect.x += self.speed

为什么不用event.type == pygame.KEYDOWN?因为那个只在你按下瞬间触发一次,松开之前不会重复。用它写移动,你按住方向键飞机只动一格就停了,体验极差。get_pressed()返回的是"当前这一刻哪些键处于按下状态",配合每帧调用,就是连续的平滑移动。

那两个边界判断self.rect.left > 0和self.rect.right < SCREEN_WIDTH也别省。没有它们,飞机能开出屏幕外,然后你再也找不到它了。第一次遇到这种情况会特别懵,明明程序没报错,飞机就是没了。因为它的坐标还在变,只是画在了你看不见的地方。

4.3 敌机生成:定时器不是越频繁越刺激

敌机生成用定时事件:

pygame.time.set_timer(pygame.USEREVENT, ENEMY_SPAWN_INTERVAL)

然后在事件循环里:

if event.type == pygame.USEREVENT: x = random.randint(0, SCREEN_WIDTH - 50) enemy = Enemy(x, -50) enemies.add(enemy)

pygame.USEREVENT是个自定义事件的数字编号,set_timer让它每隔指定毫秒触发一次。敌机初始 y 设成 -50,是在屏幕上方外面,这样它能"飞入"画面,视觉上更自然;直接生成在 y=0 会显得很突兀,像凭空冒出来。

生成间隔的调法有个经验:500 毫秒开始,看你撑得过 30 秒就说明太慢,往下调到 350;如果十秒就死,往上加到 700。这个数值没有标准答案,取决于你屏幕高度和敌机速度。我一般会先用 500 跑一遍,感受一下"手忙脚乱但不绝望"的节奏,那个值就对了。

4.4 子弹与碰撞:命中判定的完整链路

子弹类很简单,关键在于速度方向:

class Bullet(GameObject): def __init__(self, x, y): super().__init__('images/bullet.png', x, y) self.speed = -8

速度是负数,因为它向上飞。pygame 的坐标系和数学坐标不同:左上角是原点 (0,0),y 轴向下为正。所以向上飞是 y 减小。这个坐标系差异是新手必踩的坑,你会看到有人写"子弹往上飞但速度设成正数",然后就纳闷子弹为什么往下掉。

碰撞检测每帧做一次:

def check_collisions(self): hits = pygame.sprite.groupcollide(self.enemies, self.bullets, True, True) self.score += len(hits) * 10 for enemy in pygame.sprite.spritecollide(self.hero, self.enemies, True): self.hero_alive = False

spritecollide是单个精灵和一组精灵的碰撞,用在主角撞敌机上。第二个参数True表示撞到后把敌机从组里删掉。整个链路是:生成 → 移动 → 每帧检测 → 命中移除 → 加分/死亡。理解这条链,你就能加任何东西——道具、护盾、多级子弹,无非是在这条链上插节点。

4.5 绘制顺序决定你能看见什么

绘制顺序不是随便排的,后画的东西盖在前面画的上面:

self.screen.blit(self.background, (0, 0)) # 背景 self.enemies.draw(self.screen) # 敌机 self.bullets.draw(self.screen) # 子弹 self.screen.blit(self.hero.image, self.hero.rect) # 主角 self.screen.blit(score_text, (10, 10)) # 分数,永远在最上层

把背景放第一位,是因为它是底;分数放最后,因为要一直看得见。要是你把背景放到最后画,恭喜,整个屏幕全被背景盖住,你又得到一片黑屏。这个错误和忘记flip是并列的黑屏两大元凶,别问我是怎么知道的。

5. 排查实录:那些教程里从来不写的坑

5.1 常见报错速查表

现象可能原因解决方式
黑屏但有窗口忘记display.flip()或背景画在最后检查绘制顺序,加上 flip
飞机图片后带白块用了 JPG 素材换成带透明通道的 PNG
碰撞检测永远返回空rect属性被改名保证 Sprite 子类用self.rect
ModuleNotFoundError: pygamepip 装到了别的解释器用python -m pip install pygame
报"不是内部或外部命令"安装时没勾 PATH重装并勾选 Add to PATH
敌机不动或飞出画面speed 为 0 或坐标溢出检查速度值,加边界处理
按键移动一卡一卡用了 KEYDOWN 事件改用key.get_pressed()
视频内存越跑越大每次循环都image.load素材只加载一次,复用 Surface

最后一条是我踩过最深的坑。早期我把pygame.image.load()写在生成对象的__init__里,每生成一发子弹就读一次磁盘文件。打到后期屏幕上几十发子弹,帧率直接掉到 20,硬盘灯狂闪。解决方法是加载一次,全局复用:

# 在程序启动时加载 enemy_image = pygame.image.load('images/enemy.png') class Enemy(GameObject): def __init__(self, x, y): super().__init__(enemy_image, x, y) # 传 Surface 而不是路径

内存里同一张图只有一个副本,所有敌机共享它。这个优化在游戏圈是常识,但在 Python 小游戏教程里经常被忽略,因为它不影响功能,只在帧率上悄悄惩罚你。

5.2 帧率不稳怎么查

帧率是这类项目的生命线。我习惯在左上角打印实时 FPS:

fps_text = font.render(f'FPS: {int(clock.get_fps())}', True, (255, 0, 0)) screen.blit(fps_text, (SCREEN_WIDTH - 100, 10))

clock.get_fps()返回最近一秒的平均帧率。如果它长期低于 55,就该找原因了。排查顺序我一般是这么走的:先看是不是每帧都在加载资源(上面那个坑),再看是不是精灵数量太多导致碰撞检测 O(n²) 爆炸,最后看有没有画了特别大的背景图每次都缩放。这三条基本覆盖 90% 的掉帧情况。

有一点要提醒:不要把clock.tick(FPS)放在循环开头。放在末尾,它限制的是"这一帧总共花了多少时间",放在开头会变成先等一个固定时间再干活,帧率反而更不稳。这个区别很微妙,但实测是有差别的。

5.3 退出时的那点事

程序退出前要pygame.quit(),但坑在于——如果你在游戏循环里直接sys.exit()而不先pygame.quit(),窗口有时候不会立刻关掉,会卡在那儿几百毫秒。正确顺序是先清理再退出。测试的时候如果你频繁重启程序,还可能在任务管理器里看到一堆残留的 python 进程,都是因为没正常退出。

另外,sys.exit()抛的是SystemExit异常,如果你外面包了try...except Exception,它会把这个异常吃掉导致退不出去(因为 SystemExit 继承自 BaseException 不继承 Exception,实际上不会被吃,但如果你写成except BaseException就会被吃)。这个属于进阶细节,新手了解即可,写的时候单独处理 QUIT 事件最稳。

6. 加点料:让这个项目从"能跑"变成"有意思"

6.1 难度曲线:从匀速到动态加快

固定间隔很快就会腻。加个随时间递减的生成间隔:

def get_spawn_interval(self): base = 600 elapsed = (pygame.time.get_ticks() - self.start_time) / 1000 interval = max(180, base - int(elapsed * 12)) return interval

每秒把间隔缩短 12 毫秒,最低压到 180 毫秒。这样开局轻松,越到后面越紧张,第 30 秒左右达到最难。和匀速相比,玩家的心流体验会好很多——有进步感,也有压迫感。这个"最低值兜底"很重要,不加的话到后面间隔会变成负数,敌机会瞬间满屏。

调这个曲线也看你的感觉。我个人喜欢"30 秒见真章"的节奏,你也可以把系数调大做成硬核版,或者调小做成休闲版。核心是把难度增长和游玩时长绑定,而不是一上来就拉满。

6.2 加音效和爆炸动画

音效方面,pygame.mixer一条龙。先初始化,退出时也要对应关掉:

pygame.mixer.init() shoot_sound = pygame.mixer.Sound('sounds/shoot.wav') explode_sound = pygame.mixer.Sound('sounds/explode.wav') # 触发时 shoot_sound.play()

音频文件我建议用 WAV,因为它解码延迟低,适合短促的音效;WAV 文件体积大点没关系,一个射击音效也就几十 KB。MP3 在 pygame 里播放短音效偶尔会有延迟感,体验打了折扣。这是实打实的经验——你用一个几百毫秒的枪声配上一百毫秒的播放延迟,感觉枪都打不准。

爆炸动画本质是多帧图片按顺序播放。做法是先加载一串explode_0.png到explode_5.png,然后维护一个当前帧索引,每 5 帧换一张:

self.frame_index = 0 self.frame_timer = 0 def update(self): self.frame_timer += 1 if self.frame_timer >= 5: self.frame_timer = 0 self.frame_index += 1 if self.frame_index >= len(self.frames): self.kill() # 动画放完,从组里移除 else: self.image = self.frames[self.frame_index]

self.kill()是 Sprite 自带的方法,把它从所有它所在的组里移除,比手动group.remove(obj)干净。动画放完自动回收,不会在内存里越积越多。

6.3 分数、生命和游戏结束

一个能反复玩的小游戏,必须有完整的"开始→游玩→死亡→重开"状态。我用一个状态字符串来控制:

self.state = 'running' # running / gameover def update(self): if self.state == 'gameover': keys = pygame.key.get_pressed() if keys[pygame.K_r]: self.reset() return # 正常的游戏更新逻辑...

死亡时把状态切成gameover,绘制一个"按 R 重开"的提示;按下 R 调用reset()清空所有精灵组、分数归零、状态改回running。reset()里一定要把精灵组清干净,不然重开时上一局的敌机还飘在屏幕上。这个 bug 我遇到过好几次,表现就是"重开后屏幕中间已经有一堆敌机了",排查了半天才发现是没清组。

分数用font.render渲染成 Surface,再blit到屏幕。字体初始化别忘了pygame.font.init(),虽然pygame.init()会自动调它,但单独初始化更保险。渲染文字有个性能点:别在每帧都重新 render 一样的文字,可以缓存一下,只在分数变化时才重新渲染,省下的 CPU 拿去画飞机。

6.4 代码结构再优化:事件总线

当功能越来越多,game.py会膨胀到几百行,主循环里塞满各种判断。这时候可以引入一个简单的事件总线:定义几个事件,谁触发了谁去处理,主循环只负责派发。这块属于项目做大了之后的必然演化,新手不用一开始就上。但你要有这个意识:游戏逻辑和渲染逻辑要分开,update()里不该出现screen.blit,draw()里不该改数据。分开之后,你在写测试或者调参数的时候会轻松很多。

7. 我个人在这个项目里的一些体会

写飞机大战这件事,最值钱的从来不是最后那个能飞的飞机,而是中间那些磕磕绊绊。我从最初的黑屏、白块、飞机飞出去回不来,到后来能顺手用精灵组和各种定时器,中间隔着的不是语法,是对"帧"这个概念的理解。游戏编程和写脚本最大的区别,就是你要在脑子里同时运转两条时间线:一条是每个物体的状态在变,一条是屏幕每 16 毫秒刷新一次。这个思维一旦建立,你再看别的循环类的程序,都会觉得通透。

还有一个很个人的感受:这个项目的天花板其实比看起来高。你把它当作业写完,那是及格;你把难度曲线、音效、动画、重开都做了,那是良好;如果你再把它拆成模块化的代码结构,能复用、能扩展,那就是你简历上一个真的能拿得出手的东西。很多人问"我该做什么项目练手",答案往往不是去找一个没人做过的新题材,而是把手边最普通的东西做到别人做不到的完成度。

这个项目后面其实还能继续长。你可以加道具系统,打怪掉护盾和双发子弹;可以加多种敌机,有的直线飞、有的走 S 形、有的会开火;还可以加个最简单的本地排行榜,把最高分存到文件里,下次打开还在。这些扩展每一个都不难,难的是先有那个最基础、能跑通的骨架。骨架立住了,剩下的都是往上搭积木。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:19:21

分布式事务实战:从2PC到TCC、SAGA与本地消息表的选型与落地

分布式事务这个话题&#xff0c;只要做过订单、库存、支付这类交易链路的人&#xff0c;迟早都会撞上。我最早接触它是在一单“订单创建减库存”的业务改造里&#xff0c;单体应用拆成订单服务和库存服务&#xff0c;数据库一拆&#xff0c;原本一个本地事务能搞定的事情&#…

作者头像 李华
网站建设 2026/9/30 3:19:17

Qt配置OpenCV保姆级教程:从环境搭建到图像显示

很多人把Qt和OpenCV配在一起&#xff0c;是想快速做一个带界面的图像处理小工具。思路没问题&#xff0c;但真正动手的时候&#xff0c;光一个版本匹配问题就能劝退一半人。我见过不少朋友卡在“OpenCV下载好了、Qt也装完了&#xff0c;但在.pro里一写路径就报错”这一步&#…

作者头像 李华
网站建设 2026/9/30 3:19:09

网络安全系统运维方案实战:连通性、性能与监控管理落地指南

简介&#xff1a;这份文档资料面向企业IT运维人员、网络管理员及安全服务从业者&#xff0c;提供一套完整的网络安全系统运维服务方案&#xff0c;帮助解决网络连通性、性能与监控管理三大核心运维难题。资源包共1个doc文件&#xff0c;约63KB&#xff0c;内容以方案文本与作业…

作者头像 李华
网站建设 2026/9/30 3:18:51

运维转网安是技能树分叉:优势、岗位选择与落地路线

干了这么多年运维&#xff0c;身边转行的同事一抓一大把&#xff0c;有的去做云计算&#xff0c;有的去做DevOps&#xff0c;但聊下来转得最顺、反馈最好的&#xff0c;十个里有七八个都去了网络安全方向。一开始我也挺纳闷&#xff0c;运维和网安虽说都沾个“网”字&#xff0…

作者头像 李华
网站建设 2026/9/30 3:18:50

TCP与Socket编程实战:三次握手、粘包分帧与C语言示例

如果你准备进入网络编程&#xff0c;第一个绕不过去的概念组合一定是 TCP 和 Socket。我在网上搜过、也踩过坑&#xff0c;更见过不少新人的同一种翻车模式&#xff1a;三次握手背得滚瓜烂熟&#xff0c;真到写代码时 bind 和 listen 的顺序搞反&#xff0c;recv 返回 0 不知道…

作者头像 李华
网站建设 2026/9/30 3:18:47

Spring Boot 会员制医疗预约系统开发实战:从架构到踩坑

上次因为一个会员制医疗预约系统的毕业设计&#xff0c;我在实验室连续肝了三个星期。说实话&#xff0c;这类题目看起来平平无奇&#xff0c;做起来却能把 Spring Boot 全家桶里最常碰的东西全部摸一遍&#xff1a;REST API、Spring Security、JWT、Caffeine 缓存、WebSocket …

作者头像 李华