沿途的风景 - 后端技术
type: channel
link: https://inspirecl.asia/index.php/category/hdjs/
description: 后端技术
items: webman 生产部署实战：进程守护、平滑发布、监控告警与微服务化
items: webman 高并发进阶：协程实战、连接池调优与压测方法论
items: webman 数据库实战：连接池、ORM 选型、事务边界与大数据处理
items: webman 中间件与依赖注入实战：洋葱模型、PHP-DI 容器与 AOP
items: webman 架构解析：常驻内存进程模型与 PHP-FPM 的本质差异
items: Redis 缓存实战：穿透、击穿、雪崩三大问题解决方案
items: Redis 分布式锁实现：从 SET NX 到 Redlock 算法
items: FastAdmin API 接口开发：前后端分离实践
items: FastAdmin 插件开发实战：从零开发自定义插件
items: FastAdmin 权限认证体系：RBAC 角色与菜单管理

webman 生产部署实战：进程守护、平滑发布、监控告警与微服务化
type: item
date: 2026-08-22T13:40:29+08:00
description: webman 生产部署实战：进程守护、平滑发布、监控告警与微服务化把 webman 写出来是一回事，让它安稳跑在生产上是另一回事。没有 PHP-FPM 和 supervisord 的成熟套路兜底，常驻内存服务的部署、发布、监控都有一套自己的打法。这篇是 webman 系列收官：从部署形态、平滑发布到监控告警体系，最后聊聊什么时候该拆微服务。一、部署形态：三种选择1. 裸机/虚拟机 + systemd（中小项目首选）# /etc/systemd/system/webman.service [Unit] Description=webman app After=network.target mysql.service redis.service [Service] Type=forking WorkingDirectory=/var/www/app ExecStart=/usr/bin/php start.php start -d ExecReload=/usr/bin/php start.php reload ExecStop=/usr/bin/php start.php stop Restart=always RestartSec=3 [Install] WantedBy=multi-user.targetsystemctl daemon-reload systemctl enable --now webman systemctl reload webman # 平滑发布用 reload要点：Type=forking（start.php -d 是 daemonize 模式）、Restart=always 兜底进程崩溃、ExecReload 挂 reload 实现无感发布。2. Docker 容器化FROM php:8.2-cli-alpine RUN apk add --no-cache libpq libzip-dev && docker-php-ext-install pdo_mysql opcache COPY . /app WORKDIR /app RUN composer install --no-dev --optimize-autoloader EXPOSE 8787 # 容器内前台运行（容器本身就是守护者，不需要 -d） CMD ["php", "start.php", "start"]容器化要点：容器里必须前台运行（去掉 -d），让 PID 1 就是 master 进程，容器崩溃重启策略（restart: always）接管守护职责。K8s 场景用 readiness 探针指向健康检查接口。3. Supervisor 托管（多语言混合部署环境）老牌方案，配置直观，适合还有 Python/Go 混跑的机器。二、平滑发布：让 reload 真正"无感"php start.php reload 的机制是逐个重启 worker（上一篇讲过），但要真正做到无感发布，还有四个坑要填：坑一：reload 不加载的东西reload 只重新加载 worker，master 进程内存里的东西不会更新：路由配置文件、config/ 目录、bootstrap 钩子。改了这些必须 restart（master 重建，有感知）。发布脚本要区分：#!/bin/bash # 简易发布脚本 git pull composer install --no-dev --optimize-autoloader php start.php reload # 常规发布 # 改了 config/route/bootstrap 时：php start.php restart坑二：发布瞬间的连接抖动worker 逐个重启期间，正在处理中的请求要等它完成（超过 max_wait 会被强杀）。发布时把 max_wait_time 临时调大、避开高峰期，长任务（导出、批处理）走独立进程不随 worker 重启。坑三：静态文件缓存webman 自带静态文件支持时，浏览器缓存的旧 JS 配合新 HTML 可能白屏——资源文件名带 hash（app.a3f8c2.js）彻底解决。坑四：发布失败的回滚# 用软链接做版本切换，回滚 = 切回旧链接 ln -sfn /var/www/releases/20260823-0200 /var/www/app_current php start.php restart # 软链指向变了，restart 生效保留最近 5 个版本目录，回滚 10 秒完成。没有回滚方案的发布都是赌博。三、监控告警体系：四个层次常驻内存服务必须主动监控——它出问题的方式是"安静地僵死"，不是崩溃报错。层次一：进程存活（最基础）# 心跳脚本（cron 每分钟）…

