Godot 2D游戏战斗系统实战:状态机、敌人AI与血条实现

发布时间:2026/10/18 23:44:56

Godot 2D游戏战斗系统实战:状态机、敌人AI与血条实现 1. 系列回顾与本节目标没想到这个系列这么快就写到第5篇了。前四篇我们把Godot 2D游戏开发的地基打了一遍安装Godot编辑器、理解场景树和节点概念、用GDScript写最简单的打印脚本、给一个方块物体加上键盘移动控制。如果你是从系列第一篇一直跟下来的此刻应该已经能拼出一个能跑的“小方块”在窗口中用方向键或WASD来回溜达。但说实话能跑的方块只是一个空壳。真正的游戏感来自“交互”——角色要能攻击、会受伤敌人要有自己的行为界面要反馈血量变化。这些都是从“demo”到“游戏”之间的关键一步。所以这一篇我们集中解决一个问题如何给2D游戏加入一套完整的“战斗交互循环”包括玩家状态机、敌人AI、碰撞伤害、UI血条。本篇适合三类人已经会Godot基本操作但想做第一个完整玩法的新手看过很多教程但一直卡在“角色和敌人怎么互动”的读者以及想从模仿到独立设计的进阶者。我会从项目搭建到代码实现一步步走代码全部用GDScript版本基于Godot 4.x但大部分概念同样适用于Godot 3.x只是少量API有差异。另外本文不会讲太多基础概念如果你还没搞明白节点和信号建议先回头补一下系列第二篇和第三篇。现在我们直接开始。2. 项目搭建与素材准备2.1 新建项目这次用“角色分场景”结构先创建一个新的Godot项目渲染器选择GL Compatibility兼容性好后面做像素风也不卡。项目名就叫“Godot2D_BattleDemo”路径随意。创建完成后我们先在项目设置 - 主场景里暂时不设置任何东西因为后面要建多个场景。这个示例里我们会用到三个自定义场景Player玩家、Enemy敌人、HUD界面。把它们分开做独立场景是Godot的推荐做法好处是每个游戏对象低耦合、方便复用比如以后要加第二个敌人直接实例化Enemy场景即可不用重新搭节点。2.2 用“ColorRect CollisionShape2D”搭骨架不纠结美术很多同学一上来就想找美术素材结果先被素材问题卡住。其实开发阶段完全可以用占位图形。我这里用ColorRect作为角色的视觉主体再加一个CollisionShape2D定义碰撞区域。为什么不直接用内置的Sprite2D因为ColorRect不需要导入图片颜色调一下就能区分对象。等逻辑跑通了再把它替换成AnimatedSprite2D或Sprite2D碰撞形状不会受到影响。具体操作Player场景根节点用CharacterBody2D命名Player添加子节点ColorRectColor设为绿色Size设置为16 x 24模拟一个人形大小。CollisionShape2DShape子项新建RectangleShape2D尺寸设为16 x 24。再加一个Node节点命名为States用来放状态机后面用。再加一个Sprite2D默认没有纹理可以先隐藏后续美术到位后启用。Enemy场景根节点同样用CharacterBody2D命名Enemy添加子节点ColorRectColor设为红色Size设置为16 x 24。CollisionShape2DRectangleShape2D尺寸16 x 24。再加一个DetectionArea子节点类型Area2D用于检测玩家是否进入视野。在DetectionArea下添加一个CollisionShape2D用CircleShape2D半径设置为72像素视情况调整。2.3 设置物理层让玩家和敌人各归各位Godot的2D物理分为32个层默认都是第1层。为了让玩家的攻击只伤害敌人、敌人的攻击只伤害玩家我们需要划分物理层。打开项目设置 - 图层命名 - 2D物理层给前两层命名第1层player第2层enemy然后在Player的CollisionShape2D所属的CharacterBody2D节点上把Collision Layer勾选为第1层Collision Mask勾选为第2层这样玩家能检测到敌人的碰撞。Enemy节点则反过来层为第2层掩码为第1层。这里有一个新手容易踩的坑Collision Layer表示“我属于哪一层”Mask表示“我会和哪一层发生碰撞”。有些同学把两个值设成一样结果玩家和玩家自己互相碰撞或者敌人之间互相推挤。在示例中如果希望敌人之间可以相互穿过就把Enemy的Mask也设为只勾选player层如果希望敌人之间也有物理阻挡那就把enemy层也勾上。提示用Mask和Layer相互隔离碰撞是2D游戏中对象交互设计的第一步。合理规划层后面做子弹、道具、NPC都会很省心。3. 玩家状态机从“会动”到“会演”3.1 为什么需要状态机而不是一堆if很多刚接触开发的同学会把所有行为写在一个_physics_process里用一大串布尔变量控制is_attacking、is_damaged、is_dead。当时跑起来没问题但一旦玩法变复杂布尔变量之间的组合会指数膨胀比如攻击时能不能移动受伤时攻击是否被打断死亡后还能不能控制用if硬写读代码就像解毛线团。状态机把“角色处于什么状态”和“状态之间怎么切换”显式化。每个状态有自己的enter、exit、update逻辑互不干扰。我们为玩家设计四个状态move、attack、hurt、dead。每次只能处于一个状态。状态机本身不需要做成插件或复杂框架就用一个枚举加一个当前状态方法就够了。下面是我在项目里实际使用的写法很简洁适合教程。3.2 玩家场景脚本基础变量与移动在Player场景的根节点上挂载脚本player.gd先定义变量extends CharacterBody2D enum State { MOVE, ATTACK, HURT, DEAD } var speed: float 140.0 var max_health: float 100.0 var current_health: float 100.0 var attack_damage: float 10.0 var attack_cooldown: float 0.5 var attack_timer: float 0.0 var hurt_invincible_time: float 0.5 # 受伤后的无敌时间 var invincible_timer: float 0.0 var current_state: State State.MOVE var facing_direction: Vector2 Vector2.RIGHT onready var states_node: Node $States onready var collision_shape: CollisionShape2D $CollisionShape2Dfacing_direction用于记录角色面向的方向因为2D游戏攻击方向通常不单独旋转而是取最后的移动方向。这里默认向右。然后在_physics_process写状态分发func _physics_process(delta: float) - void: invincible_timer max(0.0, invincible_timer - delta) attack_timer max(0.0, attack_timer - delta) match current_state: State.MOVE: handle_move_state(delta) State.ATTACK: handle_attack_state(delta) State.HURT: handle_hurt_state(delta) State.DEAD: handle_dead_state(delta)注意此处的move状态同时要处理移动输入和攻击输入。如果玩家按下攻击键就立刻进入攻击状态攻击结束后如果没有受伤、死亡就回到move。3.3 移动状态输入与动画方向handle_move_state里读取水平虚拟轴和垂直虚拟轴用Input.get_vector更方便func handle_move_state(_delta: float) - void: var input_dir : Input.get_vector(ui_left, ui_right, ui_up, ui_down) if input_dir ! Vector2.ZERO: velocity input_dir * speed facing_direction input_dir.normalized() else: velocity Vector2.ZERO if Input.is_action_just_pressed(ui_accept): change_state(State.ATTACK) return move_and_slide()这里暂时用了Godot默认的ui_left/right/up/down/accept动作映射。你也可以在项目设置 - 输入映射里自定义一套更顺手的键位比如J键攻击。不过我建议直接使用标准映射先跑通避免键位设置干扰排查问题。move_and_slide()是CharacterBody2D的核心它会根据velocity自动移动同时处理与其它物理体的碰撞。如果角色被敌人挤住也是靠这个函数保证不会被撞进墙里。3.4 攻击状态冷却、动画与击中判定攻击不能按住就连续触发否则游戏平衡性无从谈起。这里用一个简单的冷却机制进入攻击状态时如果在攻击冷却期内就直接回到move状态否则记录攻击开始并启动一个0.2秒的攻击判定“窗口”。func handle_attack_state(delta: float) - void: velocity Vector2.ZERO move_and_slide() attack_timer attack_cooldown # 攻击持续时间内检测命中 if not attack_damage_applied: apply_attack_damage() attack_damage_applied true if state_time 0.3: change_state(State.MOVE)这里出现了两个新变量state_time和attack_damage_applied。state_time在进入新状态时清零每次_physics_process累计delta。attack_damage_applied用来确保一次攻击只造成一次伤害不会因为在同一个攻击状态下每帧都检测而重复扣血。为了计算攻击是否击中我在玩家场景下额外添加了一个AttackArea节点类型为Area2D并在其下添加碰撞形状。攻击时将这个Area2D激活并且根据facing_direction把它放到角色的正面方向。通过get_overlapping_areas()或get_overlapping_bodies()来检测敌人。注意Area2D默认检测的是其他Area2D和PhysicsBody2D检测模式通过monitoring true开启。我们在敌人身上也放置了Area2D用于检测玩家但攻击检测用的却是玩家Area2D要检测敌人的Body所以需要将AttackArea的Collision Mask设置为enemy层。为了让代码更清晰把攻击命中逻辑单独放到一个方法中func apply_attack_damage(): var attack_area: Area2D $AttackArea attack_area.rotation 0 attack_area.position facing_direction * 18 # 放在角色前方 attack_area.monitoring true for body in attack_area.get_overlapping_bodies(): if body.has_method(take_damage): body.take_damage(attack_damage, facing_direction) # 关闭攻击判定等待下一次攻击再开启 attack_area.monitoring false上述代码中两个细节很关键attack_area.position每帧根据朝向调整保证攻击判定往前放而不是跟随角色中心monitoring在攻击时开启、攻击结束后立即关闭避免持续判定。实际项目中更精细的做法是用physics frame的_on_attack_hit信号做延迟开关。但示例里直接在函数内操作简单够用。3.5 受伤与死亡当敌人攻击打到玩家时我们会调用take_damage。玩家受伤处理func take_damage(amount: float, knockback: Vector2 Vector2.ZERO) - void: if invincible_timer 0.0 or current_state State.DEAD: return current_health - amount invincible_timer hurt_invincible_time if current_health 0.0: change_state(State.DEAD) else: change_state(State.HURT) velocity knockback * 200.0 move_and_slide()无敌时间的存在非常必要。如果没有无敌帧玩家一个0.5秒的攻击动画持续碰撞敌人可能会被秒杀。在受伤状态下我们用0.4秒的HURT状态展示击退效果然后回到移动状态。hurt状态下func handle_hurt_state(delta: float) - void: state_time delta velocity velocity.lerp(Vector2.ZERO, delta * 10.0) move_and_slide() if state_time 0.4: change_state(State.MOVE)dead状态下最简单处理是让角色消失或变灰并禁用碰撞形状然后显示“游戏结束”UI。这里为了保持教程聚焦先在玩家死亡时自动调用queue_free()作为临时方案完整UI流程放在第五部分。心得状态机写好后加跳跃、蹲下、冲刺都不需要大改只需要新增一个状态枚举值并实现对应处理函数即可。这是值得刻意练习的设计思维。4. 敌人AI巡逻、追击与攻击4.1 敌人行为模式从简单巡逻开始敌人的行为通常比玩家简单但也不能像个木桩站着挨打。这个教程里我们实现三个状态的简单敌人状态机patrol巡逻、chase追击、attack攻击。没有专门做hurt和dead因为敌人被打到后只需扣血死亡时消失。敌人脚本enemy.gd的核心逻辑extends CharacterBody2D enum EnemyState { PATROL, CHASE, ATTACK } var enemy_state: EnemyState EnemyState.PATROL var patrol_left_limit: float 0.0 var patrol_right_limit: float 0.0 var base_position: Vector2 var patrol_speed: float 60.0 var chase_speed: float 100.0 var move_direction: int 1 # 1向右-1向左 var health: float 30.0 var attack_cooldown: float 1.0 var attack_timer: float 0.0 onready var player: CharacterBody2D get_node(../Player)注意base_position保存敌人生成后的初始位置巡逻左右边界可以基于这个位置偏移。这个设计比硬编码坐标灵活因为场景中放置的敌人可能在不同位置我们只需要在初始化时根据预设偏移算出边界即可。在_ready中func _ready() - void: base_position global_position patrol_left_limit base_position.x - 80 patrol_right_limit base_position.x 80身体移动的_physics_process同样用match分发。巡逻状态是最简单的倒走逻辑func handle_patrol_state(_delta: float) - void: velocity Vector2(move_direction * patrol_speed, 0) move_and_slide() if global_position.x patrol_left_limit: move_direction 1 elif global_position.x patrol_right_limit: move_direction -1 if can_see_player(): enemy_state EnemyState.CHASE4.2 用Area2D做视野检测上面can_see_player()如何实现我用了敌人身上的DetectionAreaArea2D来做视野范围。更真实的AI还会加射线检测挡墙但为了教程控制复杂度先只做圆形范围内的检测。func can_see_player() - bool: var likely_player get_tree().get_first_node_in_group(player) if likely_player null: return false detection_area.position Vector2.ZERO detection_area.monitorable true detection_area.monitoring true var bodies detection_area.get_overlapping_bodies() for body in bodies: if body.has_method(is_player): return true return false这里有个技巧我把玩家节点放到了player组里。只需在Player场景的根节点上勾选“Groups”并添加player组。然后get_tree().get_first_node_in_group(player)可以快速拿到玩家实例不需要手动从场景树里找路径。这比get_node(../Player)更健壮因为无论玩家在哪里只要在场景树内都能找到。is_player方法在玩家脚本里简单返回true作为身份标记。但这样写有个小问题每次帧循环都去get_overlapping_bodies()如果玩家距离敌人很远可能还是会有性能开销。实际项目中通常用DetectionArea的body_entered信号在进入时改变状态而不是每帧查询。不过为了示例清晰先每帧轮询。如果你希望敌人只能看到前方一定扇形区域的玩家可以用多个RayCast2D或一个ReflectionProbe2D之类的方案。更精细的做法是使用RayCast2D检测视线是否被阻挡但那是另一个复杂度层级。建议先把圆形视野跑通再按需加射线。4.3 追击与攻击触发的实现追击状态func handle_chase_state(_delta: float) - void: if player null: enemy_state EnemyState.PATROL return var direction_to_player (player.global_position - global_position).normalized() velocity direction_to_player * chase_speed move_and_slide() var distance global_position.distance_to(player.global_position) if distance 24.0: enemy_state EnemyState.ATTACK attack_timer attack_cooldown攻击状态func handle_attack_state(delta: float) - void: velocity Vector2.ZERO move_and_slide() attack_timer - delta if attack_timer 0.0: if player ! null and player.has_method(take_damage): player.take_damage(5.0, (player.global_position - global_position).normalized()) attack_timer attack_cooldown # 如果玩家跑出范围切回追击 var distance global_position.distance_to(player.global_position) if distance 48.0: enemy_state EnemyState.CHASE这里的攻击模式下敌人会原地停顿一下并输出伤害。如果玩家走远了就切回追击。敌人攻击没有无敌帧所以如果玩家贴脸持续攻击敌人也会持续扣血直到死亡。4.4 敌人死亡与掉落敌人在take_damage方法中简化处理func take_damage(amount: float, _knockback: Vector2 Vector2.ZERO) - void: health - amount if health 0: queue_free()这里有一个常见坑如果玩家在攻击状态中连续调用take_damage由于attack_damage_applied标志位在攻击中只置为一次所以不会重复。但如果你直接调用queue_free()get_overlapping_bodies()在同一个帧内还有可能拿到这个已经标记为释放的节点导致奇怪的报错。解决办法是在敌人死亡后先禁用碰撞形状再使用queue_free()。更稳妥的写法func die() - void: collision_shape.set_deferred(disabled, true) queue_free()set_deferred将属性设置延迟到当前帧结束时执行避免在物理回调过程中直接修改碰撞状态造成冲突。这个小细节在很多教程里不会提实践中最容易踩。5. UI血条与血量信号5.1 创建HUD场景TextureProgressBar还是ProgressBarGodot内置的ProgressBar可以直接用但为了风格统一我用TextureProgressBar。没有美术资源时可以用ProgressBar后续替换成纹理即可。新建一个HUD场景根节点为CanvasLayer命名为HUD。CanvasLayer的作用是让UI永远渲染在游戏画面之上不随2D相机移动。在HUD下添加一个PanelContainer作为血条背景固定在屏幕左上角。一个ProgressBar命名为HealthBar设置Min Value0Max Value100初始Value100。一个Label命名为HealthLabel显示“100/100”。一个Label命名为GameOverLabel初始隐藏用来显示游戏结束文字。给HUD根节点挂一个脚本hud.gdextends CanvasLayer onready var health_bar: ProgressBar $MarginContainer/HealthBar onready var health_label: Label $MarginContainer/HealthLabel onready var game_over_label: Label $CenterContainer/GameOverLabel func update_health(current: float, max_value: float) - void: health_bar.max_value max_value health_bar.value current health_label.text str(int(current)) / str(int(max_value)) func show_game_over() - void: game_over_label.visible true这个脚本不直接引用玩家而是通过外部调用更新。这样职责清晰HUD只负责展示不关心数据来源。5.2 用信号连接玩家血量在玩家脚本中我们定义一个信号health_changedsignal health_changed(current: float, max: float)在take_damage里调用health_changed.emit(current_health, max_health)在_ready里如果场景包含HUD节点可以手动连接。最简单的方式其实是给玩家脚本增加一个onready引用HUD但那样耦合度高。更好的做法是在主场景中连接在main.tscn中把Player和HUD都实例化然后为Player的health_changed信号连接到HUD的update_health方法。操作流程新建main.tscn根节点Node2D。把Player场景作为子节点放入。把Enemy场景放入位置放在Player旁边。把HUD场景放入。在Main的场景树里选中Player打开“节点”标签页双击health_changed信号选择目标节点为HUD方法选择update_health。这样玩家每次受伤都会自动调用HUD的更新函数。5.3 游戏结束流程当玩家take_damage时扣血到0进入死亡状态我们需要显示游戏结束文字。为了简单起见可以给玩家定义一个信号player_died在死亡状态时发出并连接到HUD的show_game_over。在玩家脚本中signal player_died func enter_dead_state() - void: change_state(State.DEAD) player_died.emit()然后在主场景中连接信号到HUD的show_game_over。这样一个最小可玩的战斗循环就完整了玩家移动→攻击敌人敌人巡逻→发现玩家→追击→攻击玩家受伤→血条更新→死亡时显示游戏结束。提示很多新手把信号连接写在_ready里用get_node找到目标节点也可以。但视觉化在场景面板里连接能减少路径写错的风险。尤其当HUD或Player层级变动时手动代码里的路径会失效而场景连接通常会在编辑时提示。6. 常见问题与调试技巧6.1 敌人穿过墙壁怎么办这是2D物理最常见的坑。如果你的敌人是用CharacterBody2D实现的但是移动时却穿过了StaticBody2D墙壁通常有两个原因敌人Collision Mask没有勾选包含墙壁的物理层。如果墙壁是默认第1层而你给敌人设置Mask只勾选了player层那么敌人就不会检测到墙壁。请检查敌人的Collision Mask确保包含墙壁所在层。场景中墙壁节点没有设置CollisionShape2D或者CollisionShape2D的Shape为空的。有些同学把StaticBody2D放上去了却忘了给它的子节点添加CollisionShape2D物理体根本没有形状自然穿模。经验之谈排查穿墙问题时打开“调试 - 可见碰撞形状”快捷键开启调试工具查看碰撞形状是否正常显示。如果玩家和敌人都有绿色矩形轮廓说明碰撞形状存在没有轮廓就是碰撞形状缺失。6.2 攻击时“抖刀”怎么办很多刚实现攻击的开发者会遇到按下攻击键攻击状态立刻结束或者0.2秒冷却、0.3秒状态结束但攻击动画感很怪。这是因为你的状态机切换太快攻击过程没有“准备-出招-收招”的阶段。解决方法把攻击状态时间拉长到0.4秒以上在攻击的前0.1秒内不执行伤害判定让玩家看到起手动作用attack_window_start和attack_window_end来标记伤害判定区间。比如var attack_state_time: float 0.0 var damage_window_start: float 0.1 var damage_window_end: float 0.2 func handle_attack_state(delta: float) - void: attack_state_time delta if attack_state_time damage_window_start and attack_state_time damage_window_end and not attack_damage_applied: apply_attack_damage() attack_damage_applied true if attack_state_time 0.5: change_state(State.MOVE)这个“伤害判定窗口”模式不仅让手感更好也能避免攻击动画刚按下就造成伤害的“瞬发”违和感。6.3 引用场景时节点路径错误很多报错Cannot get path都出现在使用get_node(../Player)时因为场景结构变了。例如玩家从Main场景的子节点变成了另一个场景的实例相对路径就失效了。推荐三个稳妥办法使用组把玩家放入player组用get_tree().get_first_node_in_group(player)使用export var player_to_chase: CharacterBody2D在场景面板中直接拖拽引用使用信号玩家移动或状态变化时把自身传给敌人监听。我实际项目最喜欢的是组合12。敌人实例化时如果玩家在场就通过导出变量引用更直接。但为了教程演示简单用的是组查找避免让新手纠结节点路径。6.4 攻击判定为什么总是从身后冒出来如果你使用attack_area.position facing_direction * 18方向向量怎么来的如果移动状态里直接用了Input.get_vector的结果它是四方向原始值可能归一化带小数但如果角色斜着移动facing_direction是斜向的攻击区域也会斜向放置看起来像是“从侧面打”。这不是bug是8方向攻击的正常结果。如果只想四方向攻击可以在读取输入后只取两者的最大值方向即将方向量化到上下左右var dir : Input.get_vector(...) if abs(dir.x) abs(dir.y): facing_direction Vector2(sign(dir.x), 0) elif abs(dir.y) abs(dir.x): facing_direction Vector2(0, sign(dir.y))这样攻击方向会更符合大多数2D动作游戏的直觉。七、延伸思考下一阶段还能加什么这一篇做完你得到的是一个极其简约但逻辑完整的动作游戏循环。后面几个方向可以继续扩展玩家增加血量回复道具让HUD信号接收更多数据敌人增加视野锥避免隔着墙看穿玩家玩家攻击特效粒子爆炸、刀光通过GPUParticles2D实现敌人增加死亡动画用一个Timer延迟释放替代立即queue_free增加音效和背景音乐用AudioStreamPlayer播放音频资源。我个人实际操作中体会最深的一点是先不要追求大而全的功能把“移动-攻击-受伤-血量反馈-敌人追击”这个最小闭环跑通哪怕是方块对方块都对后续开发有极大帮助。因为游戏系统的复杂性都建立在这个闭环之上你熟悉了节点如何交互、信号如何传递以后做射击、做塔防无非是把这套交互模式替换到不同的节点类型上。最后分享一个实用小习惯每次搭建新场景时先在场景根节点挂一个注释脚本写上“这个场景的职责”。这看起来多此一举但当你一周后回来看代码会很庆幸当初写了一句注释。保持好习惯比写出华丽的代码更容易让项目长期存活。
延伸阅读

