hyperf 二十九 修改器 二

教程:Hyperf

属性类型转换
Hyperf\Database\Model\Concerns\HasAttributes::casts被HasAttributes::setAttribute()、HasAttributes::getAttribute()调用,执行类型转换。
HasAttributes::casts为数组类型可设置基本类型或类的实例。默认设置主键id为int。
内置的基本强制转换类型:

  • json:'array', 'json', 'object', 'collection'
  • 日期:'date', 'datetime'
  • 整数:'int'\integer
  • 浮点数: real\float\double
  • 数字:decimal
  • 字符串:string
  • 布尔:bool/boolean
  • 自定义时间:custom_datetime
  • 时间戳:timestamp

decimal 可设置小数位数,格式decimal:小数位数,通过number_format()实现。

一 自定义类型转换

通过继承接口(implements)Hyperf\Contract\CastsAttributes,定义好后使用其类名称将其附加到HasAttributes::casts。

1.1 值对象类型转换

将值转换成对象。通过get()获取时设置为对象、set()设置时将值设置到对象中。

执行save()之前需要设置相应的对象。官网例子中是先获取在设置,这样获取之后数据结构中自带类。

更改数据结构中类对象值后,因为缓存原因,其类的值不会通过HasAttributes::getAttributes()获得刷新,使用HasAttributes::syncAttributes()通过缓存和属性值合并可获取设置后的更新数据。

1.2 入站类型转换

设置set()实现入站类型转换。

1.3 类型参数转换

使用“:”设置参数。

protected $casts = [
        'secret' => Hash::class.':sha256',
    ];

1.4 测试

获取

#App\Controlle\TestController
public function testmodifier() {
        $r = Article::query()->find(2);
        $article = $r->article;
        $istop = $r->is_top;
        var_dump($istop);
        $r->article->intro = "test1";
        $info1 = $r->getAttributes();
        $info2 = $r->syncAttributes()->getAttributes();
        $r->intro = "test2";
        $info3 = $r->getAttributes();
        var_dump($article, $info1, $info2, $info3);
}
#App1\Model\Article
protected $casts = [
        'id' => 'integer',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
        'is_top' => 'boolean',
        'article' => ArticleCasts::class,
    ];
#App1\Attributes\Article 
namespace App1\Attributes;

class Article {

    public $title;
    public $content;
    public $intro;

    public function __construct($title, $intro, $content) {
        $this->title = $title;
        $this->content = $content;
        $this->intro = $intro;
    }
}
#App1\Casts\ArticleCasts 
namespace App1\Casts;

use App1\Attributes\Article;
use Hyperf\Contract\CastsAttributes;

class ArticleCasts implements CastsAttributes {
    /**
     * 将取出的数据进行转换
     */
    public function get($model, $key, $value, $attributes): Article {
        return new Article(
            $attributes['title'],
            $attributes['intro'],
            $attributes['content']
        );
    }

    /**
     * 转换成将要进行存储的值
     */
    public function set($model, $key, $value, $attributes) {
        return [
            'title' => $value->title,
            'intro' => $value->intro,
            'content' => $value->content,
        ];
    }
}

 测试结果

bool(false)
object(App1\Attributes\Article)#1213 (3) {
  ["title"]=>
  string(5) "test2"
  ["content"]=>
  NULL
  ["intro"]=>
  string(5) "test1"
}
array(10) {
  ["id"]=>
  int(2)
  ["user_id"]=>
  int(1)
  ["title"]=>
  string(5) "test2"
  ["created_at"]=>
  string(19) "2024-01-13 10:06:04"
  ["updated_at"]=>
  string(19) "2024-01-13 10:06:06"
  ["deleted_at"]=>
  NULL
  ["pv_num"]=>
  int(0)
  ["intro"]=>
  NULL
  ["content"]=>
  NULL
  ["is_top"]=>
  int(0)
}
array(10) {
  ["id"]=>
  int(2)
  ["user_id"]=>
  int(1)
  ["title"]=>
  string(5) "test2"
  ["created_at"]=>
  string(19) "2024-01-13 10:06:04"
  ["updated_at"]=>
  string(19) "2024-01-13 10:06:06"
  ["deleted_at"]=>
  NULL
  ["pv_num"]=>
  int(0)
  ["intro"]=>
  string(5) "test1"
  ["content"]=>
  NULL
  ["is_top"]=>
  int(0)
}
array(10) {
  ["id"]=>
  int(2)
  ["user_id"]=>
  int(1)
  ["title"]=>
  string(5) "test2"
  ["created_at"]=>
  string(19) "2024-01-13 10:06:04"
  ["updated_at"]=>
  string(19) "2024-01-13 10:06:06"
  ["deleted_at"]=>
  NULL
  ["pv_num"]=>
  int(0)
  ["intro"]=>
  string(5) "test2"
  ["content"]=>
  NULL
  ["is_top"]=>
  int(0)
}

 写入

