本文详解如何在 discord.py 的 discord.ui.Button 中实现基于角色的访问控制,通过运行时检查用户角色权限来动态启用或禁用按钮交互,而非错误地使用命令装饰器。
本文详解如何在 discord.py 的 `discord.ui.button` 中实现基于角色的访问控制,通过运行时检查用户角色权限来动态启用或禁用按钮交互,而非错误地使用命令装饰器。
在 Discord.py 的 UI 系统(discord.ui.View / discord.ui.Button)中,@commands.has_role() 这类装饰器完全不适用——它仅作用于 commands.Bot 的命令上下文(如 @bot.command()),而按钮回调函数属于异步事件处理器,运行在 Interaction 上下文中,无命令权限系统介入。若强行添加该装饰器,不仅无效,还会导致运行时异常或静默失败。
✅ 正确做法是:在按钮回调内部主动检查用户是否拥有指定角色,并根据结果决定是否执行业务逻辑;若权限不足,则调用 interaction.response.defer(ephemeral=True) 向用户返回不可见的响应(避免“此交互已过期”错误),并可选发送私密提示。
以下是修正后的完整示例(含关键注释与健壮性增强):
import discord
from discord import ui, Interaction
from datetime import datetime, timedelta
import pytz
import time
class SimpleView(discord.ui.View):
def __init__(self):
self.start_time = None
self.stop_time = None
super().__init__(timeout=None)
@discord.ui.button(label="⬆️ ENTRATA", style=discord.ButtonStyle.green, custom_id="btn_entrata")
async def entrata(self, interaction: Interaction, button: ui.Button):
# ✅ 检查用户是否拥有 "?♂️ FUORI SERVIZIO" 角色(请替换为实际角色ID)
target_role_id = 123456789012345678 # ← 替换为你的角色ID(整数!)
role = interaction.guild.get_role(target_role_id)
if not role or role not in interaction.user.roles:
await interaction.response.send_message(
"❌ 你没有权限使用此按钮:需拥有 `?♂️ FUORI SERVIZIO` 角色。",
ephemeral=True
)
return
# ✅ 权限通过:执行业务逻辑
orario_it = datetime.now(pytz.timezone('Europe/Rome'))
orario = orario_it.strftime('%H:%M')
data = orario_it.strftime('%Y-%m-%d') # 补充缺失的 data 变量定义
SimpleView.start_time = time.time()
channel = interaction.client.get_channel(1214495527924928522)
if not channel:
await interaction.response.send_message("⚠️ 目标频道不可用,请联系管理员。", ephemeral=True)
return
embed = discord.Embed(color=0x23a559)
embed.set_thumbnail(url="https://shft.cl/img/c/cdn.discordapp.com-101395680655364460.webp")
embed.add_field(name="⬆️ ENTRATA ⬆️", value="", inline=False)
embed.add_field(name="NOME:", value=f"{interaction.user.mention}", inline=True)
embed.add_field(name="", value="", inline=True)
embed.add_field(name="DATA:", value=f"{data}", inline=True)
embed.add_field(name="ORARIO:", value=f"{orario}", inline=True)
embed.set_footer(text="ARMERIA200")
await channel.send(embed=embed)
await interaction.response.send_message(
f"***SEI ENTRATO IN SERVIZIO ALLE: {orario}.***",
ephemeral=True
)
@discord.ui.button(label="⬇️ USCITA", style=discord.ButtonStyle.red, custom_id="btn_uscita")
async def uscita(self, interaction: Interaction, button: ui.Button):
# ✅ 检查 "?♂️ IN SERVIZIO" 角色(同样替换为真实ID)
target_role_id = 987654321098765432 # ← 替换为你的角色ID
role = interaction.guild.get_role(target_role_id)
if not role or role not in interaction.user.roles:
await interaction.response.send_message(
"❌ 你没有权限使用此按钮:需拥有 `?♂️ IN SERVIZIO` 角色。",
ephemeral=True
)
return
# ✅ 执行退出逻辑
orario_it = datetime.now(pytz.timezone('Europe/Rome'))
orario = orario_it.strftime('%H:%M')
data = orario_it.strftime('%Y-%m-%d')
if SimpleView.start_time is None:
await interaction.response.send_message(
"⚠️ 你尚未执行‘ENTRATA’操作,无法计算服务时长。",
ephemeral=True
)
return
SimpleView.stop_time = time.time()
elapsed_time = SimpleView.stop_time - SimpleView.start_time
time_d = timedelta(seconds=int(elapsed_time))
channel = interaction.client.get_channel(1215061241270374400)
if not channel:
await interaction.response.send_message("⚠️ 目标频道不可用,请联系管理员。", ephemeral=True)
return
embed = discord.Embed(color=0xda373c)
embed.set_thumbnail(url="https://shft.cl/img/c/cdn.discordapp.com-101395680655364460.webp")
embed.add_field(name="⬇️ USCITA ⬇️", value="", inline=False)
embed.add_field(name="NOME:", value=f"{interaction.user.mention}", inline=True)
embed.add_field(name="DATA:", value=f"{data}", inline=True)
embed.add_field(name="ORARIO:", value=f"{orario}", inline=True)
embed.add_field(name="TEMPO IN SERVIZIO:", value=f"{time_d}", inline=True)
embed.set_footer(text="ARMERIA200")
await channel.send(embed=embed)
await interaction.response.send_message(
f"***SEI USCITO DI SERVIZIO ALLE: {orario}, DOPO: {time_d}***",
ephemeral=True
)
? 关键注意事项:
- 角色 ID 必须为整数:在 Discord 客户端开启开发者模式 → 右键角色 → “复制ID”,粘贴到代码中并确保无引号(如 123456789012345678,非 "123456789012345678")。
- interaction.guild 必须存在:按钮仅在服务器内有效,私信中 guild 为 None,需前置校验(本例已隐含在 get_role 调用中,但建议在 View 初始化时加 if not interaction.guild: 防御)。
- 避免全局变量竞态:SimpleView.start_time 是类属性,多用户并发可能冲突。生产环境应改用 self.start_time(实例属性)或基于 interaction.user.id 的字典缓存。
- 按钮视觉不可见? Discord 不支持服务端隐藏按钮,但可通过 disabled=True + 动态更新 View 实现(需 view.message.edit(view=new_view))。如需彻底隐藏,应结合 View.on_timeout() 或权限感知的初始 View 构建逻辑。
? 进阶提示:若需更灵活的权限控制(如多个角色、管理权限绕过),可封装通用检查函数:
async def check_role_or_admin(interaction: Interaction, *role_ids: int) -> bool:
if interaction.user.guild_permissions.administrator:
return True
guild = interaction.guild
return any(guild.get_role(rid) in interaction.user.roles for rid in role_ids)
掌握这一模式后,你不仅能精准控制按钮访问,更能将其迁移至下拉菜单(Select)、模态框(Modal)等所有 Interaction 驱动组件中,构建真正安全、专业的 Discord 机器人交互体验。










