在 Yii 2.0 中,Redis 的活动记录(Active Record),基于 row 查看结构,与模型字段顺序不一致的排查分析

1、在 Yii 2.0 中,Redis 的活动记录(Active Record),基于 row 查看结构,与模型字段顺序不一致。如图1

图1

2、查看 Redis 活动记录类,/common/models/redis/DouyinWebAppUserAccessToken.php

<?php

namespace common\models\redis;

use Yii;
use common\components\redis\ActiveRecord;

/**
 * This is the model class for table "{{%douyin_web_app_user_access_token}}".
 *
 * @property integer $id
 * @property integer $douyin_web_app_id 抖音的第三方服务平台网站应用ID
 * @property integer $channel_app_source_id 渠道的应用的来源ID
 * @property string $channel_app_source_uuid 渠道的应用的来源ID(UUID)
 * @property integer $douyin_web_app_user_id 抖音的第三方服务平台网站应用的用户ID
 * @property string $douyin_web_app_user_uuid 抖音的第三方服务平台网站应用的用户ID(UUID)
 * @property string $grant_type 授权类型,authorization_code:第三方平台授权模式
 * @property string $access_token 访问令牌
 * @property integer $expires_in 访问令牌有效期,单位(秒)
 * @property integer $expires_at 访问令牌有效截止时间
 * @property string $refresh_token 刷新令牌
 * @property integer $refresh_token_expires_in 刷新令牌有效期,单位(秒)
 * @property integer $refresh_token_expires_at 刷新令牌有效截止时间
 * @property string $scope 权限范围
 * @property string $open_id 授权第三方用户的用户唯一标识
 * @property integer $status 状态,0:禁用;1:启用
 * @property integer $is_deleted 是否被删除,0:否;1:是
 * @property integer $created_at 创建时间
 * @property integer $updated_at 更新时间
 * @property integer $deleted_at 删除时间
 */class DouyinWebAppUserAccessToken extends ActiveRecord
{
    /**
     * @return array the list of attributes for this record
     */    public function attributes()
    {
        return ['id', 'douyin_web_app_id', 'channel_app_source_id', 'channel_app_source_uuid', 'douyin_web_app_user_id', 'douyin_web_app_user_uuid', 'grant_type', 'access_token','expires_in', 'expires_at', 'refresh_token', 'refresh_token_expires_in', 'refresh_token_expires_at', 'scope', 'open_id', 'status', 'is_deleted', 'created_at', 'updated_at', 'deleted_at'];
    }

    /**
     * @inheritdoc
     */    public function rules()
    {
        return [
            [['id', 'douyin_web_app_id', 'channel_app_source_id', 'channel_app_source_uuid', 'douyin_web_app_user_id', 'douyin_web_app_user_uuid', 'grant_type', 'access_token','expires_in', 'expires_at', 'refresh_token', 'refresh_token_expires_in', 'refresh_token_expires_at', 'scope', 'open_id', 'status', 'is_deleted', 'created_at', 'updated_at', 'deleted_at'], 'safe'],
        ];
    }

    /**
     * @inheritdoc
     */    public function attributeLabels()
    {
        return [
            'id' => Yii::t('model/redis/douyin-web-app-user-access-token', 'ID'),
            'douyin_web_app_id' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Douyin Web App Id'),
            'channel_app_source_id' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Channel App Source Id'),
            'channel_app_source_uuid' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Channel App Source Uuid'),
            'douyin_web_app_user_id' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Douyin Web App User Id'),
            'douyin_web_app_user_uuid' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Douyin Web App User Uuid'),
            'grant_type' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Grant Type'),
            'access_token' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Access Token'),
            'expires_in' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Expires In'),
            'expires_at' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Expires At'),
            'refresh_token' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Refresh Token'),
            'refresh_token_expires_in' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Refresh Token Expires In'),
            'refresh_token_expires_at' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Refresh Token Expires At'),
            'scope' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Scope'),
            'open_id' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Open Id'),
            'status' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Status'),
            'is_deleted' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Is Deleted'),
            'created_at' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Created At'),
            'updated_at' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Updated At'),
            'deleted_at' => Yii::t('model/redis/douyin-web-app-user-access-token', 'Deleted At'),
        ];
    }

    /**
     * {@inheritdoc}
     * @return DouyinWebAppUserAccessTokenQuery the active query used by this AR class.
     */    public static function find()
    {
        return new DouyinWebAppUserAccessTokenQuery(get_called_class());
    }
}