webman 高并发进阶：协程实战、连接池调优与压测方法论
type: item
date: 2026-08-15T18:27:21+08:00
description: webman 高并发进阶：协程实战、连接池调优与压测方法论webman 裸奔就能跑出很高的 QPS，但"框架快"不等于"业务快"——一个同步 HTTP 调用就能让 worker 的事件循环卡住，吞吐瞬间跌到个位数。这篇讲 webman 高并发的三板斧：协程化改造、连接池调优、压测方法论。这是 webman 中高级的分水岭。一、先理解瓶颈：worker 事件循环是怎么被卡死的webman 的 worker 是单进程单线程事件循环（Reactor 模型）。在同一个 worker 里，请求是串行处理的：worker-1 事件循环： 事件：请求A到达 → 处理A（其中调用外部API耗时 200ms，同步阻塞！） （这 200ms 内，分配给 worker-1 的所有其他请求全部排队） 事件：请求B到达 → 处理B推论：平均响应时间 = 排队时间 + 处理时间。 只要处理路径上有阻塞调用，worker 的吞吐上限就是 1 / 平均阻塞时长。200ms 的外部调用意味着单个 worker 每秒最多 5 个请求，16 个 worker 也就 80 QPS——和框架本身的几十万 QPS 上限毫无关系。解法只有两条：消灭阻塞（异步/协程化），或者增加并行度（协程）。二、协程：webman 的三种姿势1. 内置 Swoole/Swow 协程驱动webman 支持通过配置切换协程运行时：// config/server.php 'settings' => [ 'event_loop' => \Workerman\Events\Swoole::class, // 或 Swow ],开启后，worker 内自动协程化：遇到 IO（MySQL、Redis、HTTP）时挂起当前协程，让出事件循环处理其他请求——同一个 worker 从串行变成并发。Swoole 与 Swow 的选择：Swoole 生态成熟但与 PHP 原生扩展偶有兼容性问题（hook 覆盖不全的扩展会退化为阻塞）；Swow 是 PHP 官方系方案更干净但生态年轻。生产选型建议：现有业务全是标准 PDO/cURL 的选 Swoole 一把梭；有奇怪扩展依赖的先在预发验证 Swow。2. 协程客户端协程化后最大的红利是并发聚合调用：use Swoole\Coroutine; use function Swoole\Coroutine\go; use function Swoole\Coroutine\waitGroup; // 聚合页：并行拉取用户/商品/优惠券，总耗时 = max 而不是 sum $wg = new waitGroup(); $results = []; go(function () use ($wg, &$results) { $wg->add(); $results['user'] = $this->userApi->get($uid); $wg->done(); }); go(function () use ($wg, &$results) { $wg->add(); $results['item'] = $this->itemApi->get($itemId); $wg->done(); }); $wg->wait(3.0); // 总超时 3s // 原来 200+150+100=450ms，现在 max(200,150,100)=200msBFF/聚合接口的标准优化手段，P99 延迟直接砍半。3. 协程的三个纪律纪律一：协程切换点后不信任局部状态。 yield 期间其他协程可能改了共享数据。请求级数据老老实实走 $request 属性，全局状态放 Redis。纪律二：连接必须绑协程。 协程 A 半截的事务，切换后协程 B 接着用同一个连接 = 事务串台。Swoole 的 PDO hook + webman/database 的连接池已处理绑定关系，但你自己 new PDO 存静态属性再跨协程用，必炸。纪律三：CPU 密集任务协程救不了。 协程只解决 IO 并发，CPU 密集计算照样卡死事件循环。重计算丢给自定义进程或队列，别在 worker 里跑。三、连接池：协程时代的正确配置协程化后并发量上来了，单连接成了新瓶颈，必须配连接池（webman 通过 webman/database/webman/redis 的连接池实现，Swoole 驱动下生效）：// config/database.php（连接池相关参数） 'pool' => [ 'max_connections' => 32, // 每进程最大连接数 'min_connections' => 2, // 保底连接 'connect_timeout' => 3, // 取连接超时 'wait_timeout' …

