- settlement(1.1): exhibition_revenue_records 加 UNIQUE(exhibition_id,cycle_start_time) + CreateRevenueRecord ON CONFLICT DO NOTHING; MQ 用 settled_at 替代复用 is_processed; 恢复扫描器过滤改 settled_at IS NULL; created_at 统一毫秒; 删死代码 cleanup_worker.go。 - hours(1.2): 新增 exhibition_hours_log/asset_exhibition_hours_log(source_id 唯一)幂等表; fan_profile/assetLevel 的 AddExhibitionHours 按 sourceID 幂等(事务包裹); 存量重算脚本。 - mint(1.4/1.5/1.6): crystal_transaction_records (source_id,change_type) 部分唯一索引 + UpdateCrystalBalance/CreateMintOrder 幂等; 保底改 crypto/rand; 下线伪 tx_hash; doMint Redis Lua 原子限流。 - migrations 001/002/003; 各服务单测(自包含, 缺 DB t.Skip)。 Co-Authored-By: Claude <noreply@anthropic.com>
61 lines
1.9 KiB
Go
61 lines
1.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// mintRateLimitScript atomically increments the current window counter and
|
|
// assigns its TTL when the key is first created.
|
|
var mintRateLimitScript = redis.NewScript(`
|
|
local n = redis.call("INCR", KEYS[1])
|
|
if n == 1 then
|
|
redis.call("EXPIRE", KEYS[1], ARGV[1])
|
|
end
|
|
return {n, n <= tonumber(ARGV[2]) and 1 or 0}
|
|
`)
|
|
|
|
// MintRateLimiter limits successful mint attempts with a Redis fixed window.
|
|
type MintRateLimiter struct {
|
|
rdb *redis.Client
|
|
limit int64
|
|
window time.Duration
|
|
}
|
|
|
|
// NewMintRateLimiter creates a Redis-backed mint rate limiter.
|
|
func NewMintRateLimiter(rdb *redis.Client, limit int64, window time.Duration) *MintRateLimiter {
|
|
return &MintRateLimiter{rdb: rdb, limit: limit, window: window}
|
|
}
|
|
|
|
// IncrAndCheck atomically increments the owner's counter and returns its new
|
|
// value and whether it is within the configured limit.
|
|
func (l *MintRateLimiter) IncrAndCheck(ctx context.Context, ownerUID int64, assetType string) (int64, bool, error) {
|
|
if l == nil || l.rdb == nil {
|
|
return 0, false, fmt.Errorf("redis client is not initialized")
|
|
}
|
|
|
|
windowSeconds := int64(l.window.Seconds())
|
|
result, err := mintRateLimitScript.Run(ctx, l.rdb, []string{l.key(ownerUID, assetType)}, windowSeconds, l.limit).Result()
|
|
if err != nil {
|
|
return 0, false, fmt.Errorf("redis EVAL mint rate limit: %w", err)
|
|
}
|
|
|
|
values, ok := result.([]interface{})
|
|
if !ok || len(values) != 2 {
|
|
return 0, false, fmt.Errorf("redis EVAL returned unexpected shape: %v", result)
|
|
}
|
|
count, countOK := values[0].(int64)
|
|
allowed, allowedOK := values[1].(int64)
|
|
if !countOK || !allowedOK {
|
|
return 0, false, fmt.Errorf("redis EVAL returned unexpected values: %v", result)
|
|
}
|
|
return count, allowed == 1, nil
|
|
}
|
|
|
|
func (l *MintRateLimiter) key(ownerUID int64, assetType string) string {
|
|
return fmt.Sprintf("periph:mint:%s:%d:%s", assetType, ownerUID, time.Now().UTC().Format("20060102"))
|
|
}
|