3、打印一下 Redis 的模型对象(更新前),$redisDouyinWebAppUserAccessToken。

    /**
     * 基于 Access Token 插入/更新数据至抖音的第三方服务平台网站应用的用户的访问令牌(Redis)
     * @param array $data 数据
     * 格式如下:
     *
     * [
     *     'douyinWebAppId' => 1, // 抖音的第三方服务平台网站应用ID
     *     'channelAppSourceId' => 42, // 渠道的应用的来源ID
     *     'channelAppSourceUuid' => '4dd4924a223d11ebb87054ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *     'douyinWebAppUserId' => 4, // 抖音的第三方服务平台网站应用的用户ID
     *     'douyinWebAppUserUuid' => '4dd5a770223d11eb988c54ee75d2ebc1', // 抖音的第三方服务平台网站应用的用户ID(UUID)
     *     'grantType' => 'authorization_code', // 授权类型,authorization_code:第三方平台授权模式
     *     'access_token' => 'act.fbb67f15948da5ab6d832b69daa58b7e3HEgNAeS3vv7hOBVqjNpjiJ1fZYY', // 访问令牌
     *     'captcha' => '', //
     *     'desc_url' => '', //
     *     'description' => '', // 错误码描述
     *     'error_code' => 0, // 错误码
     *     'expires_in' => 1296000, // 访问令牌有效期,单位(秒)
     *     'open_id' => '3c61649c-2cd5-4479-b2a3-0c89e8f808f8', // 授权第三方用户的用户唯一标识
     *     'refresh_expires_in' => 2592000, // 刷新令牌有效期,单位(秒)
     *     'refresh_token' => 'rft.1b82e0d7acbea76d68f45b44461e649fQzF6hSBGPuq9w7FVWv0cJ53J4aSg', // 刷新令牌
     *     'scope' => 'fans.data,fans.list,following.list,im,user_info,video.comment,video.create,video.data,video.delete,video.list', // 权限范围
     * ]
     *
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => true, // 成功
     *     'data' => [ // array
     *         'channel_app_source_uuid' => 'ddb8822607ec11e9beda54ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *     ]
     * ]
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 214010, // 返回码
     *     'message' => '', // 说明
     * ]
     *
     * @throws ServerErrorHttpException
     */    public function saveModel($data)
    {
        /* 基于抖音的第三方服务平台网站应用的用户ID查找状态为启用的单个资源 */        $redisDouyinWebAppUserAccessToken = RedisDouyinWebAppUserAccessToken::find()->where(['douyin_web_app_user_id' => $data['douyinWebAppUserId']])->isDeletedNo()->enabled()->one();
        if (!isset($redisDouyinWebAppUserAccessToken)) {
            $redisDouyinWebAppUserAccessToken = new RedisDouyinWebAppUserAccessToken();
        }

        $time = time();
        // 设置访问令牌的有效截止时间
        $accessTokenExpireAt = $time + $data['expires_in'] - Yii::$app->params['accessToken']['timeOut'];
        // 设置刷新令牌的有效截止时间
        $refreshTokenExpireAt = $time + $data['refresh_expires_in'] - Yii::$app->params['refreshToken']['timeOut'];
        $redisDouyinWebAppUserAccessToken->attributes = [
            'douyin_web_app_id' => $data['douyinWebAppId'],
            'channel_app_source_id' => $data['channelAppSourceId'],
            'channel_app_source_uuid' => $data['channelAppSourceUuid'],
            'douyin_web_app_user_id' => $data['douyinWebAppUserId'],
            'douyin_web_app_user_uuid' => $data['douyinWebAppUserUuid'],
            'grant_type' => $data['grantType'],
            'access_token' => $data['access_token'],
            'expires_in' => $data['expires_in'],
            'expires_at' => $accessTokenExpireAt,
            'refresh_token' => $data['refresh_token'],
            'refresh_token_expires_in' => $data['refresh_expires_in'],
            'refresh_token_expires_at' => $refreshTokenExpireAt,
            'scope' => $data['scope'],
            'open_id' => $data['open_id'],
            'status' => RedisDouyinWebAppUserAccessToken::STATUS_ENABLED,
        ];

        print_r($redisDouyinWebAppUserAccessToken);
        exit;

        $return = [];
        if ($redisDouyinWebAppUserAccessToken->save()) {
            $return = ['status' => true, 'data' => ['channel_app_source_uuid' => $data['channelAppSourceUuid']]];
        } elseif ($redisDouyinWebAppUserAccessToken->hasErrors()) {
            $firstError = '';
            foreach ($redisDouyinWebAppUserAccessToken->getFirstErrors() as $message) {
                $firstError = $message;
                break;
            }
            $return = ['status' => false, 'code' => 215004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '215004'), ['first_error' => $firstError]))];
        } elseif (!$redisDouyinWebAppUserAccessToken->hasErrors()) {
            throw new ServerErrorHttpException('Failed to insert/update the object (Douyin\'s third-party service platform website application user\'s access tokenn (Redis)) for unknown reason.');
        }

        return $return;
    }