更多相关文章

2026/10/18 23:39:55

恒星级AI时代:AI短剧生产管线与数字分身序章

1. 从“恒星级AI”说起:这个标题到底在讲什么“恒星级AI时代降临”这个说法,第一次看到的时候我愣了一下。恒星级的能量量级是什么概念?太阳每秒释放的能量大约是3.8乘以10的26次方瓦,人类目前全球总发电功率大概在3乘10的12次方瓦…

2026/10/18 23:39:55

快速修改PCK文件完整指南:用Patch PCK告别整包解压

快速修改PCK文件完整指南:用Patch PCK告别整包解压 【免费下载链接】gdsdecomp Godot reverse engineering tools 项目地址: https://gitcode.com/GitHub_Trending/gd/gdsdecomp 玩家反馈英文文本写错,要改 PCK 里一句话,包 1.8 GB。完…

2026/10/19 0:39:58

Java+Swing+MySQL超市商品管理系统:JDBC到JTable完整实战

简介:基于JavaSwingMySQL开发的超市商品管理系统完整源码包,面向Java初学者、数据库课程设计及毕业设计场景。系统围绕商品、库存、供应商三大核心表展开,通过Swing界面完成商品添加、查询、修改、删除等CRUD操作,并结合MySQL实现…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/19 0:04:56

悟空开源无代码平台实战:从实体建模到应用设计避坑指南

简介:基于悟空开源无代码平台的多种应用设计源码,面向希望在不编写代码的情况下快速搭建CRM、HRM、财务系统、ERP、SRM等业务应用的企业与技术人员,通过拖放式界面和内置模板即可完成应用定制与部署。压缩包共697个文件,约10.74MB…

2026/10/19 0:04:56

基于.NET9的多租户权限系统:数据权限与动态API落地实践

简介:这份资源是一套面向 .NET 与 Vue 全栈开发者的中台权限管理系统,后端基于 .Net9,前端采用 Vue3 Element Plus,并配套 uniapp uviewui 移动端方案,实现前后端分离。系统覆盖多租户隔离、数据权限、动态 Api、任务…

2026/10/19 0:04:56

场地预约小程序毕设实战:SSM+微信小程序+MySQL数据库设计到避坑

简介:面向毕业设计场景的场地预约小程序完整项目,基于微信小程序SSMMySql技术栈开发,适合计算机相关专业学生作为选题参考、功能扩展或二次开发起点。系统实现场地类型管理、场地管理、预约管理、取消申请与押金退回等功能,分为管…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