-
Notifications
You must be signed in to change notification settings - Fork 7
添加 会员 模块 #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
添加 会员 模块 #20
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #20 +/- ##
======================================
Coverage ? 1.51%
======================================
Files ? 38
Lines ? 2249
Branches ? 0
======================================
Hits ? 34
Misses ? 2214
Partials ? 1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
internal/repository/dao/quota.go
Outdated
} | ||
|
||
// Deduct 扣减 | ||
func (dao *QuotaDao) Deduct(ctx context.Context, uid int64, amount int64) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这部分逻辑挪上去 service 里面,并且你不需要使用悲观锁,用乐观锁来控制。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
悲观锁性能太差了
internal/service/quota.go
Outdated
return &QuotaService{repo: repo} | ||
} | ||
|
||
func (q *QuotaService) CreateQuota(ctx context.Context, quota domain.Quota) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
把这个修改为 Create 修改为 Create Or Incr 的语义。也就是要么是创建,要么是增加 quota 里面金额,quota里面金额可以是负数。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里也需要有一个额外的参数 key,保证幂等。
internal/service/quota.go
Outdated
return q.repo.CreateQuota(ctx, quota) | ||
} | ||
|
||
func (q *QuotaService) CreateTempQuota(ctx context.Context, quota domain.TempQuota) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
在 TempQuota 里面加一个 key 字段,string,作为唯一索引,我想到这个东西是需要考虑去重的。
internal/service/quota.go
Outdated
return q.repo.GetQuota(ctx, uid) | ||
} | ||
|
||
func (q *QuotaService) Deduct(ctx context.Context, uid int64, amount int64) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个要做成幂等的,所以还需要一个 key 的参数,作为去重。为此,你可以额外引入一个 DeductRecord 的记录表,一方面是去重,一方面也是记录整个金额的变化。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DeductRecord 这个名字不太好,因为那边 CreateOr Incr 的语义也同样需要去重。改为 QuotaRecord 表。
internal/web/quota.go
Outdated
|
||
func (q *QuotaHandler) PublicRoutes(server *gin.Engine) { | ||
group := server.Group("/quota") | ||
group.POST("/create", ginx.BS(q.CreateTempQuota)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
你这是不是重复注册了 1.CreateTempQuota?这里进一步分 tmpG := group.Group("/tmp")
internal/service/quota.go
Outdated
func (q *QuotaService) Deduct(ctx context.Context, uid int64, amount int64, key string) error { | ||
key1 := fmt.Sprintf("temp_%s", key) | ||
key2 := fmt.Sprintf("quota_%s", key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
不是这么用的,意思是扣减是一个整体,记录到 QuotaRecord 中。换言之,虽然额度分成了 Quota 和 temp Quota 两个,但是扣减这个,只会有一条 QuotaRecord。
我感觉就是在 service 里面操作数据库事务这个东西我还没说过怎么操作,那么你就挪回去 dao 里面。然后逻辑是这样的:
Begin()
insertQuotaRecord() // 检测是否成功
// 成功往后执行
// 往后执行后续的扣减逻辑。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
然后要记住在扣减的时候,要注意用 CAS 操作来解决并发问题。因为你要考虑两个 goroutine,一个扣减 10,一个扣减 20 这种并发的情况。
internal/repository/dao/quota.go
Outdated
Ctime: now, | ||
Utime: now, | ||
} | ||
result := tx.Clauses(clause.OnConflict{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个逻辑不对。是这样的:
- 如果 QuotaRecord 插入成功,说明这是一个新请求,那么就往后执行,后面的操作是对的
- 如果 QuotaRecord 插入失败,就说明要么数据库有问题,要么是重复请求,直接结束整个事务,返回 error。
internal/repository/dao/quota.go
Outdated
} | ||
tq := &tempQuotas[i] | ||
deduct := tq.Amount | ||
if deduct > remain { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
还用 min 方法就可以,现在我记得 SDK 自带了泛型的 min 方法
internal/repository/dao/quota.go
Outdated
remain := amount | ||
|
||
// 先扣临时额度 | ||
for i := range tempQuotas { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里可以直接用 tmpQuota, i := range 的写法,在 golint 里面忽略掉这个检测。
internal/repository/dao/quota.go
Outdated
Updates(map[string]any{ | ||
"amount": gorm.Expr("amount - ?", remain), | ||
"utime": now, | ||
"last_clear_time": now, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个不要更新,last_clear_time 是用户必须要在负数之后重置成功成为了正数,才需要修改。
internal/repository/dao/quota.go
Outdated
if update.RowsAffected == 0 { | ||
continue // 这条被其他并发扣完,跳过 | ||
} | ||
remain -= deduct |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
把前面的 remain < 0 break 的检测挪动到这后面
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<= 0 应该是直接 return 了。
internal/repository/dao/quota.go
Outdated
} | ||
|
||
// 如果还有剩余,从主额度扣 | ||
if remain > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
当前面你用 <= 0 return 之后,这边就不需要检测 remain 了
errs/errs.go
Outdated
@@ -18,4 +18,7 @@ import ( | |||
"errors" | |||
) | |||
|
|||
var ErrBizConfigNotFound = errors.New("biz config not found") | |||
var ( | |||
DeductAmount = errors.New("扣减失败") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
所有的 error 都用 Err 作为名字前缀,然后额外加一个叫做 ErrInsufficientQuota,明确是额度不足,后续可以想到业务方可能会检测Insufficient 错误,然后提示用户充值。
把对应的 CI 问题都修一下。 |
明哥你给 proto 生成的 go 文件干没了,导致ci 全没了😂 |
internal/repository/dao/quota.go
Outdated
Ctime: now, | ||
Utime: now, | ||
} | ||
result := tx.Clauses(clause.OnConflict{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
类似于 SaveQuota,你这里也是直接插入 Record,插入成功了才算是去重了,才需要执行后续的步骤。
|
||
run: | | ||
go install golang.org/x/tools/cmd/goimports@latest | ||
go install mvdan.cc/gofumpt@latest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
为啥需要这个?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
我觉得还是删了吧,有一个基本的格式化的就可以了,多了我也遭不住。
var ErrBizConfigNotFound = errors.New("biz config not found") | ||
var ( | ||
ErrDeductAmountFailed = errors.New("扣减失败") | ||
ErrBizConfigNotFound = errors.New("biz config not found") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
大哥都改了,都改为中文
return &QuotaDao{db: db} | ||
} | ||
|
||
func (dao *QuotaDao) SaveTempQuota(ctx context.Context, quota TempQuota) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tmp Quota 是不需要 Save 的,就是直接插入。如果是重复请求就直接报错。
|
||
return tx.Clauses(clause.OnConflict{ | ||
Columns: []clause.Column{{Name: "key"}}, | ||
DoUpdates: clause.Assignments(map[string]any{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这边记得改为 amount 相加
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
而后整个方法改名为 AddQuota。从 service 一路重命名下来。
err := tx.Where("amount > ? and uid = ?", 0, uid).First("a).Error | ||
if err != nil { | ||
// 表示找不到可以扣减的temp | ||
if errors.Is(err, gorm.ErrRecordNotFound) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里要加一个测试用例,测试这个场景。
return &QuotaHandler{svc: svc} | ||
} | ||
|
||
func (q *QuotaHandler) PublicRoutes(_ *gin.Engine) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这种空方法用不上的就直接删了
} | ||
|
||
for _, tc := range testcases { | ||
t.Run(tc.name, func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个 Handler 你可以在 setup suite 的时候就直接搞好。然后包括 mock session 啥的,都可以直接用 AnyTime 确保可以用多次 Claims 方法,并且都返回同一个值。
return slice.Map[domain.TempQuota, QuotaResponse](tempQuotaList, func(idx int, src domain.TempQuota) QuotaResponse { | ||
return QuotaResponse{ | ||
Amount: src.Amount, | ||
StartTime: time.Unix(src.StartTime, 0).Format("2006-01-02 15:04:05"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
后端不要格式化,直接返回时间戳毫秒数。
return systemErrorResult, nil | ||
} | ||
|
||
start, _ := q.toTimestamp(req.StartTime) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里假设前端返回的是 unix 毫秒数。
|
||
err := q.svc.SaveTempQuota(ctx, domain.TempQuota{Amount: req.Amount, Uid: uid, StartTime: start, EndTime: end}) | ||
if err != nil { | ||
return systemErrorResult, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return systemErrorResult, err
这样 ginx 会帮你记录这个错误,其余地方也一样。
No description provided.