4、打印一下 Redis 的模型对象(更新前),$redisDouyinWebAppUserAccessToken。结果如下:

frontend\models\redis\DouyinWebAppUserAccessToken Object
(
    [_attributes:yii\db\BaseActiveRecord:private] => Array
        (
            [douyin_web_app_id] => 1
            [channel_app_source_id] => 46
            [channel_app_source_uuid] => 24a4a41a225a11eb8b4a54ee75d2ebc1
            [douyin_web_app_user_id] => 7
            [douyin_web_app_user_uuid] => 24a5cc46225a11ebb31154ee75d2ebc1
            [grant_type] => authorization_code
            [access_token] => act.fbb67f15948da5ab6d832b69daa58b7e3HEgNAeS3vv7hOBVqjNpjiJ1fZYY
            [expires_in] => 1296000
            [expires_at] => 1606201312
            [refresh_token] => rft.1b82e0d7acbea76d68f45b44461e649fQzF6hSBGPuq9w7FVWv0cJ53J4aSg
            [refresh_token_expires_in] => 2592000
            [refresh_token_expires_at] => 1607490412
            [scope] => fans.data,fans.list,following.list,im,user_info,video.comment,video.create,video.data,video.delete,video.list
            [open_id] => 3c61649c-2cd5-4479-b2a3-0c89e8f808f8
            [status] => 1
        )

)