#App\Controlle\TestController
public function testmodifier() {
        $r = Article::query()->find(2);
        $r->is_top = 1;
        $r->article->intro = "test2";
        $attr = $r->getAttributes();
        var_dump($attr);
        $r->save();
        $attr = $r->getAttributes();
        var_dump($attr);
}

 测试结果

array(10) {
  ["id"]=>
  int(2)
  ["user_id"]=>
  int(1)
  ["title"]=>
  string(5) "test2"
  ["created_at"]=>
  string(19) "2024-01-13 10:06:04"
  ["updated_at"]=>
  string(19) "2024-03-25 09:47:00"
  ["deleted_at"]=>
  NULL
  ["pv_num"]=>
  int(0)
  ["intro"]=>
  string(5) "test1"
  ["content"]=>
  NULL
  ["is_top"]=>
  int(1)
}
array(10) {
  ["id"]=>
  int(2)
  ["user_id"]=>
  int(1)
  ["title"]=>
  string(5) "test2"
  ["created_at"]=>
  string(19) "2024-01-13 10:06:04"
  ["updated_at"]=>
  string(16) "2024-03-25 09:48"
  ["deleted_at"]=>
  NULL
  ["pv_num"]=>
  int(0)
  ["intro"]=>
  string(5) "test2"
  ["content"]=>
  NULL
  ["is_top"]=>
  int(1)
}

1.5 源码

转换类型

#Hyperf\Database\Model\Concerns\HasAttributes
public function getAttribute($key)
    {
        if (!$key) {
            return;
        }

        // If the attribute exists in the attribute array or has a "get" mutator we will
        // get the attribute's value. Otherwise, we will proceed as if the developers
        // are asking for a relationship's value. This covers both types of values.
        if (array_key_exists($key, $this->getAttributes())
            || $this->hasGetMutator($key)
            || $this->isClassCastable($key)) {
            return $this->getAttributeValue($key);
        }
        // Here we will determine if the model base class itself contains this given key
        // since we don't want to treat any of those methods as relationships because
        // they are all intended as helper methods and none of these are relations.
        if (method_exists(self::class, $key)) {
            return;
        }
        return $this->getRelationValue($key);
    }
protected function castAttribute($key, $value)
    {
        $castType = $this->getCastType($key);

        if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) {
            return $value;
        }

        switch ($castType) {
            case 'int':
            case 'integer':
                return (int) $value;
            case 'real':
            case 'float':
            case 'double':
                return $this->fromFloat($value);
            case 'decimal':
                return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
            case 'string':
                return (string) $value;
            case 'bool':
            case 'boolean':
                return (bool) $value;
            case 'object':
                return $this->fromJson($value, true);
            case 'array':
            case 'json':
                return $this->fromJson($value);
            case 'collection':
                return new BaseCollection($this->fromJson($value));
            case 'date':
                return $this->asDate($value);
            case 'datetime':
            case 'custom_datetime':
                return $this->asDateTime($value);
            case 'timestamp':
                return $this->asTimestamp($value);
        }

        if ($this->isClassCastable($key)) {
            return $this->getClassCastableAttributeValue($key, $value);
        }

        return $value;
    }
protected function asDecimal($value, $decimals)
    {
        return number_format((float) $value, (int) $decimals, '.', '');
    }
