h5红包雨代码_代码模板 | 我的代码没有else

本文介绍了如何在实际业务中运用模板模式,特别是在h5红包雨场景下。通过解释模板模式的原理和优势,阐述了在稳定不变的算法步骤中,允许子类根据需要实现变化的部分。文中通过业务梳理、流程图、代码建模和示例代码展示了如何在Golang中应用模板模式来实现抽奖系统的抽奖接口,强调了把握不变与变的核心思想。
摘要由CSDN通过智能技术生成

嗯,我的代码没有else系列,一个设计模式业务真实使用的golang系列。

df2e137c99ccf4964b69120c49916a95.png

前言

本系列主要分享,如何在我们的真实业务场景中使用设计模式。

本系列文章主要采用如下结构:

  • 什么是「XX设计模式」?

  • 什么真实业务场景可以使用「XX设计模式」?

  • 怎么用「XX设计模式」?

本文主要介绍「模板模式」如何在真实业务场景中使用。

什么是「模板模式」?

抽象类里定义好算法的执行步骤和具体算法,以及可能发生变化的算法定义为抽象方法。不同的子类继承该抽象类,并实现父类的抽象方法。

模板模式的优势:

  • 不变的算法被继承复用:不变的部分高度封装、复用。

  • 变化的算法子类继承并具体实现:变化的部分子类只需要具体实现抽象的部分即可,方便扩展,且可无限扩展。

什么真实业务场景可以用「模板模式」?

满足如下要求的所有场景:

算法执行的步骤是稳定不变的,但是具体的某些算法可能存在变化的场景。

怎么理解,举个例子:比如说你煮个面,必然需要先烧水,水烧开之后再放面进去,以上的流程我们称之为煮面过程。可知:这个煮面过程的步骤是稳定不变的,但是在不同的环境烧水的方式可能不尽相同,也许有的人用天然气烧水、有的人用电磁炉烧水、有的人用柴火烧水,等等。我们可以得到以下结论:

  • 煮面过程的步骤是稳定不变的

  • 煮面过程的烧水方式是可变的

我们有哪些真实业务场景可以用「模板模式」呢?

比如抽奖系统的抽奖接口,为什么:

  • 抽奖的步骤是稳定不变的 -> 不变的算法执行步骤

  • 不同抽奖类型活动在某些逻辑处理方式可能不同 -> 变的某些算法

怎么用「模板模式」?

关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:

  • 业务梳理

  • 业务流程图

  • 代码建模

  • 代码demo

业务梳理

我通过历史上接触过的各种抽奖场景(红包雨、糖果雨、打地鼠、大转盘(九宫格)、考眼力、答题闯关、游戏闯关、支付刮刮乐、积分刮刮乐等等),按照真实业务需求梳理了以下抽奖业务抽奖接口的大致文本流程。

了解具体业务请点击《通用抽奖工具之需求分析 | SkrShop》

主步骤主逻辑抽奖类型子步骤子逻辑
1校验活动编号(serial_no)是否存在、并获取活动信息---
2校验活动、场次是否正在进行---
3其他参数校验(不同活动类型实现不同)---
4活动抽奖次数校验(同时扣减)---
5活动是否需要消费积分---
6场次抽奖次数校验(同时扣减)---
7获取场次奖品信息---
8获取node奖品信息(不同活动类型实现不同)按时间抽奖类型1do nothing(抽取该场次的奖品即可,无需其他逻辑)
8按抽奖次数抽奖类型1判断是该用户第几次抽奖
82获取对应node的奖品信息
83复写原所有奖品信息(抽取该node节点的奖品)
8按数额范围区间抽奖1判断属于哪个数额区间
82获取对应node的奖品信息
83复写原所有奖品信息(抽取该node节点的奖品)
9抽奖---
10奖品数量判断---
11组装奖品信息---

注:流程不一定完全准确

结论:

  • 主逻辑是稳定不变的

  • 其他参数校验获取node奖品信息的算法是可变的

业务流程图

我们通过梳理的文本业务流程得到了如下的业务流程图:

bb482ec4f8b8bc395b955d6c82d60db8.png

代码建模

通过上面的分析我们可以得到:

一个抽象类
- 具体共有方法`Run`,里面定义了算法的执行步骤
- 具体私有方法,不会发生变化的具体方法
- 抽象方法,会发生变化的方法
子类一(按时间抽奖类型)
- 继承抽象类父类
- 实现抽象方法
子类二(按抽奖次数抽奖类型)
- 继承抽象类父类
- 实现抽象方法
子类三(按数额范围区间抽奖)
- 继承抽象类父类
- 实现抽象方法

