Joyber 发布的文章

你想要的无需自身开发、用户安装后就能直接用的第三方消息通知APP,大多是开源轻量型工具,用户安装后获取密钥或绑定服务器就能接收通知,适配个人或中小团队的各类通知场景,以下是几款主流工具:

  1. PushDeer
    这款开源工具主打“无需开发自有APP”,用户直接安装官方APP或使用轻应用即可。使用时,用户安装后绑定设备会生成专属pushkey,你只需通过简单的HTTP请求,就能向绑定的设备推送文字、图片、Markdown格式的消息。它还支持自架服务器,数据安全性更高,适合给用户推送订单提醒、系统通知等内容。比如用户开户登录后,将其pushkey与账号绑定,后续就能精准推送相关业务消息。
  2. PushMe
    专为安卓设备设计的轻量级通知APP,安装包不到3M,无多余功能。用户安装后无需注册登录,获取专属push_key就能接收通知,操作零门槛。它支持文本、Markdown、HTML等多种消息格式,还能自定义通知的声音、震动方式,甚至支持语音播报消息。其接口兼容企业微信、钉钉等群机器人,可直接替换对接,适合快速给安卓用户推送服务器监控、业务提醒等消息。
  3. Ntfy
    开源免费的跨平台通知APP,覆盖安卓、iOS及桌面端,核心优势是无需账号和复杂配置。用户安装后订阅对应的主题,你通过HTTP请求就能向该主题推送消息,用户端实时接收。它支持消息优先级设置、文件附件发送,还能端到端加密保障隐私。如果有更高的数据掌控需求,也可自托管服务器,适合给用户推送日常业务通知、离线消息等。
  4. Gotify
    开源的消息推送工具,搭配对应的安卓客户端APP使用。你可先搭建简易的Gotify服务器,用户安装APP后,输入服务器地址和访问令牌就能完成绑定。后续通过调用简单的API,就能向用户推送消息,还能设置消息优先级,让重要的开户成功通知、业务预警等消息优先展示。该工具适合小团队定向给用户推送精准通知,且安全性可通过SSL加密保障。
  5. 灵动通知
    一款轻量化的免费通知管理APP,用户安装后开启无障碍服务权限即可使用,无需额外开发对接。它能聚合接收社交软件、应用推送、系统通知等各类消息,还支持灵动岛胶囊样式弹窗,用户不用解锁手机就能查看消息。如果你的平台消息需适配用户手机的多样化通知场景,这款APP可让用户快速接收和查看消息,适合日常通用类通知推送。

Redis 是可以转移(迁移/搬迁) 的,无论是迁移数据到新实例、新服务器,还是整体迁移 Redis 服务,都有成熟的方案。具体迁移方式需根据场景(如是否停机、数据量大小、是否跨版本/跨架构)选择,以下是最常用的方法和注意事项:

一、核心迁移场景

1. 单机 Redis 迁移到新服务器/新端口
2. 集群(Redis Cluster)迁移(扩缩容/整体搬迁)
3. 主从架构迁移(切换主节点/迁移从节点)
4. 云厂商 Redis 实例迁移到自建/其他云

二、常用迁移方法(按优先级/场景划分)

方法 1:RDB 冷迁移(停机迁移,适合小数据量)

原理:通过 Redis 的 RDB 持久化文件(快照)迁移数据,需停机保证数据一致性。
步骤:

1. 目标实例:停止 Redis 服务,清空原有数据( redis-cli FLUSHALL ),确保配置(端口、密码、持久化策略)与源实例兼容。
2. 源实例:

  • 停机: redis-cli SHUTDOWN SAVE (优雅关闭,自动生成最新 RDB 文件);
  • 若不停机:执行  redis-cli BGSAVE ,等待后台生成 RDB(默认路径  dump.rdb )。
    3. 拷贝 RDB 文件:用  scp / rsync  将源实例的  dump.rdb  拷贝到目标实例的 Redis 数据目录(通过  redis-cli CONFIG GET dir  查看)。
    4. 启动目标 Redis: redis-server redis.conf ,自动加载 RDB 文件恢复数据。
    5. 验证: redis-cli KEYS *  或  dbsize  确认数据一致。

优点:简单、速度快;
缺点:需停机(或 BGSAVE 期间可能有少量数据丢失),不适合高可用场景。

方法 2:AOF 迁移(精准恢复,适合需完整数据)

原理:AOF 是增量日志,记录所有写操作,恢复后数据与源实例完全一致。
步骤:

1. 源实例:执行  redis-cli BGREWRITEAOF (优化 AOF 文件大小),等待重写完成。
2. 拷贝 AOF 文件(默认  appendonly.aof )到目标实例数据目录。
3. 目标实例:修改配置  appendonly yes ,关闭 RDB(可选),启动 Redis 自动加载 AOF。
注意:AOF 文件通常比 RDB 大,加载速度慢,适合数据量小、对一致性要求极高的场景。

方法 3:redis-migrate-tool(开源工具,支持在线迁移)

原理:唯品会开源的 Redis 数据迁移工具,支持单机/集群/主从互迁,在线无感知迁移。
核心优势:

  • 支持增量同步(迁移过程中源实例的新写入会同步到目标);
  • 支持跨版本、跨架构(如单机→集群、集群→单机);
  • 可校验数据一致性。
    基本使用:

    # 1. 安装(需编译)
    git clone https://github.com/vipshop/redis-migrate-tool.git
    cd redis-migrate-tool && make

2. 编写配置文件(migrate.conf)

[source]
type: single  # 源类型:single/cluster/master/slave
servers:
- 192.168.1.100:6379  # 源实例地址
auth: 123456  # 源密码(可选)

[target]
type: single
servers:
- 192.168.1.101:6379  # 目标实例地址
auth: 654321  # 目标密码(可选)

[common]
listen: 0.0.0.0:8888  # 工具监听端口

3. 启动迁移

./redis-migrate-tool -c migrate.conf -o migrate.log -d

4. 校验数据

./redis-migrate-tool -c migrate.conf -C check

 

方法 4:Redis 复制(主从同步,在线迁移)

原理:将目标实例作为源实例的从节点,同步完成后切换为主节点,适合高可用场景。
步骤:

1. 目标实例:清空数据( FLUSHALL ),确保端口/密码与源兼容。
2. 目标实例执行:

# 连接源主节点(若源有密码,加 -a 密码)
redis-cli SLAVEOF 192.168.1.100 6379
# (可选)设置主节点密码(若源有密码)
redis-cli CONFIG SET masterauth 123456

 
3. 等待同步完成:通过  redis-cli INFO replication  查看  master_link_status:up  且  sync_full:1 。
4. 切换目标为独立主节点:
redis-cli SLAVEOF NO ONE

 
5. 割接业务:将应用的 Redis 连接地址改为目标实例。

优点:完全在线,无数据丢失;
缺点:迁移过程中源实例需承担复制压力,适合中小数据量。

方法 5:集群迁移(Redis Cluster)

若源/目标是 Redis 集群,推荐两种方式:

1. redis-cli --cluster reshard:手动迁移槽位(适合扩缩容);
2. redis-migrate-tool:支持集群→集群、集群→单机的全量+增量迁移(推荐)。

三、迁移关键注意事项

1. 数据一致性:

  • 停机迁移优先用  SHUTDOWN SAVE  生成 RDB,避免数据丢失;
  • 在线迁移需确保迁移期间源实例的写操作被同步(如 redis-migrate-tool 增量同步、主从复制)。
    2. 版本兼容:
  • 低版本→高版本:通常兼容,但需注意新特性(如 Redis 7.0 的 ACL);
  • 高版本→低版本:可能不兼容(如 RDB/AOF 格式差异),建议先测试。
    3. 配置兼容:
  • 目标实例需同步源的关键配置:密码(requirepass)、最大内存(maxmemory)、持久化策略(save/appendonly)、过期策略等。
    4. 性能与压力:
  • 在线迁移时,源实例会有复制/网络压力,建议低峰期操作;
  • 大数据量(TB 级)优先用 redis-migrate-tool 分批次迁移。
    5. 验证:
  • 迁移后必做:校验 key 数量( dbsize )、随机抽查 key 的值、检查过期时间( TTL )、验证业务读写正常。
    6. 回滚方案:
  • 迁移完成后,保留源实例一段时间,若目标异常,可快速切回源实例。

四、总结

迁移方式 适用场景 优点 缺点
RDB 冷迁移 小数据量、可停机 简单、速度快 需停机、可能丢增量数据
主从复制 在线迁移、中小数据量 无停机、数据一致 源实例有复制压力
redis-migrate-tool 全场景(单机/集群、大数量) 在线、增量同步、可校验 需编译安装、配置稍复杂
AOF 迁移 数据一致性要求极高 精准恢复 文件大、加载慢

根据你的实际场景(是否停机、数据量、架构)选择即可,中小规模推荐主从复制,大规模/集群推荐redis-migrate-tool。

redis-migrate-tool 支持通过 增量同步(实时复制) 机制,将迁移结束后源库的新写入数据同步到目标库,核心依赖 Redis 的  SYNC / PSYNC  协议实现。

关键实现逻辑与配置:

1. 增量同步的触发:

  • 工具默认执行「全量迁移 + 增量同步」流程:全量数据传输完成后,不会立即退出,而是持续监听源库的写操作(如  SET 、 HSET 、 LPUSH  等),并实时转发到目标库。
  • 即使全量迁移结束后,源库新增的写入数据,也会被实时同步到目标库,确保两端数据最终一致。
    2. 核心配置(必须启用):
  • 配置文件中需显式开启增量同步(默认开启,无需额外修改,但若手动关闭需恢复):

    [source]
    type: single
    addr: 源库IP:端口
    auth: 源库密码(如有)
    
    [target]
    type: single
    addr: 目标库IP:端口
    auth: 目标库密码(如有)
    
    [common]
    # 增量同步开关(默认yes,开启后全量迁移后持续同步)
    incremental_sync = yes
    # 增量同步超时时间(默认3600秒,0表示无限期同步,直到手动停止)
    incremental_sync_timeout = 0

     
    3. 迁移结束后的操作:

  • 若需长期同步:保持工具运行(后台启动,如  nohup redis-migrate-tool -c config.ini & ),直到业务完全切换到目标库。
  • 停止同步:确认应用已全部切换到目标库后,手动终止工具进程( kill -9 进程ID ),增量同步即停止。

注意事项:

  • 仅支持「单源库 → 单目标库」的增量同步,集群场景需额外配置分片映射。
  • 若迁移过程中工具中断,重启后会通过  PSYNC  协议续传增量数据(无需重新全量迁移)。

bun create elysia app  是基于 Bun 构建工具创建 Elysia 框架项目的快捷命令,以下是 结合前端开发场景的完整使用流程+适配建议:

一、核心使用步骤(可直接复制执行)

1. 前提条件:确保已安装 Bun(未安装则先执行)

安装 Bun(Windows/Mac/Linux 通用)

curl -fsSL https://bun.sh/install | bash
 
2. 创建 Elysia 项目:执行命令后自动生成项目结构
bun create elysia app
 
3. 启动项目:进入项目目录并运行开发服务器
cd app
bun run dev
 

启动后默认端口  3000 ,访问  http://localhost:3000  即可看到默认页面。

二、关键适配说明(针对 Vue3/TSX 前端开发)

  • 项目结构:生成的  src/index.ts  是入口文件,可直接编写 Elysia 路由和接口(适配 Hyperf 后端的 API 调用场景)。
  • TS 支持:默认集成 TypeScript,可直接使用  interface  定义类型(与 Vue3/TSX 类型系统一致)。
  • 跨域配置:若需对接本地 Hyperf 后端,在入口文件添加跨域中间件:
    import { Elysia } from 'elysia'

const app = new Elysia()
.use(cors()) // 需先安装:bun add @elysiajs/cors
.get('/', () => 'Hello Elysia')
.listen(3000)
 

三、扩展建议

  • 若需结合 Vue3 前端开发,可将 Elysia 作为 BFF 层(中间层),转发请求到 Hyperf 后端,简化前端接口调用。
  • 支持热更新:开发时修改代码自动重启服务器,无需手动刷新。

Bun 是一个用 Zig 编写的全栈 JavaScript/TypeScript 运行时与集成工具包,目标是替代 Node.js,提供更快的性能和更统一的开发体验。

核心定位与优势

  • 一站式工具链:集成运行时、包管理器、打包器、测试运行器,单命令搞定开发。
  • 极致性能:包安装速度通常是 npm 的 20-100 倍,启动与执行更快。
  • 原生兼容:支持 package.json、node_modules 与多数 npm 包,可直接运行 Node.js 项目。
  • 内置支持:原生解析 TypeScript/JSX,自动加载 .env,内置 fetch/WebSocket 等 Web API。

常用命令(可直接复制)

安装 Bun(Windows/Mac/Linux)

curl -fsSL https://bun.sh/install | bash

创建项目(如 Elysia)

bun create elysia app

安装依赖

bun install # 替代 npm install
bun add axios # 替代 npm add axios

运行脚本与开发

bun run dev # 启动开发服务器
bun test # 运行测试
bun build # 打包项目
 

对 Vue3/TSX 开发的价值

  • 与 Vue3/TSX 类型系统无缝衔接,无需额外配置即可解析 TS/TSX。
  • 可作为 BFF 层快速搭建接口服务,对接 Hyperf 后端更高效。
  • 热更新与快速启动提升开发效率,适合快速迭代的前端项目。

现状与注意事项

  • 已被 Anthropic 收购,路线图聚焦高性能与 Node.js 兼容性。
  • 适合新项目;老 Node.js 项目迁移前建议先做兼容性测试。