protected function isDateCastable($key)
    {
        return $this->hasCast($key, ['date', 'datetime']);
    }
protected function isJsonCastable($key)
    {
        return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
    }
public function getAttributeValue($key)
    {
        return $this->transformModelValue($key, $this->getAttributeFromArray($key));
    }
protected function transformModelValue($key, $value)
    {
        // If the attribute has a get mutator, we will call that then return what
        // it returns as the value, which is useful for transforming values on
        // retrieval from the model to a form that is more useful for usage.
        if ($this->hasGetMutator($key)) {
            return $this->mutateAttribute($key, $value);
        }

        // If the attribute exists within the cast array, we will convert it to
        // an appropriate native PHP type dependent upon the associated value
        // given with the key in the pair. Dayle made this comment line up.
        if ($this->hasCast($key)) {
            return $this->castAttribute($key, $value);
        }

        // If the attribute is listed as a date, we will convert it to a DateTime
        // instance on retrieval, which makes it quite convenient to work with
        // date fields without having to create a mutator for each property.
        if ($value !== null
            && \in_array($key, $this->getDates(), false)) {
            return $this->asDateTime($value);
        }

        return $value;
    }

数据更新

#Hyperf\Database\Model\Concerns\HasAttributes
public function syncAttributes()
    {
        $this->mergeAttributesFromClassCasts();
        return $this;
    }
protected function mergeAttributesFromClassCasts()
    {
        foreach ($this->classCastCache as $key => $value) {
            if ($value instanceof Synchronized && $value->isSynchronized()) {
                continue;
            }

            $caster = $this->resolveCasterClass($key);

            $this->attributes = array_merge(
                $this->attributes,
                $caster instanceof CastsInboundAttributes
                ? [$key => $value]
                : $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes))
            );
        }
    }

二 数组和json转换

数据库存json,数据库字段为text或json,数据获取时转变为数组。

获取时使用 Hyperf\Database\Model\Model::__get()调用Hyperf\Database\Model\Concerns\HasAttributes::getAttribute()。

设置时使用Model::__set()调用HasAttributes::setAttribute()。

判断是否可为json数据,则使用json_encode()转化为json数据保存。

根据源码,HasAttributes::casts数组中设置字段类型为array, json, object, collection中的类型,可为json字符串保存。

2.1 测试

#App\Controller\TestController
public function testmodifier() { 
        $article = Article::query()->find(2);
        $options = $article->content;
        var_dump($options);
        if (empty($options)) {
            $article->content = ['intro' => 'test', 'content' => 'test1'];
            $article->save();
            $options = $article->content;
        }
        var_dump($options);
}

 测试结果

NULL
array(2) {
  ["intro"]=>
  string(4) "test"
  ["content"]=>
  string(5) "test1"
}
select content from articles where id=2


{"intro":"test","content":"test1"}

 2.2 源码

getAttribute()源码详见1.5源码。

#Hyperf\Database\Model\Concerns\HasAttributes
public function setAttribute($key, $value) {
        // First we will check for the presence of a mutator for the set operation
        // which simply lets the developers tweak the attribute as it is set on
        // the model, such as "json_encoding" an listing of data for storage.
        if ($this->hasSetMutator($key)) {
            return $this->setMutatedAttributeValue($key, $value);
        }

        // If an attribute is listed as a "date", we'll convert it from a DateTime
        // instance into a form proper for storage on the database tables using
        // the connection grammar's date format. We will auto set the values.
        if ($value && $this->isDateAttribute($key)) {
            $value = $this->fromDateTime($value);
        }

        if ($this->isClassCastable($key)) {
            $this->setClassCastableAttribute($key, $value);

            return $this;
        }

        if ($this->isJsonCastable($key) && !is_null($value)) {
            $value = $this->castAttributeAsJson($key, $value);
        }

        // If this attribute contains a JSON ->, we'll set the proper value in the
        // attribute's underlying array. This takes care of properly nesting an
        // attribute in the array's value in the case of deeply nested items.
        if (Str::contains($key, '->')) {
            return $this->fillJsonAttribute($key, $value);
        }

        $this->attributes[$key] = $value;

        return $this;
    }