但是golang里面没有继承的概念,我们就把对抽象类里抽象方法的依赖转化成对接口interface里抽像方法的依赖,同时也可以利用合成复用的方式“继承”模板:

抽象行为的接口`BehaviorInterface`(包含如下需要实现的方法)
- 其他参数校验的方法`checkParams`
- 获取node奖品信息的方法`getPrizesByNode`
抽奖结构体类
- 具体共有方法`Run`,里面定义了算法的执行步骤
- 具体私有方法`checkParams` 里面的逻辑实际依赖的接口BehaviorInterface.checkParams(ctx)的抽象方法
- 具体私有方法`getPrizesByNode` 里面的逻辑实际依赖的接口BehaviorInterface.getPrizesByNode(ctx)的抽象方法
- 其他具体私有方法,不会发生变化的具体方法
实现`BehaviorInterface`的结构体一(按时间抽奖类型)
- 实现接口方法
实现`BehaviorInterface`的结构体二(按抽奖次数抽奖类型)
- 实现接口方法
实现`BehaviorInterface`的结构体三(按数额范围区间抽奖)
- 实现接口方法

同时得到了我们的UML图:

fe0786ff4f7e4d9be62c46379242c60f.png

代码demo

package mainimport ("fmt""runtime"
)//------------------------------------------------------------//我的代码没有`else`系列//模板模式//@auhtor TIGERB//------------------------------------------------------------const (// ConstActTypeTime 按时间抽奖类型
ConstActTypeTime int32 = 1// ConstActTypeTimes 按抽奖次数抽奖
ConstActTypeTimes int32 = 2// ConstActTypeAmount 按数额范围区间抽奖
ConstActTypeAmount int32 = 3
)// Context 上下文type Context struct {
ActInfo *ActInfo
}// ActInfo 上下文type ActInfo struct {// 活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖
ActivityType int32// 其他字段略
}// BehaviorInterface 不同抽奖类型的行为差异的抽象接口type BehaviorInterface interface {// 其他参数校验(不同活动类型实现不同)
checkParams(ctx *Context) error// 获取node奖品信息(不同活动类型实现不同)
getPrizesByNode(ctx *Context) error
}// TimeDraw 具体抽奖行为// 按时间抽奖类型 比如红包雨type TimeDraw struct{}// checkParams 其他参数校验(不同活动类型实现不同)func (draw TimeDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按时间抽奖类型:特殊参数校验...")return
}// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)func (draw TimeDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "do nothing(抽取该场次的奖品即可,无需其他逻辑)...")return
}// TimesDraw 具体抽奖行为// 按抽奖次数抽奖类型 比如答题闯关type TimesDraw struct{}// checkParams 其他参数校验(不同活动类型实现不同)func (draw TimesDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按抽奖次数抽奖类型:特殊参数校验...")return
}// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)func (draw TimesDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "1. 判断是该用户第几次抽奖...")
fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")return
}// AmountDraw 具体抽奖行为// 按数额范围区间抽奖 比如订单金额刮奖type AmountDraw struct{}// checkParams 其他参数校验(不同活动类型实现不同)func (draw *AmountDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按数额范围区间抽奖:特殊参数校验...")return
}// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)func (draw *AmountDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "1. 判断属于哪个数额区间...")
fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")return
}// Lottery 抽奖模板type Lottery struct {// 不同抽奖类型的抽象行为
concreteBehavior BehaviorInterface
}// Run 抽奖算法// 稳定不变的算法步骤func (lottery *Lottery) Run(ctx *Context) (err error) {// 具体方法:校验活动编号(serial_no)是否存在、并获取活动信息if err = lottery.checkSerialNo(ctx); err != nil {return err
}// 具体方法:校验活动、场次是否正在进行if err = lottery.checkStatus(ctx); err != nil {return err
}// ”抽象方法“:其他参数校验if err = lottery.checkParams(ctx); err != nil {return err
}// 具体方法:活动抽奖次数校验(同时扣减)if err = lottery.checkTimesByAct(ctx); err != nil {return err
}// 具体方法:活动是否需要消费积分if err = lottery.consumePointsByAct(ctx); err != nil {return err
}// 具体方法:场次抽奖次数校验(同时扣减)if err = lottery.checkTimesBySession(ctx); err != nil {return err
}// 具体方法:获取场次奖品信息if err = lottery.getPrizesBySession(ctx); err != nil {return err
}// ”抽象方法“:获取node奖品信息if err = lottery.getPrizesByNode(ctx); err != nil {return err
}// 具体方法:抽奖if err = lottery.drawPrizes(ctx); err != nil {return err
}// 具体方法:奖品数量判断if err = lottery.checkPrizesStock(ctx); err != nil {return err
}// 具体方法:组装奖品信息if err = lottery.packagePrizeInfo(ctx); err != nil {return err
}return
}// checkSerialNo 校验活动编号(serial_no)是否存在func (lottery *Lottery) checkSerialNo(ctx *Context) (err error) {
fmt.Println(runFuncName(), "校验活动编号(serial_no)是否存在、并获取活动信息...")// 获取活动信息伪代码
ctx.ActInfo = &ActInfo{// 假设当前的活动类型为按抽奖次数抽奖
ActivityType: ConstActTypeTimes,
}// 获取当前抽奖类型的具体行为switch ctx.ActInfo.ActivityType {case 1:// 按时间抽奖
lottery.concreteBehavior = &TimeDraw{}case 2:// 按抽奖次数抽奖
lottery.concreteBehavior = &TimesDraw{}case 3:// 按数额范围区间抽奖
lottery.concreteBehavior = &AmountDraw{}default:return fmt.Errorf("不存在的活动类型")
}return
}// checkStatus 校验活动、场次是否正在进行func (lottery *Lottery) checkStatus(ctx *Context) (err error) {
fmt.Println(runFuncName(), "校验活动、场次是否正在进行...")return
}// checkParams 其他参数校验(不同活动类型实现不同)// 不同场景变化的算法 转化为依赖抽象func (lottery *Lottery) checkParams(ctx *Context) (err error) {// 实际依赖的接口的抽象方法return lottery.concreteBehavior.checkParams(ctx)
}// checkTimesByAct 活动抽奖次数校验func (lottery *Lottery) checkTimesByAct(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动抽奖次数校验...")return
}// consumePointsByAct 活动是否需要消费积分func (lottery *Lottery) consumePointsByAct(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动是否需要消费积分...")return
}// checkTimesBySession 活动抽奖次数校验func (lottery *Lottery) checkTimesBySession(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动抽奖次数校验...")return
}// getPrizesBySession 获取场次奖品信息func (lottery *Lottery) getPrizesBySession(ctx *Context) (err error) {
fmt.Println(runFuncName(), "获取场次奖品信息...")return
}// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)// 不同场景变化的算法 转化为依赖抽象func (lottery *Lottery) getPrizesByNode(ctx *Context) (err error) {// 实际依赖的接口的抽象方法return lottery.concreteBehavior.getPrizesByNode(ctx)
}// drawPrizes 抽奖func (lottery *Lottery) drawPrizes(ctx *Context) (err error) {
fmt.Println(runFuncName(), "抽奖...")return
}// checkPrizesStock 奖品数量判断func (lottery *Lottery) checkPrizesStock(ctx *Context) (err error) {
fmt.Println(runFuncName(), "奖品数量判断...")return
}// packagePrizeInfo 组装奖品信息func (lottery *Lottery) packagePrizeInfo(ctx *Context) (err error) {
fmt.Println(runFuncName(), "组装奖品信息...")return
}func main() {
(&Lottery{}).Run(&Context{})
}// 获取正在运行的函数名func runFuncName() string {
pc := make([]uintptr, 1)
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])return f.Name()
}