sizeg/yii2-jwt 扩展本身未直接提供 refresh_token 的生成与管理逻辑(核心仅负责 JWT 令牌的签名/验证),但可基于 JWT 标准和业务需求,手动实现「access_token + refresh_token 双令牌机制」——核心思路是:生成两个功能不同的 JWT 令牌,access_token 短期有效(用于接口访问),refresh_token 长期有效(用于刷新 access_token)。

以下是完整实现方案(兼容 Yii2 2.0.53 版本),包含「生成、存储、刷新、验证」全流程:

一、核心前提

  1. 已安装 sizeg/yii2-jwt 扩展:

    composer require sizeg/yii2-jwt
  2. 已在 Yii2 配置文件中配置 JWT 组件(config/main.php):

    'components' => [
        'jwt' => [
            'class' => \sizeg\jwt\Jwt::class,
            'key' => 'your-secret-key-32bytes-long-123456', // 密钥(建议 ≥32 位,存环境变量)
            'jwtValidationData' => \app\components\JwtValidationData::class, // 自定义验证类
            'algorithm' => \Lcobucci\JWT\Signer\Hmac\Sha256::class, // 加密算法(默认 SHA256)
        ],
    ],
  3. 自定义 JWT 验证类(app/components/JwtValidationData.php):

    namespace app\components;
    
    use sizeg\jwt\JwtValidationData;
    
    class JwtValidationData extends JwtValidationData
    {
        public function init()
        {
            parent::init();
            // 验证发行人(可选,需与生成时一致)
            $this->validateIssuer('https://your-domain.com');
            // 验证受众(可选)
            $this->validateAudience('your-app-name');
        }
    }

二、实现 refresh_token 的核心逻辑

1. 令牌设计原则

令牌类型有效期用途存储方式包含 payload 信息
access_token短期(15-60 分钟)接口访问验证前端存储(localStorage/Header)用户 ID、角色、过期时间等
refresh_token长期(7-30 天)刷新 access_token数据库+前端存储用户 ID、刷新令牌 ID、过期时间

2. 数据库表设计(存储 refresh_token,确保唯一性和可吊销)

创建 user_refresh_token 表(用于存储用户的有效 refresh_token,支持吊销/过期管理):