protected function isJsonCastable($key) {
        return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
    }
public function hasCast($key, $types = null) {
        if (array_key_exists($key, $this->getCasts())) {
            return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
        }

        return false;
    }
public function getCasts() {
        if ($this->getIncrementing()) {
            return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts);
        }

        return $this->casts;
    }
protected function castAttributeAsJson($key, $value) {
        $value = $this->asJson($value);

        if ($value === false) {
            throw JsonEncodingException::forAttribute(
                $this,
                $key,
                json_last_error_msg()
            );
        }

        return $value;
    }

三 Date类型转换

在设置的时候,若不是Hyperf\Database\Model\Model::CREATED_AT,Model::UPDATED_AT,即字段不为created_at或updated_at,且设置参数如日期格式,则不会调用转换。

因为判断是否可转换的时候被过滤掉。像官网设置created_at不会被转换,可能和版本有关系。

再说获取。

直接获取对应属性,即运行Hyperf\Database\Model\Model::__get()。 转换类型会被判断为custom_datetime,转换为Carbon类。

但是设置的格式未被解析,还是需要手动设置格式。使用Hyperf\Database\Model\Concerns\HasAttributes::getAttributes()获取,直接获取HasAttributes::attributes属性。

这两种对与设置的格式都不会解析,返回都是根据HasAttributes::dateFormat,其默认值都是Y-m-d H:i:s。

使用HasAttributes::attributesToArray(),会调用HasAttributes::addCastAttributesToArray(),会判断是否为custom_datetime。

是custom_datetime类型,则会调用php系统类自带函数format(),通过解析参数,获得格式化数据。

其实和自己使用format(),运行原理一样,效果也一样。

3.1 测试

#App\Controller\TestController
public function testmodifier() {
    $pr = PushRecode::query()->find(1);
    $pr->push_time = date('Y-m-d H:i:s');
    var_dump($pr->push_time);
    $pr = $pr->syncAttributes();
    var_dump($pr->push_time->format('Y-m-d H:00:00'));
    $pr->created_at = date('Y-m-d H:i:s');
    $info = $pr->syncAttributes()->getAttributes();
    var_dump($info);
    $info = $pr->syncAttributes()->attributesToArray();
    var_dump($info);
}

测试结果

object(Carbon\Carbon)#1151 (19) {
  ["endOfTime":protected]=>
  bool(false)
  ["startOfTime":protected]=>
  bool(false)
  ["constructedObjectId":protected]=>
  string(32) "00000000607fdc84000000003fa11939"
  ["localMonthsOverflow":protected]=>
  NULL
  ["localYearsOverflow":protected]=>
  NULL
  ["localStrictModeEnabled":protected]=>
  NULL
  ["localHumanDiffOptions":protected]=>
  NULL
  ["localToStringFormat":protected]=>
  NULL
  ["localSerializer":protected]=>
  NULL
  ["localMacros":protected]=>
  NULL
  ["localGenericMacros":protected]=>
  NULL
  ["localFormatFunction":protected]=>
  NULL
  ["localTranslator":protected]=>
  NULL
  ["dumpProperties":protected]=>
  array(3) {
    [0]=>
    string(4) "date"
    [1]=>
    string(13) "timezone_type"
    [2]=>
    string(8) "timezone"
  }
  ["dumpLocale":protected]=>
  NULL
  ["dumpDateProperties":protected]=>
  NULL
  ["date"]=>
  string(26) "2024-04-11 09:30:03.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(3) "UTC"
}
string(19) "2024-04-11 09:00:00"
array(4) {
  ["id"]=>
  int(1)
  ["is_push"]=>
  int(1)
  ["push_time"]=>
  string(19) "2024-04-11 09:30:03"
  ["created_at"]=>
  string(19) "2024-04-11 09:30:03"
}
array(4) {
  ["id"]=>
  int(1)
  ["is_push"]=>
  int(1)
  ["push_time"]=>
  string(10) "2024-04-11"
  ["created_at"]=>
  string(10) "2024-04-11"
}

3.2 源码