以下是代码执行结果:

[Running] go run ".../easy-tips/go/src/patterns/template/template.go"
main.(*Lottery).checkSerialNo 校验活动编号(serial_no)是否存在、并获取活动信息...
main.(*Lottery).checkStatus 校验活动、场次是否正在进行...
main.TimesDraw.checkParams 按抽奖次数抽奖类型:特殊参数校验...
main.(*Lottery).checkTimesByAct 活动抽奖次数校验...
main.(*Lottery).consumePointsByAct 活动是否需要消费积分...
main.(*Lottery).checkTimesBySession 活动抽奖次数校验...
main.(*Lottery).getPrizesBySession 获取场次奖品信息...
main.TimesDraw.getPrizesByNode 1. 判断是该用户第几次抽奖...
main.TimesDraw.getPrizesByNode 2. 获取对应node的奖品信息...
main.TimesDraw.getPrizesByNode 3. 复写原所有奖品信息(抽取该node节点的奖品)...
main.(*Lottery).drawPrizes 抽奖...
main.(*Lottery).checkPrizesStock 奖品数量判断...
main.(*Lottery).packagePrizeInfo 组装奖品信息...

demo代码地址:https://github.com/TIGERB/easy-tips/blob/master/go/src/patterns/template/template.go

代码demo2(利用golang的合成复用特性实现)