CREATE TABLE `user_refresh_token` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '用户ID',
  `refresh_token` varchar(255) NOT NULL COMMENT '刷新令牌(JWT字符串)',
  `token_id` varchar(64) NOT NULL COMMENT '令牌唯一标识(避免重复)',
  `expired_at` int(11) NOT NULL COMMENT '过期时间戳(秒)',
  `created_at` int(11) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` int(11) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1-有效,0-无效(吊销)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_token_id` (`token_id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_expired_at` (`expired_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户刷新令牌表';

3. 生成双令牌(access_token + refresh_token

在用户登录成功后,同时生成两个 JWT 令牌,refresh_token 存入数据库和返回给前端:

namespace app\services;

use app\models\UserRefreshToken;
use sizeg\jwt\Jwt;
use Yii;
use Lcobucci\JWT\Token\Builder;
use Lcobucci\JWT\Encoding\Charset;
use Lcobucci\JWT\Encoding\JoseEncoder;

class AuthService
{
    /**
     * 生成双令牌(access_token + refresh_token)
     * @param int $userId 用户ID
     * @return array ['access_token' => string, 'refresh_token' => string, 'expires_in' => int]
     */
    public function generateTokens(int $userId)
    {
        $jwt = Yii::$app->jwt;
        $now = time();

        // -------------------------- 1. 生成 access_token(短期有效,15分钟)--------------------------
        $accessTokenExpire = $now + 900; // 15*60=900秒
        $accessToken = $jwt->getBuilder()
            ->issuedBy('https://your-domain.com') // 发行人(与验证类一致)
            ->permittedFor('your-app-name') // 受众(与验证类一致)
            ->identifiedBy(uniqid('access_', true)) // 令牌唯一ID
            ->issuedAt($now) // 签发时间
            ->expiresAt($accessTokenExpire) // 过期时间
            ->withClaim('user_id', $userId) // 自定义载荷:用户ID
            ->withClaim('token_type', 'access') // 标记令牌类型
            ->getToken($jwt->getSigner(), $jwt->getSigningKey()); // 签名生成令牌

        // -------------------------- 2. 生成 refresh_token(长期有效,7天)--------------------------
        $refreshTokenExpire = $now + 604800; // 7*24*3600=604800秒
        $tokenId = uniqid('refresh_', true); // 刷新令牌唯一标识(用于数据库存储和吊销)
        
        $refreshToken = $jwt->getBuilder()
            ->issuedBy('https://your-domain.com')
            ->permittedFor('your-app-name')
            ->identifiedBy($tokenId) // 关联数据库的 token_id
            ->issuedAt($now)
            ->expiresAt($refreshTokenExpire)
            ->withClaim('user_id', $userId)
            ->withClaim('token_type', 'refresh') // 标记令牌类型(关键:区分 access/refresh)
            ->getToken($jwt->getSigner(), $jwt->getSigningKey());

        // -------------------------- 3. 存储 refresh_token 到数据库(用于后续验证和吊销)--------------------------
        $this->saveRefreshToken($userId, (string)$refreshToken, $tokenId, $refreshTokenExpire);

        // 返回结果(前端存储 access_token 和 refresh_token)
        return [
            'access_token' => (string)$accessToken,
            'refresh_token' => (string)$refreshToken,
            'expires_in' => $accessTokenExpire - $now, // access_token 剩余有效期(秒)
            'refresh_expires_in' => $refreshTokenExpire - $now, // refresh_token 剩余有效期(秒)
        ];
    }

    /**
     * 保存 refresh_token 到数据库
     */
    private function saveRefreshToken(int $userId, string $refreshToken, string $tokenId, int $expiredAt)
    {
        // 1. 先失效该用户之前的所有 refresh_token(可选:单设备登录,避免多令牌)
        UserRefreshToken::updateAll(
            ['status' => 0],
            ['user_id' => $userId, 'status' => 1]
        );

        // 2. 新增当前 refresh_token
        $model = new UserRefreshToken();
        $model->user_id = $userId;
        $model->refresh_token = $refreshToken;
        $model->token_id = $tokenId;
        $model->expired_at = $expiredAt;
        $model->created_at = time();
        $model->save(false); // 无需验证,直接保存
    }
}

4. 登录接口调用(生成令牌)

在控制器中调用服务类,返回双令牌给前端:

namespace app\controllers;

use app\services\AuthService;
use Yii;
use yii\web\Controller;

class AuthController extends Controller
{
    public function actionLogin()
    {
        // 1. 模拟用户登录验证(实际场景:验证用户名密码)
        $userId = 1001; // 登录成功后获取的用户ID

        // 2. 生成双令牌
        $authService = new AuthService();
        $tokens = $authService->generateTokens($userId);

        // 3. 返回结果(前端存储 tokens)
        return $this->asJson([
            'code' => 200,
            'msg' => '登录成功',
            'data' => $tokens,
        ]);
    }
}

三、刷新 access_token(核心:用 refresh_token 换新的 access_token

access_token 过期后,前端用 refresh_token 调用刷新接口,获取新的 access_token(无需重新登录):

namespace app\services;

use app\models\UserRefreshToken;
use sizeg\jwt\Jwt;
use Yii;
use Lcobucci\JWT\Exception\TokenExpiredException;
use Lcobucci\JWT\Exception\TokenInvalidException;

class AuthService
{
    // ... 之前的 generateTokens() 和 saveRefreshToken() 方法 ...

    /**
     * 用 refresh_token 刷新 access_token
     * @param string $refreshToken 前端传入的 refresh_token
     * @return array|null 新的 tokens,失败返回 null
     */
    public function refreshAccessToken(string $refreshToken)
    {
        $jwt = Yii::$app->jwt;

        try {
            // -------------------------- 1. 验证 refresh_token 有效性(JWT 签名+过期)--------------------------
            $token = $jwt->getParser(new JoseEncoder())->parse($refreshToken);
            $validationData = Yii::createObject(\app\components\JwtValidationData::class);
            
            // 验证 JWT 签名、发行人、受众
            if (!$jwt->validateToken($token, $validationData)) {
                Yii::error('refresh_token 验证失败(签名/发行人/受众无效)');
                return null;
            }

            // -------------------------- 2. 提取 refresh_token 载荷信息 --------------------------
            $payload = $token->getClaims();
            $userId = $payload['user_id']->getValue(); // 用户ID
            $tokenType = $payload['token_type']->getValue(); // 令牌类型
            $tokenId = $payload['jti']->getValue(); // 令牌唯一ID(关联数据库)
            $expiredAt = $payload['exp']->getValue(); // 过期时间戳

            // 验证令牌类型是否为 refresh
            if ($tokenType !== 'refresh') {
                Yii::error('无效的令牌类型(非 refresh_token)');
                return null;
            }

            // -------------------------- 3. 验证数据库中的 refresh_token(是否有效/未吊销)--------------------------
            $dbToken = UserRefreshToken::findOne([
                'user_id' => $userId,
                'token_id' => $tokenId,
                'refresh_token' => $refreshToken,
                'status' => 1,
                'expired_at' => ['>', time()], // 未过期
            ]);

            if (!$dbToken) {
                Yii::error('refresh_token 不存在或已失效');
                return null;
            }

            // -------------------------- 4. 生成新的 access_token(可选:同时生成新的 refresh_token,滚动更新)--------------------------
            $newTokens = $this->generateTokens($userId);

            // (可选)失效旧的 refresh_token(滚动更新,增强安全性)
            $dbToken->status = 0;
            $dbToken->save(false);

            return $newTokens;

        } catch (TokenExpiredException $e) {
            Yii::error('refresh_token 已过期:' . $e->getMessage());
            return null;
        } catch (TokenInvalidException $e) {
            Yii::error('refresh_token 无效:' . $e->getMessage());
            return null;
        } catch (\Exception $e) {
            Yii::error('刷新 access_token 失败:' . $e->getMessage());
            return null;
        }
    }
}

刷新接口控制器调用

namespace app\controllers;

use app\services\AuthService;
use Yii;
use yii\web\Controller;

class AuthController extends Controller
{
    public function actionRefreshToken()
    {
        // 1. 获取前端传入的 refresh_token(从 Header 或请求体)
        $refreshToken = Yii::$app->request->post('refresh_token');
        if (!$refreshToken) {
            return $this->asJson([
                'code' => 400,
                'msg' => 'refresh_token 不能为空',
            ]);
        }

        // 2. 刷新 access_token
        $authService = new AuthService();
        $newTokens = $authService->refreshAccessToken($refreshToken);

        if (!$newTokens) {
            return $this->asJson([
                'code' => 401,
                'msg' => 'refresh_token 无效或已过期,请重新登录',
            ]);
        }

        // 3. 返回新的 tokens
        return $this->asJson([
            'code' => 200,
            'msg' => '刷新成功',
            'data' => $newTokens,
        ]);
    }
}

四、关键注意事项(安全+兼容性)

1. 安全性保障

  • refresh_token 必须存储在数据库:用于验证令牌有效性和吊销(如用户退出登录时,标记 status=0);
  • 滚动更新 refresh_token:每次刷新 access_token 时,生成新的 refresh_token 并失效旧的,避免长期有效令牌泄露风险;
  • 密钥安全:JWT 密钥(key)需 ≥32 位,存环境变量(如 getenv('JWT_SECRET_KEY')),禁止硬编码到代码;
  • access_token 短期有效:减少令牌泄露后的风险窗口(建议 15-60 分钟)。

2. 兼容性处理(Yii2 2.0.53 + sizeg/yii2-jwt 版本)

  • sizeg/yii2-jwt 版本较低(如 ≤1.0),getParser() 方法可能不支持 JoseEncoder,需调整解析逻辑:

    // 低版本解析 token 写法(无 JoseEncoder)
    $token = $jwt->getParser()->parse($refreshToken);
  • 确保 Lcobucci\JWT 依赖版本兼容(sizeg/yii2-jwt 通常依赖 lcobucci/jwt:^3.0),若报错需调整 composer.json

    "require": {
        "lcobucci/jwt": "3.4.*" // 固定兼容版本
    }

3. 吊销 refresh_token(用户退出登录)

用户退出时,需将对应的 refresh_token 标记为无效,避免被恶意使用:

public function actionLogout()
{
    $refreshToken = Yii::$app->request->post('refresh_token');
    if (!$refreshToken) {
        return $this->asJson(['code' => 400, 'msg' => 'refresh_token 不能为空']);
    }

    // 查找并失效该 refresh_token
    UserRefreshToken::updateAll(
        ['status' => 0],
        ['refresh_token' => $refreshToken, 'status' => 1]
    );

    return $this->asJson(['code' => 200, 'msg' => '退出成功']);
}

五、总结

sizeg/yii2-jwt 生成 refresh_token 的核心是「手动构建双 JWT 令牌 + 数据库存储 refresh_token」:

  1. access_token 短期有效,用于接口访问;
  2. refresh_token 长期有效,存入数据库,用于刷新 access_token
  3. 刷新时验证 refresh_token 的 JWT 签名、数据库状态、过期时间;
  4. 支持令牌滚动更新和吊销,兼顾安全性和用户体验。

按上述方案实现后,即可完成「登录生成双令牌 → 接口用 access_token 访问 → 过期用 refresh_token 刷新」的完整认证流程。

在 Yii2 中批量添加数据(批量插入 MySQL),核心是利用 ActiveRecordbatchInsert() 方法或 Query 构建器的 batchInsert(),避免循环调用 save() 导致的性能问题(减少数据库连接次数,一次 SQL 插入多条数据)。

以下是 3 种实用方案(按推荐优先级排序),覆盖不同场景(简单批量插入、带验证、关联数据批量插入),并兼容你的 Yii2 2.0.53 版本:

一、核心前提

假设模型为 User(对应表 sp_user),需批量插入的字段为 nameagestatus,待插入数据格式如下:

// 原始批量数据(可含多余字段,后续会过滤)
$batchData = [
    ['name' => '张三', 'age' => 20, 'status' => 1, 'extra' => '无效字段'],
    ['name' => '李四', 'age' => 22, 'status' => 1, 'extra' => '无效字段'],
    ['name' => '王五', 'age' => 25, 'status' => 0, 'extra' => '无效字段'],
];

方案 1:ActiveRecord::batchInsert()(最简洁,推荐)

Yii2 ActiveRecord 静态方法 batchInsert(),直接构造批量插入 SQL,性能最优(一次 SQL 执行),支持自动过滤无效字段。

用法示例:

use app\models\User;

// 步骤 1:定义允许插入的字段(避免无效字段报错)
$allowedFields = ['name', 'age', 'status'];

// 步骤 2:过滤批量数据中的无效字段(仅保留允许的字段)
$validBatchData = [];
foreach ($batchData as $item) {
    // 仅保留 $allowedFields 中的字段,自动过滤 extra 等无效字段
    $validItem = array_intersect_key($item, array_flip($allowedFields));
    $validBatchData[] = $validItem;
}

// 步骤 3:批量插入(核心方法)
$rowCount = User::batchInsert(
    $allowedFields, // 允许插入的字段(必须与表字段一致)
    $validBatchData // 过滤后的批量数据
)->execute();

// 结果:$rowCount 为成功插入的行数(此处为 3)
echo "批量插入成功,共插入 {$rowCount} 条数据";

生成的 SQL(性能最优):

INSERT INTO `sp_user` (`name`, `age`, `status`) 
VALUES ('张三', 20, 1), ('李四', 22, 1), ('王五', 25, 0);

特点:

  • 性能最好(一次 SQL 插入多条,无循环连接数据库);
  • 需手动指定允许插入的字段(避免无效字段);
  • 不触发模型的 beforeSave()/afterSave() 钩子,不执行数据验证(需提前确保数据合法)。

方案 2:带数据验证的批量插入(安全优先)

若需对批量数据进行验证(如 name 必填、age 为整数),需循环验证每条数据,再批量插入,兼顾安全与性能。

用法示例:

use app\models\User;
use yii\db\Exception;

$allowedFields = ['name', 'age', 'status'];
$validBatchData = [];

// 步骤 1:循环验证每条数据(触发模型规则)
foreach ($batchData as $item) {
    $model = new User();
    // 加载数据并过滤无效字段
    $model->load(['User' => $item], '');
    // 验证数据(触发模型 rules() 中的规则)
    if ($model->validate()) {
        // 仅保留允许插入的字段,加入有效数据集合
        $validItem = $model->getAttributes($allowedFields);
        $validBatchData[] = $validItem;
    } else {
        // 处理验证失败的情况(如打印错误信息)
        echo "数据验证失败:" . json_encode($model->errors) . "\n";
    }
}

// 步骤 2:批量插入验证通过的数据
if (!empty($validBatchData)) {
    try {
        $rowCount = User::batchInsert($allowedFields, $validBatchData)->execute();
        echo "批量插入成功,共插入 {$rowCount} 条数据";
    } catch (Exception $e) {
        echo "插入失败:" . $e->getMessage();
    }
}

特点:

  • 触发模型验证(rules())和属性过滤(getAttributes()),数据更安全;
  • 验证失败的数据会被跳过,不影响整体插入;
  • 性能略低于方案 1(多了循环验证步骤),但仍优于循环 save()

方案 3:Query 构建器批量插入(兼容非 ActiveRecord 场景)

若不依赖 ActiveRecord 模型(如临时表、视图),可直接用 Query 构建器的 batchInsert(),用法与方案 1 类似,但更灵活。

用法示例:

use yii\db\Query;
use yii\db\Exception;

$allowedFields = ['name', 'age', 'status'];
$validBatchData = [];

// 过滤无效字段(同方案 1)
foreach ($batchData as $item) {
    $validItem = array_intersect_key($item, array_flip($allowedFields));
    $validBatchData[] = $validItem;
}

// 批量插入(指定表名和字段)
try {
    $rowCount = (new Query())
        ->insert('sp_user', $allowedFields) // 表名 + 字段
        ->batchInsert($validBatchData) // 批量数据
        ->execute();
    echo "批量插入成功,共插入 {$rowCount} 条数据";
} catch (Exception $e) {
    echo "插入失败:" . $e->getMessage();
}

特点:

  • 不依赖 ActiveRecord 模型,直接操作表名;
  • 语法简洁,性能与方案 1 一致;
  • 同样不触发模型钩子和验证,需手动确保数据合法。

二、关键优化与避坑要点

1. 性能优化:控制单次插入数量

MySQL 对单次插入的条数有默认限制(取决于 max_allowed_packet 配置),若批量数据超过 1000 条,建议分批次插入(避免 SQL 过长导致失败):

$batchSize = 1000; // 每批插入 1000 条
$totalData = $validBatchData;
$totalCount = count($totalData);

for ($i = 0; $i < $totalCount; $i += $batchSize) {
    // 截取每批数据
    $batch = array_slice($totalData, $i, $batchSize);
    User::batchInsert($allowedFields, $batch)->execute();
}

2. 自动过滤无效字段(简化写法)

若不想手动定义 $allowedFields,可通过模型的 attributes() 自动获取所有字段(对应数据库表字段):

// 自动获取模型所有有效字段(无需手动写 $allowedFields)
$allowedFields = $model->attributes(); // 或 User::getTableSchema()->columnNames;

// 过滤批量数据
foreach ($batchData as $item) {
    $validItem = array_intersect_key($item, array_flip($allowedFields));
    $validBatchData[] = $validItem;
}

3. 处理自增 ID

若表有自增 ID(如 id),无需在 $allowedFields 中包含 id,MySQL 会自动生成自增 ID。

4. 事务支持(确保数据一致性)

若批量插入需保证「要么全成功,要么全失败」,需包裹事务:

use yii\db\Transaction;

$db = User::getDb();
$transaction = $db->beginTransaction(Transaction::READ_COMMITTED);

try {
    // 批量插入
    $rowCount = User::batchInsert($allowedFields, $validBatchData)->execute();
    $transaction->commit(); // 全部成功,提交事务
    echo "批量插入成功,共插入 {$rowCount} 条数据";
} catch (Exception $e) {
    $transaction->rollBack(); // 部分失败,回滚事务
    echo "插入失败:" . $e->getMessage();
}

5. 兼容 JSON 字段(若有)

若批量插入包含 JSON 字段(如 profile),需确保数据为数组或 JSON 字符串,Yii2 会自动处理:

$batchData = [
    ['name' => '张三', 'profile' => ['age' => 20, 'city' => '北京']], // 数组格式
    ['name' => '李四', 'profile' => json_encode(['age' => 22, 'city' => '上海'])], // JSON 字符串格式
];
// 直接批量插入,Yii2 会自动将数组转为 JSON 字符串
User::batchInsert(['name', 'profile'], $batchData)->execute();

三、场景对比与选择

方案优点缺点适用场景
方案 1(ActiveRecord::batchInsert)性能最优,简洁无验证,无钩子数据已验证、无需触发模型逻辑
方案 2(带验证)数据安全,支持验证性能略低需验证数据(如用户输入数据)
方案 3(Query 构建器)不依赖 ActiveRecord无模型相关功能临时表、视图、非模型场景

推荐优先级:

  • 大多数场景(数据合法、无需验证)→ 方案 1;
  • 用户输入数据、需验证 → 方案 2;
  • 非 ActiveRecord 场景 → 方案 3。

四、最终简化写法(方案 1 + 自动过滤字段 + 事务)

use app\models\User;
use yii\db\Exception;
use yii\db\Transaction;

$batchData = [/* 你的批量数据 */];
$model = new User();
$allowedFields = $model->attributes(); // 自动获取所有有效字段
$validBatchData = [];

// 自动过滤无效字段
foreach ($batchData as $item) {
    $validItem = array_intersect_key($item, array_flip($allowedFields));
    $validBatchData[] = $validItem;
}

// 事务 + 批量插入
$db = User::getDb();
$transaction = $db->beginTransaction(Transaction::READ_COMMITTED);

try {
    if (!empty($validBatchData)) {
        $rowCount = User::batchInsert($allowedFields, $validBatchData)->execute();
    }
    $transaction->commit();
    echo "批量插入成功,共插入 {$rowCount} 条数据";
} catch (Exception $e) {
    $transaction->rollBack();
    echo "插入失败:" . $e->getMessage();
}

这种写法兼顾了 性能、安全性、简洁性,能自动过滤无效字段,避免「字段不存在」报错,同时通过事务保证数据一致性,适合绝大多数批量添加场景。