Hyperf\Database\Model\Model::getAttribute()内容详见1.5源码。

Hyperf\Database\Model\Model::setAttribute()内容详见2.2源码。

#Hyperf\Database\Model\Concerns\HasAttributes
protected function isClassCastable($key) {
        return array_key_exists($key, $this->getCasts())
        && class_exists($class = $this->parseCasterClass($this->getCasts()[$key]))
        && !in_array($class, static::$primitiveCastTypes);
    }
protected function parseCasterClass($class) {
        return strpos($class, ':') === false
        ? $class
        : explode(':', $class, 2)[0];
    }
protected static $primitiveCastTypes = [
        'array',
        'bool',
        'boolean',
        'collection',
        'custom_datetime',
        'date',
        'datetime',
        'decimal',
        'double',
        'float',
        'int',
        'integer',
        'json',
        'object',
        'real',
        'string',
        'timestamp',
    ];
public function getCasts() {
        if ($this->getIncrementing()) {
            return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts);
        }

        return $this->casts;
    }

public function getAttributes() {
        return $this->attributes;
    }


protected function isDateAttribute($key) {
        return in_array($key, $this->getDates(), true)
        || $this->isDateCastable($key);
    }
protected function isDateCastable($key) {
        return $this->hasCast($key, ['date', 'datetime']);
    }


public function getAttributeValue($key) {
        return $this->transformModelValue($key, $this->getAttributeFromArray($key));
    }
protected function transformModelValue($key, $value) {
        // If the attribute has a get mutator, we will call that then return what
        // it returns as the value, which is useful for transforming values on
        // retrieval from the model to a form that is more useful for usage.
        if ($this->hasGetMutator($key)) {
            return $this->mutateAttribute($key, $value);
        }

        // If the attribute exists within the cast array, we will convert it to
        // an appropriate native PHP type dependent upon the associated value
        // given with the key in the pair. Dayle made this comment line up.
        if ($this->hasCast($key)) {
            return $this->castAttribute($key, $value);
        }

        // If the attribute is listed as a date, we will convert it to a DateTime
        // instance on retrieval, which makes it quite convenient to work with
        // date fields without having to create a mutator for each property.
        if ($value !== null
            && \in_array($key, $this->getDates(), false)) {
            return $this->asDateTime($value);
        }

        return $value;
    }
protected function castAttribute($key, $value) {
        $castType = $this->getCastType($key);

        if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) {
            return $value;
        }
        switch ($castType) {
        case 'int':
        case 'integer':
            return (int) $value;
        case 'real':
        case 'float':
        case 'double':
            return $this->fromFloat($value);
        case 'decimal':
            return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
        case 'string':
            return (string) $value;
        case 'bool':
        case 'boolean':
            return (bool) $value;
        case 'object':
            return $this->fromJson($value, true);
        case 'array':
        case 'json':
            return $this->fromJson($value);
        case 'collection':
            return new BaseCollection($this->fromJson($value));
        case 'date':
            return $this->asDate($value);
        case 'datetime':
        case 'custom_datetime':
            return $this->asDateTime($value);
        case 'timestamp':
            return $this->asTimestamp($value);
        }

        if ($this->isClassCastable($key)) {
            return $this->getClassCastableAttributeValue($key, $value);
        }

        return $value;
    }
protected function getCastType($key) {
        if ($this->isCustomDateTimeCast($this->getCasts()[$key])) {
            return 'custom_datetime';
        }

        if ($this->isDecimalCast($this->getCasts()[$key])) {
            return 'decimal';
        }

        return trim(strtolower($this->getCasts()[$key]));
    }