package mainimport ("fmt""runtime"
)//------------------------------------------------------------//我的代码没有`else`系列//模板模式//@auhtor TIGERB//------------------------------------------------------------const (// ConstActTypeTime 按时间抽奖类型
ConstActTypeTime int32 = 1// ConstActTypeTimes 按抽奖次数抽奖
ConstActTypeTimes int32 = 2// ConstActTypeAmount 按数额范围区间抽奖
ConstActTypeAmount int32 = 3
)// Context 上下文type Context struct {
ActInfo *ActInfo
}// ActInfo 上下文type ActInfo struct {// 活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖
ActivityType int32// 其他字段略
}// BehaviorInterface 不同抽奖类型的行为差异的抽象接口type BehaviorInterface interface {// 其他参数校验(不同活动类型实现不同)
checkParams(ctx *Context) error// 获取node奖品信息(不同活动类型实现不同)
getPrizesByNode(ctx *Context) error
}// TimeDraw 具体抽奖行为// 按时间抽奖类型 比如红包雨type TimeDraw struct {// 合成复用模板
Lottery
}// checkParams 其他参数校验(不同活动类型实现不同)func (draw TimeDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按时间抽奖类型:特殊参数校验...")return
}// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)func (draw TimeDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "do nothing(抽取该场次的奖品即可,无需其他逻辑)...")return
}// TimesDraw 具体抽奖行为// 按抽奖次数抽奖类型 比如答题闯关type TimesDraw struct {// 合成复用模板
Lottery
}// checkParams 其他参数校验(不同活动类型实现不同)func (draw TimesDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按抽奖次数抽奖类型:特殊参数校验...")return
}// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)func (draw TimesDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "1. 判断是该用户第几次抽奖...")
fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")return
}// AmountDraw 具体抽奖行为// 按数额范围区间抽奖 比如订单金额刮奖type AmountDraw struct {// 合成复用模板
Lottery
}// checkParams 其他参数校验(不同活动类型实现不同)func (draw *AmountDraw) checkParams(ctx *Context) (err error) {
fmt.Println(runFuncName(), "按数额范围区间抽奖:特殊参数校验...")return
}// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)func (draw *AmountDraw) getPrizesByNode(ctx *Context) (err error) {
fmt.Println(runFuncName(), "1. 判断属于哪个数额区间...")
fmt.Println(runFuncName(), "2. 获取对应node的奖品信息...")
fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该node节点的奖品)...")return
}// Lottery 抽奖模板type Lottery struct {// 不同抽奖类型的抽象行为
ConcreteBehavior BehaviorInterface
}// Run 抽奖算法// 稳定不变的算法步骤func (lottery *Lottery) Run(ctx *Context) (err error) {// 具体方法:校验活动编号(serial_no)是否存在、并获取活动信息if err = lottery.checkSerialNo(ctx); err != nil {return err
}// 具体方法:校验活动、场次是否正在进行if err = lottery.checkStatus(ctx); err != nil {return err
}// ”抽象方法“:其他参数校验if err = lottery.checkParams(ctx); err != nil {return err
}// 具体方法:活动抽奖次数校验(同时扣减)if err = lottery.checkTimesByAct(ctx); err != nil {return err
}// 具体方法:活动是否需要消费积分if err = lottery.consumePointsByAct(ctx); err != nil {return err
}// 具体方法:场次抽奖次数校验(同时扣减)if err = lottery.checkTimesBySession(ctx); err != nil {return err
}// 具体方法:获取场次奖品信息if err = lottery.getPrizesBySession(ctx); err != nil {return err
}// ”抽象方法“:获取node奖品信息if err = lottery.getPrizesByNode(ctx); err != nil {return err
}// 具体方法:抽奖if err = lottery.drawPrizes(ctx); err != nil {return err
}// 具体方法:奖品数量判断if err = lottery.checkPrizesStock(ctx); err != nil {return err
}// 具体方法:组装奖品信息if err = lottery.packagePrizeInfo(ctx); err != nil {return err
}return
}// checkSerialNo 校验活动编号(serial_no)是否存在func (lottery *Lottery) checkSerialNo(ctx *Context) (err error) {
fmt.Println(runFuncName(), "校验活动编号(serial_no)是否存在、并获取活动信息...")return
}// checkStatus 校验活动、场次是否正在进行func (lottery *Lottery) checkStatus(ctx *Context) (err error) {
fmt.Println(runFuncName(), "校验活动、场次是否正在进行...")return
}// checkParams 其他参数校验(不同活动类型实现不同)// 不同场景变化的算法 转化为依赖抽象func (lottery *Lottery) checkParams(ctx *Context) (err error) {// 实际依赖的接口的抽象方法return lottery.ConcreteBehavior.checkParams(ctx)
}// checkTimesByAct 活动抽奖次数校验func (lottery *Lottery) checkTimesByAct(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动抽奖次数校验...")return
}// consumePointsByAct 活动是否需要消费积分func (lottery *Lottery) consumePointsByAct(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动是否需要消费积分...")return
}// checkTimesBySession 活动抽奖次数校验func (lottery *Lottery) checkTimesBySession(ctx *Context) (err error) {
fmt.Println(runFuncName(), "活动抽奖次数校验...")return
}// getPrizesBySession 获取场次奖品信息func (lottery *Lottery) getPrizesBySession(ctx *Context) (err error) {
fmt.Println(runFuncName(), "获取场次奖品信息...")return
}// getPrizesByNode 获取node奖品信息(不同活动类型实现不同)// 不同场景变化的算法 转化为依赖抽象func (lottery *Lottery) getPrizesByNode(ctx *Context) (err error) {// 实际依赖的接口的抽象方法return lottery.ConcreteBehavior.getPrizesByNode(ctx)
}// drawPrizes 抽奖func (lottery *Lottery) drawPrizes(ctx *Context) (err error) {
fmt.Println(runFuncName(), "抽奖...")return
}// checkPrizesStock 奖品数量判断func (lottery *Lottery) checkPrizesStock(ctx *Context) (err error) {
fmt.Println(runFuncName(), "奖品数量判断...")return
}// packagePrizeInfo 组装奖品信息func (lottery *Lottery) packagePrizeInfo(ctx *Context) (err error) {
fmt.Println(runFuncName(), "组装奖品信息...")return
}func main() {
ctx := &Context{
ActInfo: &ActInfo{
ActivityType: ConstActTypeAmount,
},
}switch ctx.ActInfo.ActivityType {case ConstActTypeTime: // 按时间抽奖类型
instance := &TimeDraw{}
instance.ConcreteBehavior = instance
instance.Run(ctx)case ConstActTypeTimes: // 按抽奖次数抽奖
instance := &TimesDraw{}
instance.ConcreteBehavior = instance
instance.Run(ctx)case ConstActTypeAmount: // 按数额范围区间抽奖
instance := &AmountDraw{}
instance.ConcreteBehavior = instance
instance.Run(ctx)default:// 报错return
}
}// 获取正在运行的函数名func runFuncName() string {
pc := make([]uintptr, 1)
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])return f.Name()
}