webman 数据库实战：连接池、ORM 选型、事务边界与大数据处理
type: item
date: 2026-08-15T09:22:54+08:00
description: webman 数据库实战：连接池、ORM 选型、事务边界与大数据处理数据库是 webman 与传统 FPM 框架差异最大的环节。FPM 下每个请求开新连接、请求结束关闭，天然隔离；webman 的每个 worker 进程持有自己的连接并跨请求复用——连接管理从"框架的事"变成了"你的事"。这篇讲透连接池配置、think-orm 与 Eloquent 的选型、事务的正确边界，以及千万级数据的处理姿势。一、常驻内存下的连接模型先看清楚 webman 的数据库连接结构：worker-1 ─── 连接A（MySQL）──┐ worker-2 ─── 连接B ├── MySQL max_connections 上限（如 1000） worker-3 ─── 连接C │ ...（16 个 worker） │ └── Redis 连接同理推论一：最大连接数 = worker 进程数 × 每进程连接数。 16 个 worker、每个用了 2 个连接配置（读写分离），就是 32 个常驻连接。上线前先算这笔账，别和 MySQL 的 max_connections 打架。推论二：连接是懒建立的。 进程启动时不连库，第一次查询才建立。所以刚 reload 完的进程有冷启动延迟，压测时记得先预热。推论三：连接会"死"。 MySQL wait_timeout（默认 8 小时）内没有活动，服务端单方面断开，webman 侧还握着死连接，下一个请求直接 MySQL has gone away。二、断线重连：三层配置兜底以 think-orm（webman 默认集成）为例：// config/thinkorm.php return [ 'default' => 'mysql', 'connections' => [ 'mysql' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'app', 'username' => 'root', 'password' => 'root', 'charset' => 'utf8mb4', 'break_reconnect' => true, // ① 断线自动重连 'fields_cache' => true, // ② 字段信息缓存（常驻内存的红利） ], ], ];三层兜底缺一不可：break_reconnect => true：ORM 层检测断连自动重连心跳保活：定时进程每 5 分钟发一条 SELECT 1，让连接永不超时（webman 的 config/process.php 里加自定义进程实现）重试幂等：断线重连只对查询安全，写操作重试必须保证幂等，否则可能双写// 心跳进程示例 return [ 'db-keepalive' => [ 'handler' => function () { while (true) { Db::query('SELECT 1'); sleep(300); } }, ], ];三、ORM 选型：think-orm 还是 Laravel Eloquent？webman 官方两者都支持，选型看团队基因：维度think-ormEloquent（illuminate/database）上手成本ThinkPHP 系团队零成本Laravel 系团队零成本模型功能基础够用关联预加载、访问器、软删更丰富常驻内存适配fields_cache 等针对性优化需要自己注意静态缓存生态迁移TP 项目平移Laravel 项目平移中大型项目的建议：ORM 只做"查询构造 + 结果映射"，复杂查询直接裸 SQL 或查询构造器。模型层越薄，常驻内存下的心智负担越小。一个 Eloquent 用户必须知道的区别：FPM 下 DB::connection() 每请求新建，webman 下连接跨请求驻留——事务没有提交/回滚，下一个请求会接着在这个事务里跑，数据不一致的幽灵bug就是这么来的。think-orm 的 Db::startTrans() 同理。// ❌ 事故现场：异常没回滚，连接带着事务回到池里 try { Db::startTrans(); // ... 写操作 throw new \RuntimeException('boom'); // 没有 catch 回滚！ } finally { Db::commit(); // finally 里 commit，异常时也 commit 了半截数据？ }四、事务的正确姿势铁律一：事务边界必须完整闭合try { Db::startTrans(); $this->deductStock($skuId, $num); // 扣库存 $this->createOrder($uid, $skuI…