protected function asDateTime($value) {
        // If this value is already a Carbon instance, we shall just return it as is.
        // This prevents us having to re-instantiate a Carbon instance when we know
        // it already is one, which wouldn't be fulfilled by the DateTime check.
        if ($value instanceof Carbon || $value instanceof CarbonInterface) {
            return Carbon::instance($value);
        }

        // If the value is already a DateTime instance, we will just skip the rest of
        // these checks since they will be a waste of time, and hinder performance
        // when checking the field. We will just return the DateTime right away.
        if ($value instanceof DateTimeInterface) {
            return Carbon::parse(
                $value->format('Y-m-d H:i:s.u'),
                $value->getTimezone()
            );
        }

        // If this value is an integer, we will assume it is a UNIX timestamp's value
        // and format a Carbon object from this timestamp. This allows flexibility
        // when defining your date fields as they might be UNIX timestamps here.
        if (is_numeric($value)) {
            return Carbon::createFromTimestamp($value);
        }

        // If the value is in simply year, month, day format, we will instantiate the
        // Carbon instances from that format. Again, this provides for simple date
        // fields on the database, while still supporting Carbonized conversion.
        if ($this->isStandardDateFormat($value)) {
            return Carbon::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay());
        }

        $format = $this->getDateFormat();

        // Finally, we will just assume this date is in the format used by default on
        // the database connection and use that format to create the Carbon object
        // that is returned back out to the developers after we convert it here.
        if (Carbon::hasFormat($value, $format)) {
            return Carbon::createFromFormat($format, $value);
        }

        return Carbon::parse($value);
    }
protected function isCustomDateTimeCast($cast) {
        return strncmp($cast, 'date:', 5) === 0
        || strncmp($cast, 'datetime:', 9) === 0;
    }

根据源码,判断array_key_exists($key, $this->getCasts())、class_exists($class = $this->parseCasterClass($this->getCasts()[$key]))应为true,!in_array($class, static::$primitiveCastTypes)为false,所以isClassCastable()为false。

isDateAttribute()结果应该也为false。

之后执行getAttributeValue(),获取castType为custom_datetime,最终转换为Carbon\Carbon类。

#Hyperf\Database\Model\Concerns\HasAttributes
public function attributesToArray() {
        // If an attribute is a date, we will cast it to a string after converting it
        // to a DateTime / Carbon instance. This is so we will get some consistent
        // formatting while accessing attributes vs. arraying / JSONing a model.
        $attributes = $this->addDateAttributesToArray(
            $attributes = $this->getArrayableAttributes()
        );

        $attributes = $this->addMutatedAttributesToArray(
            $attributes,
            $mutatedAttributes = $this->getMutatedAttributes()
        );

        // Next we will handle any casts that have been setup for this model and cast
        // the values to their appropriate type. If the attribute has a mutator we
        // will not perform the cast on those attributes to avoid any confusion.
        $attributes = $this->addCastAttributesToArray(
            $attributes,
            $mutatedAttributes
        );

        // Here we will grab all of the appended, calculated attributes to this model
        // as these attributes are not really in the attributes array, but are run
        // when we need to array or JSON the model for convenience to the coder.
        foreach ($this->getArrayableAppends() as $key) {
            $attributes[$key] = $this->mutateAttributeForArray($key, null);
        }

        return $attributes;
    }
protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes) {
        foreach ($this->getCasts() as $key => $value) {
            if (!array_key_exists($key, $attributes) || in_array($key, $mutatedAttributes)) {
                continue;
            }

            // Here we will cast the attribute. Then, if the cast is a date or datetime cast
            // then we will serialize the date for the array. This will convert the dates
            // to strings based on the date format specified for these Model models.
            $attributes[$key] = $this->castAttribute(
                $key,
                $attributes[$key]
            );

            // If the attribute cast was a date or a datetime, we will serialize the date as
            // a string. This allows the developers to customize how dates are serialized
            // into an array without affecting how they are persisted into the storage.
            if ($attributes[$key]
                && ($value === 'date' || $value === 'datetime')) {
                $attributes[$key] = $this->serializeDate($attributes[$key]);
            }

            if ($attributes[$key] && $this->isCustomDateTimeCast($value)) {
                $attributes[$key] = $attributes[$key]->format(explode(':', $value, 2)[1]);
            }

            if ($attributes[$key] instanceof Arrayable) {
                $attributes[$key] = $attributes[$key]->toArray();
            }
        }

        return $attributes;
    }

官网打不开了……后续内容等官网好了再说吧……

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lsswear

感谢大佬打赏 q(≧▽≦q)

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值