以下是代码执行结果:

[Running] go run ".../easy-tips/go/src/patterns/template/templateOther.go"
main.(*Lottery).checkSerialNo 校验活动编号(serial_no)是否存在、并获取活动信息...
main.(*Lottery).checkStatus 校验活动、场次是否正在进行...
main.(*AmountDraw).checkParams 按数额范围区间抽奖:特殊参数校验...
main.(*Lottery).checkTimesByAct 活动抽奖次数校验...
main.(*Lottery).consumePointsByAct 活动是否需要消费积分...
main.(*Lottery).checkTimesBySession 活动抽奖次数校验...
main.(*Lottery).getPrizesBySession 获取场次奖品信息...
main.(*AmountDraw).getPrizesByNode 1. 判断属于哪个数额区间...
main.(*AmountDraw).getPrizesByNode 2. 获取对应node的奖品信息...
main.(*AmountDraw).getPrizesByNode 3. 复写原所有奖品信息(抽取该node节点的奖品)...
main.(*Lottery).drawPrizes 抽奖...
main.(*Lottery).checkPrizesStock 奖品数量判断...
main.(*Lottery).packagePrizeInfo 组装奖品信息...

demo2代码地址:https://github.com/TIGERB/easy-tips/blob/master/go/src/patterns/template/templateOther.go

结语

最后总结下,「模板模式」抽象过程的核心是把握不变与变:

  • 不变:Run方法里的抽奖步骤 -> 被继承复用

  • 变:不同场景下 -> 被具体实现

    • checkParams参数校验逻辑

    • getPrizesByNode获取该节点奖品的逻辑

特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。

1b8b4186a7666a2e417d866bdf1a4a6a.png

d630b45cf14e77443a16d2ebb58189ca.gif

点击

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值