5、打印一下 Redis 的模型对象(更新后),$redisDouyinWebAppUserAccessToken。

    /**
     * 基于 Access Token 插入/更新数据至抖音的第三方服务平台网站应用的用户的访问令牌(Redis)
     * @param array $data 数据
     * 格式如下:
     *
     * [
     *     'douyinWebAppId' => 1, // 抖音的第三方服务平台网站应用ID
     *     'channelAppSourceId' => 42, // 渠道的应用的来源ID
     *     'channelAppSourceUuid' => '4dd4924a223d11ebb87054ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *     'douyinWebAppUserId' => 4, // 抖音的第三方服务平台网站应用的用户ID
     *     'douyinWebAppUserUuid' => '4dd5a770223d11eb988c54ee75d2ebc1', // 抖音的第三方服务平台网站应用的用户ID(UUID)
     *     'grantType' => 'authorization_code', // 授权类型,authorization_code:第三方平台授权模式
     *     'access_token' => 'act.fbb67f15948da5ab6d832b69daa58b7e3HEgNAeS3vv7hOBVqjNpjiJ1fZYY', // 访问令牌
     *     'captcha' => '', //
     *     'desc_url' => '', //
     *     'description' => '', // 错误码描述
     *     'error_code' => 0, // 错误码
     *     'expires_in' => 1296000, // 访问令牌有效期,单位(秒)
     *     'open_id' => '3c61649c-2cd5-4479-b2a3-0c89e8f808f8', // 授权第三方用户的用户唯一标识
     *     'refresh_expires_in' => 2592000, // 刷新令牌有效期,单位(秒)
     *     'refresh_token' => 'rft.1b82e0d7acbea76d68f45b44461e649fQzF6hSBGPuq9w7FVWv0cJ53J4aSg', // 刷新令牌
     *     'scope' => 'fans.data,fans.list,following.list,im,user_info,video.comment,video.create,video.data,video.delete,video.list', // 权限范围
     * ]
     *
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => true, // 成功
     *     'data' => [ // array
     *         'channel_app_source_uuid' => 'ddb8822607ec11e9beda54ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *     ]
     * ]
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 214010, // 返回码
     *     'message' => '', // 说明
     * ]
     *
     * @throws ServerErrorHttpException
     */    public function saveModel($data)
    {
        /* 基于抖音的第三方服务平台网站应用的用户ID查找状态为启用的单个资源 */        $redisDouyinWebAppUserAccessToken = RedisDouyinWebAppUserAccessToken::find()->where(['douyin_web_app_user_id' => $data['douyinWebAppUserId']])->isDeletedNo()->enabled()->one();
        if (!isset($redisDouyinWebAppUserAccessToken)) {
            $redisDouyinWebAppUserAccessToken = new RedisDouyinWebAppUserAccessToken();
        }

        $time = time();
        // 设置访问令牌的有效截止时间
        $accessTokenExpireAt = $time + $data['expires_in'] - Yii::$app->params['accessToken']['timeOut'];
        // 设置刷新令牌的有效截止时间
        $refreshTokenExpireAt = $time + $data['refresh_expires_in'] - Yii::$app->params['refreshToken']['timeOut'];
        $redisDouyinWebAppUserAccessToken->attributes = [
            'douyin_web_app_id' => $data['douyinWebAppId'],
            'channel_app_source_id' => $data['channelAppSourceId'],
            'channel_app_source_uuid' => $data['channelAppSourceUuid'],
            'douyin_web_app_user_id' => $data['douyinWebAppUserId'],
            'douyin_web_app_user_uuid' => $data['douyinWebAppUserUuid'],
            'grant_type' => $data['grantType'],
            'access_token' => $data['access_token'],
            'expires_in' => $data['expires_in'],
            'expires_at' => $accessTokenExpireAt,
            'refresh_token' => $data['refresh_token'],
            'refresh_token_expires_in' => $data['refresh_expires_in'],
            'refresh_token_expires_at' => $refreshTokenExpireAt,
            'scope' => $data['scope'],
            'open_id' => $data['open_id'],
            'status' => RedisDouyinWebAppUserAccessToken::STATUS_ENABLED,
        ];

        $return = [];
        if ($redisDouyinWebAppUserAccessToken->save()) {
            $return = ['status' => true, 'data' => ['channel_app_source_uuid' => $data['channelAppSourceUuid']]];
        } elseif ($redisDouyinWebAppUserAccessToken->hasErrors()) {
            $firstError = '';
            foreach ($redisDouyinWebAppUserAccessToken->getFirstErrors() as $message) {
                $firstError = $message;
                break;
            }
            $return = ['status' => false, 'code' => 215004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '215004'), ['first_error' => $firstError]))];
        } elseif (!$redisDouyinWebAppUserAccessToken->hasErrors()) {
            throw new ServerErrorHttpException('Failed to insert/update the object (Douyin\'s third-party service platform website application user\'s access tokenn (Redis)) for unknown reason.');
        }

        print_r($redisDouyinWebAppUserAccessToken);
        exit;

        return $return;
    }

6、打印一下 Redis 的模型对象(更新后),$redisDouyinWebAppUserAccessToken。符合预期。字段顺序与定义一致。结果如下:

frontend\models\redis\DouyinWebAppUserAccessToken Object
(
    [_attributes:yii\db\BaseActiveRecord:private] => Array
        (
            [douyin_web_app_id] => 1
            [channel_app_source_id] => 48
            [channel_app_source_uuid] => 73d70b7e225d11eb932654ee75d2ebc1
            [douyin_web_app_user_id] => 8
            [douyin_web_app_user_uuid] => 73d82e5a225d11eb932554ee75d2ebc1
            [grant_type] => authorization_code
            [access_token] => act.fbb67f15948da5ab6d832b69daa58b7e3HEgNAeS3vv7hOBVqjNpjiJ1fZYY
            [expires_in] => 1296000
            [expires_at] => 1606202733
            [refresh_token] => rft.1b82e0d7acbea76d68f45b44461e649fQzF6hSBGPuq9w7FVWv0cJ53J4aSg
            [refresh_token_expires_in] => 2592000
            [refresh_token_expires_at] => 1607491833
            [scope] => fans.data,fans.list,following.list,im,user_info,video.comment,video.create,video.data,video.delete,video.list
            [open_id] => 3c61649c-2cd5-4479-b2a3-0c89e8f808f8
            [status] => 1
            [is_deleted] => 0
            [deleted_at] => 0
            [created_at] => 1604907033
            [updated_at] => 1604907033
            [id] => 2
        )

)

7、查看 GUI for Redis 的日志,以确认真正执行的命令。如图2

图2

hscan cpa:ar:douyin_web_app_user_access_token:a:2 0 count 100

8、打开控制台,执行与上面同样的命令。其响应与 GUI 一致。如图3

图3

本地环境:10>hscan cpa:ar:douyin_web_app_user_access_token:a:2 0 count 100
 1)  "0"
 2)    1)   "id"
  2)   "2"
  3)   "channel_app_source_uuid"
  4)   "73d70b7e225d11eb932654ee75d2ebc1"
  5)   "refresh_token"
  6)   "rft.1b82e0d7acbea76d68f45b44461e649fQzF6hSBGPuq9w7FVWv0cJ53J4aSg"
  7)   "open_id"
  8)   "3c61649c-2cd5-4479-b2a3-0c89e8f808f8"
  9)   "created_at"
  10)   "1604907033"
  11)   "is_deleted"
  12)   "0"
  13)   "status"
  14)   "1"
  15)   "refresh_token_expires_at"
  16)   "1607491833"
  17)   "channel_app_source_id"
  18)   "48"
  19)   "expires_in"
  20)   "1296000"
  21)   "refresh_token_expires_in"
  22)   "2592000"
  23)   "deleted_at"
  24)   "0"
  25)   "douyin_web_app_id"
  26)   "1"
  27)   "access_token"
  28)   "act.fbb67f15948da5ab6d832b69daa58b7e3HEgNAeS3vv7hOBVqjNpjiJ1fZYY"
  29)   "grant_type"
  30)   "authorization_code"
  31)   "douyin_web_app_user_uuid"
  32)   "73d82e5a225d11eb932554ee75d2ebc1"
  33)   "expires_at"
  34)   "1606202733"
  35)   "updated_at"
  36)   "1604907033"
  37)   "douyin_web_app_user_id"
  38)   "8"
  39)   "scope"
  40)   "fans.data,fans.list,following.list,im,user_info,video.comment,video.create,video.data,video.delete,video.list"

本地环境:10>

9、在 Yii 2.0 中,Redis 的活动记录(Active Record),基于 row 查看结构,与模型字段顺序一致(在另一个类似结构的模型中)。如图4

图4

10、# 配置域字段最大个数限制,hash-max-zipmap-entries 512。# 配置字段值最大字节限制,hash-max-zipmap-value 64。当满足以上两个条件时,哈希表 key 会被压缩,否则将按照正常的哈希结构来存储。由于字段:scope 的值:fans.data,fans.list,following.list,im,user_info,video.comment,video.create,video.data,video.delete,video.list 已经超出最大字节限制。决定修改字段:scope 的值为空字符串。基于 row 查看结构,与模型字段顺序一致。最终符合预期。如图5

图5

永夜