DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,847 @@
package idempotency
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"github.com/gomodule/redigo/redis"
"longheng.io/server/internal/platform/db"
)
const redisBeginScript = `
-- Begin 原子检查 committed/pending/sequence 水位,只写 pending 占位,不提交业务结果。
local committed_key = KEYS[1]
local committed_seq_key = KEYS[2]
local pending_key = KEYS[3]
local pending_seq_key = KEYS[4]
local ttl_ms = tonumber(ARGV[1])
local seq = tonumber(ARGV[2]) or 0
local fingerprint = ARGV[3] or ""
local owner = ARGV[4] or ""
local function metadata(value)
local first = string.find(value or "", "\n", 1, true)
if first == nil then return "", "" end
local second = string.find(value, "\n", first + 1, true)
if second == nil then return string.sub(value, first + 1), "" end
return string.sub(value, first + 1, second - 1), string.sub(value, second + 1)
end
local function conflicts(value)
local stored, _ = metadata(value)
return stored ~= "" and fingerprint ~= "" and stored ~= fingerprint
end
local function marker(status)
return status .. "\n" .. fingerprint .. "\n" .. owner
end
local committed = redis.call("GET", committed_key)
if committed then
if conflicts(committed) then return "conflict" end
return "duplicate"
end
local pending_value = redis.call("GET", pending_key)
if pending_value then
if conflicts(pending_value) then return "conflict" end
return "in_flight"
end
if seq > 0 then
local highest = tonumber(redis.call("GET", committed_seq_key) or "0")
if highest >= seq then
redis.call("PSETEX", committed_key, ttl_ms, marker("replay"))
return "replay"
end
while true do
local pending = redis.call("ZREVRANGE", pending_seq_key, 0, 0, "WITHSCORES")
if pending[1] == nil then
break
end
if redis.call("EXISTS", pending[1]) == 1 then
return "in_flight"
end
redis.call("ZREM", pending_seq_key, pending[1])
end
redis.call("ZADD", pending_seq_key, seq, pending_key)
redis.call("PEXPIRE", pending_seq_key, ttl_ms)
end
redis.call("PSETEX", pending_key, ttl_ms, marker("pending"))
return "accepted"
`
const redisCommitScript = `
-- Commit 把 pending 移入 committed,并推进 sequence 水位;之后重复请求只能走 duplicate/replay。
local committed_key = KEYS[1]
local committed_seq_key = KEYS[2]
local pending_key = KEYS[3]
local pending_seq_key = KEYS[4]
local ttl_ms = tonumber(ARGV[1])
local seq = tonumber(ARGV[2]) or 0
local status = ARGV[3]
local fingerprint = ARGV[4] or ""
local owner = ARGV[5] or ""
if status == "" then
status = "accepted"
end
local function metadata(value)
local first = string.find(value or "", "\n", 1, true)
if first == nil then return "", "" end
local second = string.find(value, "\n", first + 1, true)
if second == nil then return string.sub(value, first + 1), "" end
return string.sub(value, first + 1, second - 1), string.sub(value, second + 1)
end
local function conflicts(value)
local stored, _ = metadata(value)
return stored ~= "" and fingerprint ~= "" and stored ~= fingerprint
end
local committed = redis.call("GET", committed_key)
if committed then
if conflicts(committed) then return "conflict" end
local _, committed_owner = metadata(committed)
if committed_owner == owner then return "already_committed" end
return "lost"
end
local pending = redis.call("GET", pending_key)
if pending then
local _, pending_owner = metadata(pending)
if pending_owner ~= owner then return "lost" end
if conflicts(pending) then return "conflict" end
elseif owner ~= "" then
return "lost"
end
redis.call("PSETEX", committed_key, ttl_ms, status .. "\n" .. fingerprint .. "\n" .. owner)
if seq > 0 then
local highest = tonumber(redis.call("GET", committed_seq_key) or "0")
if highest < seq then
redis.call("SET", committed_seq_key, seq, "PX", ttl_ms)
else
redis.call("PEXPIRE", committed_seq_key, ttl_ms)
end
redis.call("ZREM", pending_seq_key, pending_key)
end
redis.call("DEL", pending_key)
return status
`
const redisCommitResLua = `
-- CommitResult 在一个 Lua 原子操作里提交防重状态和可重放结果,响应热缓存不参与一致性边界。
local committed_key = KEYS[1]
local committed_seq_key = KEYS[2]
local pending_key = KEYS[3]
local pending_seq_key = KEYS[4]
local result_key = KEYS[5]
local ttl_ms = tonumber(ARGV[1])
local seq = tonumber(ARGV[2]) or 0
local status = ARGV[3]
local result = ARGV[4]
local fingerprint = ARGV[5] or ""
local owner = ARGV[6] or ""
if status == "" then
status = "accepted"
end
local function metadata(value)
local first = string.find(value or "", "\n", 1, true)
if first == nil then return "", "" end
local second = string.find(value, "\n", first + 1, true)
if second == nil then return string.sub(value, first + 1), "" end
return string.sub(value, first + 1, second - 1), string.sub(value, second + 1)
end
local function conflicts(value)
local stored, _ = metadata(value)
return stored ~= "" and fingerprint ~= "" and stored ~= fingerprint
end
local committed = redis.call("GET", committed_key)
if committed then
if conflicts(committed) then return "conflict" end
local _, committed_owner = metadata(committed)
if committed_owner == owner and redis.call("GET", result_key) == result then return "already_committed" end
return "lost"
end
local pending = redis.call("GET", pending_key)
if pending then
local _, pending_owner = metadata(pending)
if pending_owner ~= owner then return "lost" end
if conflicts(pending) then return "conflict" end
elseif owner ~= "" then
return "lost"
end
redis.call("PSETEX", committed_key, ttl_ms, status .. "\n" .. fingerprint .. "\n" .. owner)
redis.call("PSETEX", result_key, ttl_ms, result)
if seq > 0 then
local highest = tonumber(redis.call("GET", committed_seq_key) or "0")
if highest < seq then
redis.call("SET", committed_seq_key, seq, "PX", ttl_ms)
else
redis.call("PEXPIRE", committed_seq_key, ttl_ms)
end
redis.call("ZREM", pending_seq_key, pending_key)
end
redis.call("DEL", pending_key)
return status
`
const redisAbortScript = `
-- Abort 只删除 pending key 和 pending sequence 索引,不推进 committed 水位,允许失败请求重试。
local pending_key = KEYS[1]
local pending_seq_key = KEYS[2]
local fingerprint = ARGV[1] or ""
local owner = ARGV[2] or ""
local pending = redis.call("GET", pending_key)
if pending then
local split = string.find(pending, "\n", 1, true)
local stored = ""
local stored_owner = ""
if split ~= nil then
local second = string.find(pending, "\n", split + 1, true)
if second == nil then
stored = string.sub(pending, split + 1)
else
stored = string.sub(pending, split + 1, second - 1)
stored_owner = string.sub(pending, second + 1)
end
end
if stored_owner ~= owner then return "lost" end
if stored ~= "" and fingerprint ~= "" and stored ~= fingerprint then
return "conflict"
end
elseif owner ~= "" then
return "lost"
end
redis.call("DEL", pending_key)
redis.call("ZREM", pending_seq_key, pending_key)
return "aborted"
`
const redisCommitLookup = `
local key = KEYS[1]
local seq_key = KEYS[2]
local ttl_ms = tonumber(ARGV[1])
local seq = tonumber(ARGV[2]) or 0
local fingerprint = ARGV[3] or ""
local value = redis.call("GET", key)
if value then
local split = string.find(value, "\n", 1, true)
local stored = ""
if split ~= nil then
local second = string.find(value, "\n", split + 1, true)
if second == nil then stored = string.sub(value, split + 1) else stored = string.sub(value, split + 1, second - 1) end
end
if stored ~= "" and fingerprint ~= "" and stored ~= fingerprint then
return "conflict"
end
return "duplicate"
end
if seq > 0 then
local highest = tonumber(redis.call("GET", seq_key) or "0")
if highest >= seq then
local source_ttl = tonumber(redis.call("PTTL", seq_key) or "-1")
local replay_ttl = ttl_ms
if source_ttl > 0 and source_ttl < replay_ttl then replay_ttl = source_ttl end
if replay_ttl > 0 then redis.call("PSETEX", key, replay_ttl, "replay\n" .. fingerprint .. "\n") end
return "replay"
end
end
return "miss"
`
const redisCommitStore = `
local key = KEYS[1]
local seq_key = KEYS[2]
local ttl_ms = tonumber(ARGV[1])
local seq = tonumber(ARGV[2]) or 0
local status = ARGV[3]
local fingerprint = ARGV[4] or ""
local owner = ARGV[5] or ""
if status == "" then
status = "accepted"
end
local current = redis.call("GET", key)
if current then
local split = string.find(current, "\n", 1, true)
local stored = ""
if split ~= nil then
local second = string.find(current, "\n", split + 1, true)
if second == nil then stored = string.sub(current, split + 1) else stored = string.sub(current, split + 1, second - 1) end
end
if stored ~= "" and fingerprint ~= "" and stored ~= fingerprint then
return "conflict"
end
end
redis.call("PSETEX", key, ttl_ms, status .. "\n" .. fingerprint .. "\n" .. owner)
if seq > 0 then
local highest = tonumber(redis.call("GET", seq_key) or "0")
if highest < seq then
redis.call("SET", seq_key, seq, "PX", ttl_ms)
else
redis.call("PEXPIRE", seq_key, ttl_ms)
end
end
return status
`
const redisCommitRecover = `
local key = KEYS[1]
local seq_key = KEYS[2]
local seq = tonumber(ARGV[1]) or 0
local fingerprint = ARGV[2] or ""
local owner = ARGV[3] or ""
local value = redis.call("GET", key)
if value then
local split = string.find(value, "\n", 1, true)
local stored = ""
local stored_owner = ""
if split ~= nil then
local second = string.find(value, "\n", split + 1, true)
if second == nil then stored = string.sub(value, split + 1) else
stored = string.sub(value, split + 1, second - 1)
stored_owner = string.sub(value, second + 1)
end
end
if stored ~= "" and fingerprint ~= "" and stored ~= fingerprint then
return "conflict"
end
if stored_owner == owner then return "hit" end
return "lost"
end
if owner == "" and seq > 0 then
local highest = tonumber(redis.call("GET", seq_key) or "0")
if highest >= seq then
return "hit"
end
end
return "miss"
`
const redisResultLookup = `
return redis.call("GET", KEYS[1])
`
const redisResultRecover = `
if redis.call("EXISTS", KEYS[1]) ~= 1 then
return "miss"
end
local marker = redis.call("GET", KEYS[1])
local first = string.find(marker or "", "\n", 1, true)
local stored_owner = ""
if first ~= nil then
local second = string.find(marker, "\n", first + 1, true)
if second ~= nil then stored_owner = string.sub(marker, second + 1) end
end
if stored_owner ~= ARGV[2] then return "miss" end
local result = redis.call("GET", KEYS[2])
if result == ARGV[1] then
return "hit"
end
return "miss"
`
type RedisOptions struct {
Address string
Password string
DB int
KeyPrefix string
PoolSize int
Timeout time.Duration
ConnectTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
TTL time.Duration
Now func() time.Time
Executor db.RedisExecutor
}
type redisStore struct {
keyPrefix string
ttl time.Duration
executor db.RedisExecutor
closer interface{ Close() error }
}
func NewRedis(options RedisOptions) *Guard {
options = normRedisOpts(options)
return New(Options{
TTL: options.TTL,
Now: options.Now,
Kind: "redis",
Store: newRedisOpts(options),
})
}
func newRedisOpts(options RedisOptions) *redisStore {
options = normRedisOpts(options)
executor := options.Executor
var closer interface{ Close() error }
if executor == nil {
redigo := db.NewRedigoExecutor(db.RedigoOptions{
Address: options.Address,
Password: options.Password,
DB: options.DB,
PoolSize: options.PoolSize,
Timeout: options.Timeout,
ConnectTimeout: options.ConnectTimeout,
ReadTimeout: options.ReadTimeout,
WriteTimeout: options.WriteTimeout,
})
executor = redigo
closer = redigo
} else if candidate, ok := executor.(interface{ Close() error }); ok {
closer = candidate
}
return newRedisStore(options.KeyPrefix, options.TTL, executor, closer)
}
func newRedisStore(keyPrefix string, ttl time.Duration, executor db.RedisExecutor, closer interface{ Close() error }) *redisStore {
if ttl <= 0 {
ttl = 10 * time.Minute
}
keyPrefix = strings.TrimSpace(keyPrefix)
if keyPrefix == "" {
keyPrefix = "idempotency"
}
return &redisStore{
keyPrefix: keyPrefix,
ttl: ttl,
executor: executor,
closer: closer,
}
}
func normRedisOpts(options RedisOptions) RedisOptions {
options.Address = strings.TrimSpace(options.Address)
if options.Address == "" {
options.Address = "127.0.0.1:6379"
}
options.KeyPrefix = strings.TrimSpace(options.KeyPrefix)
if options.KeyPrefix == "" {
options.KeyPrefix = "idempotency"
}
if options.PoolSize <= 0 {
options.PoolSize = 8
}
if options.Timeout <= 0 {
options.Timeout = 2 * time.Second
}
if options.ConnectTimeout <= 0 {
options.ConnectTimeout = options.Timeout
}
if options.ReadTimeout <= 0 {
options.ReadTimeout = options.Timeout
}
if options.WriteTimeout <= 0 {
options.WriteTimeout = options.Timeout
}
if options.TTL <= 0 {
options.TTL = 10 * time.Minute
}
return options
}
func (s *redisStore) Check(ctx context.Context, item Request) (Decision, error) {
decision, err := s.Begin(ctx, item)
if err != nil {
return Decision{}, err
}
if decision.Status == StatusAccepted {
if err := s.Commit(ctx, item, decision); err != nil {
return Decision{}, err
}
}
return decision, nil
}
func (s *redisStore) Begin(ctx context.Context, item Request) (Decision, error) {
if s == nil || s.executor == nil {
return Decision{}, fmt.Errorf("redis idempotency executor is nil")
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return Decision{}, err
}
key := item.Key
if key == "" {
key = DerivedKey(item.Scope, item.Subject, item.Session, item.Sequence)
}
// Redis Begin 全部交给 Lua,避免多 logic 节点同时接受同一 key 或同一 sequence。
statusText, err := redis.String(s.executor.Do(
ctx,
"EVAL",
redisBeginScript,
4,
s.requestKey(key),
s.sequenceKey(sequenceScope(item.Scope, item.Subject, item.Session)),
s.pendingRequestKey(key),
s.pendingSequenceKey(sequenceScope(item.Scope, item.Subject, item.Session)),
int64(s.ttlOrDefault()/time.Millisecond),
item.Sequence,
item.Fingerprint,
item.reservationToken,
))
if err != nil {
return Decision{}, err
}
if statusText == "conflict" {
return Decision{}, ErrRequestConflict
}
status := Status(statusText)
if status != StatusDuplicate && status != StatusInFlight && status != StatusReplay {
status = StatusAccepted
}
decision := Decision{Status: status, Key: key, Sequence: item.Sequence}
if status == StatusAccepted {
decision.ownerToken = item.reservationToken
}
return decision, nil
}
func (s *redisStore) Commit(ctx context.Context, item Request, decision Decision) error {
if s == nil || s.executor == nil {
return fmt.Errorf("redis idempotency executor is nil")
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return err
}
key := decision.Key
if key == "" {
key = item.Key
}
if key == "" {
key = DerivedKey(item.Scope, item.Subject, item.Session, item.Sequence)
}
status := decision.Status
if status == "" {
status = StatusAccepted
}
// Commit 的 Lua 同步删除 pending 并写 committed;调用方必须在业务 handler 成功后再调用。
statusText, err := redis.String(s.executor.Do(
ctx,
"EVAL",
redisCommitScript,
4,
s.requestKey(key),
s.sequenceKey(sequenceScope(item.Scope, item.Subject, item.Session)),
s.pendingRequestKey(key),
s.pendingSequenceKey(sequenceScope(item.Scope, item.Subject, item.Session)),
int64(s.ttlOrDefault()/time.Millisecond),
item.Sequence,
string(status),
item.Fingerprint,
decision.ownerToken,
))
if err == nil && statusText == "conflict" {
return ErrRequestConflict
}
if err == nil && statusText == "lost" {
return ErrReservationLost
}
if err != nil && s.redisCommitRecovered(ctx, item, key, decision.ownerToken) {
return nil
}
return err
}
func (s *redisStore) CommitResult(ctx context.Context, item Request, decision Decision, payload []byte) error {
if s == nil || s.executor == nil {
return fmt.Errorf("redis idempotency executor is nil")
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return err
}
key := decision.Key
if key == "" {
key = item.Key
}
if key == "" {
key = DerivedKey(item.Scope, item.Subject, item.Session, item.Sequence)
}
status := decision.Status
if status == "" {
status = StatusAccepted
}
statusText, err := redis.String(s.executor.Do(
ctx,
"EVAL",
redisCommitResLua,
5,
s.requestKey(key),
s.sequenceKey(sequenceScope(item.Scope, item.Subject, item.Session)),
s.pendingRequestKey(key),
s.pendingSequenceKey(sequenceScope(item.Scope, item.Subject, item.Session)),
s.resultKey(key),
int64(s.ttlOrDefault()/time.Millisecond),
item.Sequence,
string(status),
payload,
item.Fingerprint,
decision.ownerToken,
))
if err == nil && statusText == "conflict" {
return ErrRequestConflict
}
if err == nil && statusText == "lost" {
return ErrReservationLost
}
if err != nil && s.redisResultRecovered(ctx, key, payload, decision.ownerToken) {
return nil
}
return err
}
func (s *redisStore) LookupResult(ctx context.Context, decision Decision) ([]byte, bool, error) {
if s == nil || s.executor == nil {
return nil, false, fmt.Errorf("redis idempotency executor is nil")
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, false, err
}
key := strings.TrimSpace(decision.Key)
if key == "" {
return nil, false, nil
}
payload, err := redis.Bytes(s.executor.Do(ctx, "EVAL", redisResultLookup, 1, s.resultKey(key)))
if errors.Is(err, redis.ErrNil) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
return append([]byte(nil), payload...), true, nil
}
func (s *redisStore) redisResultRecovered(parent context.Context, key string, payload []byte, ownerToken string) bool {
ctx, cancel := idemRecoverCtx(parent)
defer cancel()
status, err := redis.String(s.executor.Do(
ctx,
"EVAL",
redisResultRecover,
2,
s.requestKey(key),
s.resultKey(key),
payload,
ownerToken,
))
return err == nil && status == "hit"
}
func (s *redisStore) redisCommitRecovered(parent context.Context, item Request, key, ownerToken string) bool {
ctx, cancel := idemRecoverCtx(parent)
defer cancel()
status, err := redis.String(s.executor.Do(
ctx,
"EVAL",
redisCommitRecover,
2,
s.requestKey(key),
s.sequenceKey(sequenceScope(item.Scope, item.Subject, item.Session)),
item.Sequence,
item.Fingerprint,
ownerToken,
))
return err == nil && status == "hit"
}
func (s *redisStore) Abort(ctx context.Context, item Request, decision Decision) error {
if s == nil || s.executor == nil {
return fmt.Errorf("redis idempotency executor is nil")
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return err
}
key := decision.Key
if key == "" {
key = item.Key
}
if key == "" {
key = DerivedKey(item.Scope, item.Subject, item.Session, item.Sequence)
}
// Abort 用于 handler 失败,避免 Redis pending 卡住后续重试。
statusText, err := redis.String(s.executor.Do(
ctx,
"EVAL",
redisAbortScript,
2,
s.pendingRequestKey(key),
s.pendingSequenceKey(sequenceScope(item.Scope, item.Subject, item.Session)),
item.Fingerprint,
decision.ownerToken,
))
if err == nil && statusText == "conflict" {
return ErrRequestConflict
}
if err == nil && statusText == "lost" {
return ErrReservationLost
}
return err
}
func (s *redisStore) LookupCommitted(ctx context.Context, item Request) (Decision, bool, error) {
if s == nil || s.executor == nil {
return Decision{}, false, fmt.Errorf("redis idempotency executor is nil")
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return Decision{}, false, err
}
key := item.Key
if key == "" {
key = DerivedKey(item.Scope, item.Subject, item.Session, item.Sequence)
}
statusText, err := redis.String(s.executor.Do(
ctx,
"EVAL",
redisCommitLookup,
2,
s.committedRequestKey(key),
s.committedSequenceKey(sequenceScope(item.Scope, item.Subject, item.Session)),
int64(s.ttlOrDefault()/time.Millisecond),
item.Sequence,
item.Fingerprint,
))
if err != nil {
return Decision{}, false, err
}
switch Status(statusText) {
case StatusDuplicate:
return Decision{Status: StatusDuplicate, Key: key, Sequence: item.Sequence}, true, nil
case StatusReplay:
return Decision{Status: StatusReplay, Key: key, Sequence: item.Sequence}, true, nil
default:
if statusText == "conflict" {
return Decision{}, false, ErrRequestConflict
}
return Decision{}, false, nil
}
}
func (s *redisStore) StoreCommitted(ctx context.Context, item Request, decision Decision) error {
return s.storeCommittedTTL(ctx, item, decision, s.ttlOrDefault())
}
func (s *redisStore) storeCommittedTTL(ctx context.Context, item Request, decision Decision, ttl time.Duration) error {
if s == nil || s.executor == nil {
return fmt.Errorf("redis idempotency executor is nil")
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return err
}
if ttl < time.Millisecond {
return nil
}
key := decision.Key
if key == "" {
key = item.Key
}
if key == "" {
key = DerivedKey(item.Scope, item.Subject, item.Session, item.Sequence)
}
status := decision.Status
if status == "" {
status = StatusAccepted
}
statusText, err := redis.String(s.executor.Do(
ctx,
"EVAL",
redisCommitStore,
2,
s.committedRequestKey(key),
s.committedSequenceKey(sequenceScope(item.Scope, item.Subject, item.Session)),
int64(ttl/time.Millisecond),
item.Sequence,
string(status),
item.Fingerprint,
decision.ownerToken,
))
if err == nil && statusText == "conflict" {
return ErrRequestConflict
}
return err
}
func (s *redisStore) Snapshot() map[string]any {
if s == nil {
return map[string]any{}
}
return map[string]any{
"backend": "redis",
"ttl_seconds": int64(s.ttlOrDefault() / time.Second),
"key_prefix": s.keyPrefixOrDefault(),
}
}
func (s *redisStore) Close(context.Context) error {
if s == nil || s.closer == nil {
return nil
}
return s.closer.Close()
}
func (s *redisStore) requestKey(key string) string {
return s.keyPrefixOrDefault() + ":key:" + digestKey(key)
}
func (s *redisStore) sequenceKey(scope string) string {
return s.keyPrefixOrDefault() + ":seq:" + digestKey(scope)
}
func (s *redisStore) committedRequestKey(key string) string {
return s.keyPrefixOrDefault() + ":committed:key:" + digestKey(key)
}
func (s *redisStore) committedSequenceKey(scope string) string {
return s.keyPrefixOrDefault() + ":committed:seq:" + digestKey(scope)
}
func (s *redisStore) pendingRequestKey(key string) string {
return s.keyPrefixOrDefault() + ":pending:key:" + digestKey(key)
}
func (s *redisStore) pendingSequenceKey(scope string) string {
return s.keyPrefixOrDefault() + ":pending:seq:" + digestKey(scope)
}
func (s *redisStore) resultKey(key string) string {
return s.keyPrefixOrDefault() + ":result:key:" + digestKey(key)
}
func (s *redisStore) ttlOrDefault() time.Duration {
if s == nil || s.ttl <= 0 {
return 10 * time.Minute
}
return s.ttl
}
func (s *redisStore) keyPrefixOrDefault() string {
if s == nil {
return "idempotency"
}
prefix := strings.TrimSpace(s.keyPrefix)
if prefix == "" {
return "idempotency"
}
return prefix
}
func digestKey(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])
}