webman 中间件与依赖注入实战：洋葱模型、PHP-DI 容器与 AOP
type: item
date: 2026-08-06T00:49:04+08:00
description: webman 中间件与依赖注入实战：洋葱模型、PHP-DI 容器与 AOPwebman 的请求处理链路看似简单（路由 → 中间件 → 控制器），但中间的洋葱模型和 PHP-DI 容器藏着不少中级开发者没吃透的细节。这篇文章把中间件的执行顺序、全局/路由级/方法级三种挂载方式、依赖注入的边界一次性讲清楚，最后再聊一个常驻内存框架特有的话题——AOP。一、洋葱模型：代码顺序与执行顺序是两回事class AuthMiddleware implements MiddlewareInterface { public function process(Request $request, callable $handler): Response { // ── 前置逻辑（进入时执行）── $userId = $this->jwt->parseToken($request); $request->userId = $userId; $response = $handler($request); // ← 下一层洋葱 // ── 后置逻辑（返回时执行）── $response->withHeader('X-Trace-Id', $request->header('x-trace-id')); return $response; } }三层中间件的执行顺序：请求 → [A前置] → [B前置] → [C前置] → Controller 响应 ← [A后置] ← [B后置] ← [C后置] ←最容易踩的坑：异常发生时的执行路径。 如果控制器抛异常，中间件的后置代码默认不会执行——除非你在中间件里 try/catch 包住 $handler($request)。跨域中间件经常因此"时灵时不灵"：异常响应没带 CORS 头，浏览器报的不是后端错误而是跨域错误。所以 CORS 中间件要这样写：try { $response = $handler($request); } catch (\Throwable $e) { $response = response($e->getMessage(), 500); } return $response->withHeaders([ 'Access-Control-Allow-Origin' => '*', 'Access-Control-Allow-Headers' => 'Content-Type, Authorization', ]);二、三种挂载粒度1. 全局中间件（config/middleware.php）return [ // 每个请求都会经过 app\middleware\AccessLog::class, app\middleware\Cors::class, ];适合：日志、CORS、限流。注意全局中间件每个请求都会跑，别放重逻辑。2. 路由/应用级中间件// config/route.php Route::group('/admin', function () { Route::resource('/users', app\admin\controller\UserController::class); })->middleware([ app\middleware\AdminAuth::class, ]); // 或整个应用目录生效：app/admin/config/middleware.php return [ app\middleware\AdminAuth::class, ];多应用（多入口目录）模式下，app/{应用名}/config/middleware.php 只对该应用生效——这是做多租户后台、API 版本隔离的天然工具。3. 控制器方法级（中间件别名）// config/middleware.php 里注册别名 return [ 'auth' => [app\middleware\Auth::class], ]; // 控制器里按方法指定 class OrderController { #[Middleware(['auth'])] // 整个控制器生效 public function index() {} #[Middleware(['auth', 'throttle:60,1'])] public function create() {} }执行优先级：全局 → 应用级 → 路由级 → 方法级。同层内按注册顺序。三、实战：三个高频中间件的正确写法限流中间件（基于 Redis 滑动窗口）class Throttle implements MiddlewareInterface { public function __construct(private Redis $redi…

webman 架构解析：常驻内存进程模型与 PHP-FPM 的本质差异
type: item
date: 2026-08-04T22:09:17+08:00
description: webman 架构解析：常驻内存进程模型与 PHP-FPM 的本质差异webman 是基于 Workerman 构建的高性能 PHP 框架，官方基准 QPS 常态化压到十几万。但很多人把 webman 当"换个写法的 Laravel"来用，结果内存泄漏、全局状态污染这些经典问题轮番上演。要真正用好 webman，必须先理解它与 PHP-FPM 在架构上的本质差异——这是所有后续实践的地基。一、两种执行模型：请求级生命周期 vs 常驻内存PHP-FPM：每次请求都是新生浏览器 → Nginx → PHP-FPM(worker) → 加载框架 → 初始化 → 处理请求 → 返回 → 销毁一切FPM 模式下，每个请求结束都会释放全部内存、销毁所有变量。代价是每次请求都要重新加载框架（几十毫秒的 bootstrap），但好处是状态绝对隔离——你再怎么写全局变量，下一个请求都是白纸。webman：一次加载，永久服务浏览器 → webman(worker 进程) → 直接路由分发 → 处理请求 → 返回（框架和对象留在内存）webman 启动时把框架、路由表、配置全部加载进内存，之后每个请求只是"触发一次回调"。QPS 高的根源就在这：省掉了每请求一次的框架 bootstrap 开销。这个差异带来两个直接推论：性能提升的本质是"摊销"：初始化成本只付一次，请求越多收益越大内存不自动释放：请求之间共享进程内存，全局状态会"存活"——这是福也是祸二、进程模型：master / worker 的分工master 进程（不处理业务） ├── 监听端口、管理 worker ├── fork 出 N 个 worker 进程 └── 收到 reload 信号时逐个平滑重启 worker │ ├── worker-1 ← 每个进程独立事件循环，可同时 hold 住数千连接 ├── worker-2 ├── ... └── worker-N（默认 = CPU 核数）三个关键认知：1. worker 数不是越多越好。 webman 是多进程 + 每进程事件循环（非阻塞 IO）模型，worker 数建议等于 CPU 核数。如果你的业务有阻塞调用（同步 HTTP 客户端、同步 RPC），连接会"卡住"事件循环，这时才需要加进程数来对冲——但更好的解法是消灭阻塞调用（后面协程篇细讲）。2. 进程间不共享内存。 你在 worker-1 里写的静态变量，worker-2 看不见。跨进程共享状态要用 Redis 或进程间通信，别指望 static 属性全站生效。3. request 对象请求间隔离，但对象池里的组件不是。 框架层面的组件（DB 连接、日志实例）跨请求复用，这是性能来源；你自己的类如果注册成了单例，它同样跨请求存活。三、启动流程：从 public/index.php 说起// public/index.php（简化） require_once __DIR__ . '/../vendor/autoload.php'; support\App::run();App::run() 内部做了什么：1. 加载 .env 与 config/*.php（此后修改配置需重启） 2. 扫描 app/controller 生成路由表（注解路由另算） 3. 初始化容器（webman 基于 PHP-DI，支持构造器注入） 4. 加载 bootstrap/*.php（进程启动时执行一次的钩子） 5. 初始化数据库/Redis 连接（懒加载，首次使用时建立） 6. master fork workers → 每个 worker 进入事件循环等待请求bootstrap 目录是理解 webman 的钥匙：放在这里的代码每个进程只执行一次。数据库连接的建立、连接池的初始化、Swoole 协程运行时的开启，都在这个阶段。官方的 webman/database、webman/redis 等插件都是通过 bootstrap 文件挂载的。四、常驻内存的四宗罪与解法罪一：静态属性 / 全局变量跨请求污染// ❌ 经典事故：上一单用户的数据泄漏给下一单 class OrderService { public static ?array $currentUser = null; } // 请求A设置了 currentUser，请求B进来时它还活着！解法：请求级数据一律走 Request 对象传递，或用 $request->xxx 属性挂载。静态属性只允许存"进程级不变量"（配置、编译好的正则、容器实例）。罪二：内存泄漏每请求增长的数组、未释放的大结果集、监听器越挂越多……FPM 下这些泄漏随请求结束被清零，webman 下会累积到 memory_limit 进程崩溃。排查手段：// 在中间件里记录内存水…

Redis 缓存实战：穿透、击穿、雪崩三大问题解决方案
type: item
date: 2026-05-10T11:42:54+08:00
description: 前言Redis 作为缓存使用时，会遇到三大经典问题：缓存穿透、缓存击穿和缓存雪崩。这三个问题在实际生产中频繁出现，处理不当可能导致数据库被压垮甚至服务不可用。本文将深入分析每个问题的成因并给出完整解决方案。一、缓存穿透1.1 问题描述用户请求的数据在缓存和数据库中都不存在，每次请求都直接打到数据库。常见于恶意攻击（用不存在的 ID 大量请求）或业务异常数据。请求 → 缓存MISS → 数据库MISS → 返回空 ↑ │ └──── 不断重复 ──────────────────────┘1.2 危害数据库承受大量无效查询如果是恶意攻击，可能打垮数据库浪费服务器资源1.3 解决方案方案一：缓存空值function getUser($id) { $cacheKey = "user:{$id}"; $data = $redis->get($cacheKey); if ($data !== false) { // 命中缓存（包括空值缓存） return $data === 'NULL' ? null : json_decode($data, true); } // 查数据库 $data = $db->query("SELECT * FROM users WHERE id = ?", [$id]); if (empty($data)) { // 缓存空值，设置较短过期时间（60秒） $redis->setex($cacheKey, 60, 'NULL'); return null; } $redis->setex($cacheKey, 3600, json_encode($data)); return $data; }优点：实现简单缺点：可能缓存大量空值，占用内存；数据后来新增了也不会被感知（除非过期）方案二：布隆过滤器（Bloom Filter）// 系统初始化时，将所有存在的ID加入布隆过滤器 function initBloomFilter() { $redis->del('bloom:user_ids'); $userIds = $db->query("SELECT id FROM users"); foreach ($userIds as $id) { // 使用多个哈希函数 bloomAdd('bloom:user_ids', $id); } } // 检查ID是否可能存在 function bloomAdd($key, $value) { $hashes = multiHash($value); foreach ($hashes as $h) { $redis->setBit($key, $h, 1); } } function bloomExists($key, $value) { $hashes = multiHash($value); foreach ($hashes as $h) { if ($redis->getBit($key, $h) == 0) { return false; // 一定不存在 } } return true; // 可能存在（有误判率） } // 请求处理 function getUser($id) { // 先过布隆过滤器 if (!bloomExists('bloom:user_ids', $id)) { return null; // 一定不存在，直接返回 } // 再查缓存和数据库 // ... }方案三：接口层参数校验// 在入口处拦截非法参数 function getUser($id) { // ID 必须是正整数 if (!is_numeric($id) || $id <= 0 || $id > PHP_INT_MAX) { throw new InvalidArgumentException('Invalid user ID'); } // 其他业务规则校验 if (strlen($id) > 10) { throw new InvalidArgumentException('ID too long'); } // 正常流程... }二、缓存击穿2.1 问题描述某个热点 key 突然过期（或被删除），大量并发请求同时访问这个 key，全部穿透到数据库。热点key过期 │ ├──→ 请求1 → 缓存MISS → 查DB ──┐ ├──→ 请求2 → 缓存MISS → 查DB ──┤ ├──→ 请求3 → 缓存MISS → 查DB ──┤ ├──→ 请求N → 缓存MISS → 查DB ──┤ │ │ └──────── 数据库被压垮 ◄────────┘2.2 解决方案方案一：互斥锁（推荐）function getHotData($key) { $data = $redis-…

Redis 分布式锁实现：从 SET NX 到 Redlock 算法
type: item
date: 2026-04-02T10:27:44+08:00
description: 前言分布式锁是分布式系统中控制资源访问的核心机制。Redis 因其单线程特性和高性能，是实现分布式锁的首选方案。本文将从最简单的实现出发，逐步演进到生产级的 Redlock 算法。一、为什么需要分布式锁1.1 单机锁的问题// 单机环境：PHP文件锁或flock可以解决 $fp = fopen('/tmp/lock.txt', 'w+'); flock($fp, LOCK_EX); // 加锁 // 执行业务逻辑 flock($fp, LOCK_UN); // 释放锁 fclose($fp);但在分布式环境中，请求可能被分配到不同服务器，文件锁无法跨机器生效。1.2 分布式锁的核心要求要求说明互斥性同一时刻只有一个客户端持有锁避免死锁锁必须有过期时间，防止持有者宕机导致永久锁死解铃还须系铃人只有锁的持有者才能释放锁高可用锁服务自身不能成为单点二、Redis 分布式锁演进2.1 版本一：SETNX（初版）// 最简单的实现 $lockKey = 'lock:order:1001'; // 尝试加锁 $redis->setnx($lockKey, 1); // 返回1表示成功，0表示失败 // 设置过期时间（防死锁） $redis->expire($lockKey, 30); // 业务逻辑 processOrder(); // 释放锁 $redis->del($lockKey);致命问题：setnx 和 expire 是两条命令，不保证原子性。如果 setnx 成功后服务崩溃，锁将永远不会过期，导致死锁。2.2 版本二：SET NX EX（推荐基础版）$lockKey = 'lock:order:1001'; $lockValue = uniqid('', true); // 生成唯一标识 // 一条命令完成加锁+过期，原子操作 $result = $redis->set($lockKey, $lockValue, ['NX', 'EX' => 30]); if ($result) { try { // 获取锁成功，执行业务 processOrder(); } finally { // 释放锁 $redis->del($lockKey); } } else { // 获取锁失败 throw new Exception('系统繁忙，请稍后重试'); }改进：SET key value NX EX 30 是原子操作，解决了死锁问题。遗留问题：释放锁时直接 DEL，可能删掉别人的锁。2.3 版本三：锁标识 + Lua 释放（安全版）$lockKey = 'lock:order:1001'; $lockValue = uniqid('', true); // 加锁 $result = $redis->set($lockKey, $lockValue, ['NX', 'EX' => 30]); if (!$result) { throw new Exception('获取锁失败'); } try { processOrder(); } finally { // 用Lua脚本安全释放锁：只有value匹配才删除 $luaScript = " if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end "; $redis->eval($luaScript, [$lockKey, $lockValue], 1); }为什么需要 Lua 脚本？时间线： T1: 客户端A GET lock → value=A T2: 锁过期，自动删除 T3: 客户端B SET lock NX → 成功，value=B T4: 客户端A DEL lock → 删掉了B的锁！Lua 脚本保证了 GET + 判断 + DEL 三步原子执行。2.4 版本四：锁续期（生产可用版）业务执行时间可能超过锁的过期时间，需要后台续期：class RedisLock { private $redis; private $lockKey; private $lockValue; private $ttl = 30; private $renewTimer = null; public function lock($key, $ttl = 30) { $this->lockKey = $key; $this->lockValue = uniqid('', true); $this->ttl = $ttl; // 尝试加锁，最多重试3次 for ($i = 0; $i < 3; $i++) { $result = $this->redis->set( $t…

FastAdmin API 接口开发：前后端分离实践
type: item
date: 2026-03-29T21:30:15+08:00
description: 前言随着前后端分离架构的普及，FastAdmin 的 API 模块越来越重要。本文将系统讲解如何使用 FastAdmin 开发 RESTful API 接口，包括 Token 认证、参数验证、数据返回格式规范以及与前端框架的对接实践。一、API 模块概述FastAdmin 的 API 模块位于 application/api/，专门用于提供接口服务：application/api/ ├── controller/ │ ├── Common.php # 公共控制器 │ ├── Index.php # 首页接口 │ ├── User.php # 用户接口 │ ├── Api.php # 基类控制器 │ └── Demo.php # 示例接口 ├── model/ │ └── User.php # API 用户模型 └── config.php # API 模块配置二、API 基类控制器所有 API 控制器继承自 app\common\controller\Api：<?php // application/common/controller/Api.php namespace app\common\controller; use think\Controller; use app\common\library\Token; use app\common\library\Auth; class Api extends Controller { // 不需要登录的方法 protected $noNeedLogin = ['*']; // 不需要权限验证的方法 protected $noNeedRight = ['*']; // 当前请求的 Token protected $token = ''; // 当前登录用户 protected $auth = null; // 初始化 public function _initialize() { parent::_initialize(); // 跨域处理 $this->checkCors(); // Token 认证 $this->token = $this->request->server('HTTP_TOKEN', $this->request->request('token', '')); // 初始化认证 $this->auth = Auth::instance(); // 检查是否需要登录 $action = strtolower($this->request->action()); if (!in_array($action, array_map('strtolower', $this->noNeedLogin))) { if (!$this->auth->isLogin()) { $this->error(__('Please login first'), null, 401); } } } // 跨域设置 protected function checkCors() { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: token, Content-Type, Authorization'); if ($this->request->method() == 'OPTIONS') { $this->success(); } } // 成功返回 protected function success($msg = '', $data = null, $code = 1) { $this->result($msg, $data, $code); } // 失败返回 protected function error($msg = '', $data = null, $code = 0) { $this->result($msg, $data, $code); } // 统一返回格式 protected function result($msg, $data = null, $code = 1) { $result = [ 'code' => $code, 'msg' => $msg, 'time' => time(), 'data' => $data, ]; throw new \think\exception\HttpResponseException( json($result) ); } }统一返回格式{ "code": 1, …

FastAdmin 插件开发实战：从零开发自定义插件
type: item
date: 2026-03-28T21:54:11+08:00
description: 前言FastAdmin 的插件系统是其生态的核心——通过在线安装插件，可以快速扩展论坛、CMS、商城等功能模块，无需修改框架核心代码。本文将从零开始，带你完整开发一个 FastAdmin 插件。一、插件目录结构一个标准的 FastAdmin 插件目录如下：addons/ └── mydemo/ # 插件目录（插件标识） ├── Mydemo.php # 插件主类 ├── config.php # 插件配置文件 ├── info.ini # 插件信息文件 ├── controller/ │ └── Index.php # 插件控制器 ├── model/ │ └── Demo.php # 插件模型 ├── view/ │ └── index/ │ └── index.html # 插件视图 ├── public/ │ ├── css/ │ │ └── mydemo.css # 插件样式 │ └── js/ │ └── mydemo.js # 插件脚本 └── hooks.php # 钩子定义（可选）二、创建插件2.1 使用命令行创建php think addon -a mydemo -c create这会在 addons/mydemo/ 目录下自动生成插件骨架文件。2.2 info.ini 插件信息name = mydemo title = 示例插件 description = 这是一个 FastAdmin 示例插件 author = Your Name version = 1.0.0 state = 1字段说明name插件标识（目录名，全小写）title插件名称description插件描述author作者version版本号state状态：1=正常，0=禁用2.3 插件主类 Mydemo.php<?php namespace addons\mydemo; use think\Addons; class Mydemo extends Addons { // 插件安装时执行 public function install() { // 创建数据表 $sql = "CREATE TABLE IF NOT EXISTS `fa_mydemo` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL DEFAULT '', `content` text, `createtime` int(10) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; \think\Db::execute($sql); return true; } // 插件卸载时执行 public function uninstall() { // 删除数据表 \think\Db::execute("DROP TABLE IF EXISTS `fa_mydemo`"); return true; } // 插件启用时执行 public function enable() { return true; } // 插件禁用时执行 public function disable() { return true; } // 钩子：在后台首页显示 public function adminIndexTop($params) { echo ' 欢迎使用 Mydemo 插件！ '; } }2.4 config.php 插件配置<?php return [ [ 'name' => 'title', 'title' => '插件标题', 'type' => 'text', 'content' => '', 'value' => 'Mydemo Plugin', 'rule' => 'required', 'msg' => '请填写插件标题', 'tip' => '显示在插件管理页面的标题', ], [ 'name' => 'display_count', 'title' => '显示数量', 'type' => 'number', 'content' => '', 'value' => '10', 'rule' => 'required', 'msg' => '请填写显示数量', 'tip' => '前台显示的数据条数', ], [ 'name' => 'is_open', 'title' => '是否开启', 'type' => 'radio', 'content' => ['0' => '关闭', '1' => '开启'], 'value' => '1…

FastAdmin 权限认证体系：RBAC 角色与菜单管理
type: item
date: 2026-03-26T16:21:57+08:00
description: 前言FastAdmin 内置了完整的 RBAC（基于角色的访问控制）权限体系，支持细粒度的菜单管理、操作权限和数据权限控制。本文将深入讲解 FastAdmin 权限认证的工作原理和实际配置方法。一、RBAC 权限模型概述FastAdmin 的权限体系包含三个核心实体：管理员（Admin）──→ 角色组（Group）──→ 权限规则（Rule）实体数据表说明管理员fa_admin后台用户，每个用户属于一个或多个角色组角色组fa_group角色定义，每个角色关联一组权限规则权限规则fa_auth_rule权限节点，对应控制器的每个操作方法二、权限认证流程2.1 认证类 AuthFastAdmin 的权限认证由 application/common/library/Auth.php 实现：namespace app\common\library; use think\Session; use think\Cookie; use app\admin\model\Admin as AdminModel; class Auth { protected $logined = false; // 是否登录 protected $admin = null; // 当前管理员信息 protected $error = ''; // 错误信息 protected $requestUri = ''; // 当前请求 URL // 检查是否已登录 public function isLogin() { if ($this->logined) { return true; } $admin = Session::get('admin'); if (!$admin) { return false; } // 验证管理员状态 $this->admin = AdminModel::get($admin['id']); if (!$this->admin || $this->admin['status'] != 'normal') { return false; } $this->logined = true; return true; } // 检查权限 public function check($name = '') { if (!$this->isLogin()) { return false; } // 超级管理员直接通过 if ($this->admin['id'] == 1) { return true; } // 获取当前用户的所有权限规则 $rules = $this->getRuleList(); if (!$name) { $name = strtolower($this->request->module() . '/' . $this->request->controller() . '/' . $this->request->action()); } $name = strtolower($name); // 检查是否有匹配的权限 foreach ($rules as $rule) { if ($this->checkRule($rule, $name)) { return true; } } return false; } }2.2 权限检查链路请求进入 → Backend 控制器初始化 → Auth::check() 检查当前 URL 是否有权限 → 无权限 → 返回 403 / 跳转登录 → 有权限 → 继续执行2.3 控制器中的权限注解FastAdmin 通过注解方式声明权限节点：// application/admin/controller/content/Article.php namespace app\admin\controller\content; use app\common\controller\Backend; class Article extends Backend { // 默认不需要登录的方法 protected $noNeedLogin = []; // 默认不需要权限验证的方法 protected $noNeedRight = []; /** * 文章列表 */ public function index() { return parent::index(); } /** * 添加文章 */ public function add() { return parent::add(); } /** * 编辑文章 */ public function edit($ids = null) { return parent::edit($ids); } /** * 删除文章 */ public function…