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

+79
View File
@@ -0,0 +1,79 @@
package profile
import (
"context"
"fmt"
"time"
)
type RequestEvent struct {
AccountID string `json:"account_id"`
SessionID string `json:"session_id,omitempty"`
Route string `json:"route,omitempty"`
RequestID string `json:"request_id,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
StartedAt time.Time `json:"started_at"`
}
type RequestResult struct {
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Duration time.Duration `json:"duration"`
}
type RequestHook interface {
BeforeProfileRequest(context.Context, RequestEvent) context.Context
AfterProfileRequest(context.Context, RequestEvent, RequestResult)
}
type RequestHooks []RequestHook
func (hooks RequestHooks) Run(ctx context.Context, event RequestEvent, handler func(context.Context) error) error {
if ctx == nil {
ctx = context.Background()
}
if handler == nil {
return fmt.Errorf("profile request handler is required")
}
if event.StartedAt.IsZero() {
event.StartedAt = time.Now().UTC()
} else {
event.StartedAt = event.StartedAt.UTC()
}
event.Metadata = cloneRequestMetadata(event.Metadata)
for _, hook := range hooks {
if hook == nil {
continue
}
next := hook.BeforeProfileRequest(ctx, event)
if next != nil {
ctx = next
}
}
err := handler(ctx)
result := RequestResult{
OK: err == nil,
Duration: time.Since(event.StartedAt),
}
if err != nil {
result.Error = err.Error()
}
for _, hook := range hooks {
if hook != nil {
hook.AfterProfileRequest(ctx, event, result)
}
}
return err
}
func cloneRequestMetadata(in map[string]string) map[string]string {
if len(in) == 0 {
return nil
}
out := make(map[string]string, len(in))
for key, value := range in {
out[key] = value
}
return out
}
@@ -0,0 +1,118 @@
package profile
import (
"context"
"fmt"
"time"
"longheng.io/server/internal/platform/db"
)
type SaveActiveHook[T any] func(context.Context, string, T, T) error
type LifecycleOptions[T any, F comparable] struct {
Store db.Store[T]
Runtime *Runtime[T, F]
}
type Lifecycle[T any, F comparable] struct {
store db.Store[T]
runtime *Runtime[T, F]
}
func NewLifecycle[T any, F comparable](options LifecycleOptions[T, F]) (*Lifecycle[T, F], error) {
if options.Store == nil {
return nil, fmt.Errorf("profile lifecycle store is required")
}
if options.Runtime == nil {
return nil, fmt.Errorf("profile lifecycle runtime is required")
}
return &Lifecycle[T, F]{
store: options.Store,
runtime: options.Runtime,
}, nil
}
func (l *Lifecycle[T, F]) Preflight(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
return db.Check(ctx, l.store)
}
func (l *Lifecycle[T, F]) CloseOrFlush(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
return db.CloseOrFlush(ctx, l.store)
}
func (l *Lifecycle[T, F]) LoadOrCreate(ctx context.Context, key string) (T, bool, error) {
return l.runtime.LoadOrCreate(ctx, key)
}
func (l *Lifecycle[T, F]) SaveAndUnload(ctx context.Context, key string) (T, T, bool, error) {
return l.runtime.SaveAndUnload(ctx, key)
}
func (l *Lifecycle[T, F]) SaveActiveAndUnload(ctx context.Context, hook SaveActiveHook[T]) error {
if ctx == nil {
ctx = context.Background()
}
var firstErr error
for _, key := range l.runtime.ActiveKeys() {
saved, summary, ok, err := l.runtime.SaveAndUnload(ctx, key)
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
if ok && hook != nil {
if err := hook(ctx, key, saved, summary); err != nil && firstErr == nil {
firstErr = err
}
}
}
return firstErr
}
func (l *Lifecycle[T, F]) Upsert(key string, mutate func(T, bool, time.Time) (T, []F, error)) (T, error) {
return l.runtime.Upsert(key, mutate)
}
func (l *Lifecycle[T, F]) MutateLoaded(ctx context.Context, key string, mutate func(*T, time.Time) ([]F, error)) (T, T, error) {
return l.runtime.MutateLoaded(ctx, key, mutate)
}
func (l *Lifecycle[T, F]) MutateActive(key string, mutate func(*T, time.Time) ([]F, error)) (T, T, bool, error) {
return l.runtime.MutateActive(key, mutate)
}
func (l *Lifecycle[T, F]) LoadDetached(ctx context.Context, key string) (T, bool, error) {
return l.runtime.LoadDetached(ctx, key)
}
func (l *Lifecycle[T, F]) NewDetached(key string, now time.Time) T {
return l.runtime.NewDetached(key, now)
}
func (l *Lifecycle[T, F]) AttachIfAbsent(key string, record T, fields ...F) (T, bool) {
return l.runtime.AttachIfAbsent(key, record, fields...)
}
func (l *Lifecycle[T, F]) Get(key string) (T, bool) {
return l.runtime.Get(key)
}
func (l *Lifecycle[T, F]) Snapshot() []T {
return l.runtime.Snapshot()
}
func (l *Lifecycle[T, F]) ActiveKeys() []string {
return l.runtime.ActiveKeys()
}
func (l *Lifecycle[T, F]) ActiveByPredicate(match func(T) bool) (T, bool) {
return l.runtime.ActiveByPredicate(match)
}
@@ -0,0 +1,167 @@
package profile
import (
"context"
"fmt"
"slices"
"strings"
"sync"
"time"
)
type OfflineDelivery struct {
AccountID string `json:"account_id"`
Kind string `json:"kind"`
Payload []byte `json:"payload,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type OfflinePipeline interface {
EnqueueOffline(context.Context, OfflineDelivery) error
DrainOffline(context.Context, string, func(context.Context, OfflineDelivery) error) (int, error)
SnapshotOffline(string) []OfflineDelivery
}
type MemoryOfflinePipeline struct {
mu sync.Mutex
maxPerAccount int
now func() time.Time
deliveries map[string][]OfflineDelivery
}
func NewMemoryOfflinePipeline(maxPerAccount int, now func() time.Time) *MemoryOfflinePipeline {
if maxPerAccount <= 0 {
maxPerAccount = 256
}
if now == nil {
now = time.Now
}
return &MemoryOfflinePipeline{
maxPerAccount: maxPerAccount,
now: now,
deliveries: make(map[string][]OfflineDelivery),
}
}
func (p *MemoryOfflinePipeline) EnqueueOffline(ctx context.Context, delivery OfflineDelivery) error {
if err := ctxErr(ctx); err != nil {
return err
}
normalized, err := normOfflineDelivery(delivery, p.nowUTC())
if err != nil {
return err
}
p.mu.Lock()
defer p.mu.Unlock()
if p.deliveries == nil {
p.deliveries = make(map[string][]OfflineDelivery)
}
queue := slices.Clone(p.deliveries[normalized.AccountID])
queue = append(queue, cloneOfflineDelivery(normalized))
if len(queue) > p.maxPerAccount {
queue = slices.Clone(queue[len(queue)-p.maxPerAccount:])
}
p.deliveries[normalized.AccountID] = queue
return nil
}
func (p *MemoryOfflinePipeline) DrainOffline(ctx context.Context, accountID string, handler func(context.Context, OfflineDelivery) error) (int, error) {
if err := ctxErr(ctx); err != nil {
return 0, err
}
if handler == nil {
return 0, fmt.Errorf("offline delivery handler is required")
}
accountID = strings.TrimSpace(accountID)
if accountID == "" || p == nil {
return 0, nil
}
p.mu.Lock()
queue := cloneOfflineItems(p.deliveries[accountID])
p.mu.Unlock()
delivered := 0
for idx, delivery := range queue {
if err := ctxErr(ctx); err != nil {
p.keepUndelivered(accountID, queue[idx:])
return delivered, err
}
if err := handler(ctx, cloneOfflineDelivery(delivery)); err != nil {
p.keepUndelivered(accountID, queue[idx:])
return delivered, err
}
delivered++
}
p.keepUndelivered(accountID, nil)
return delivered, nil
}
func (p *MemoryOfflinePipeline) SnapshotOffline(accountID string) []OfflineDelivery {
accountID = strings.TrimSpace(accountID)
if accountID == "" || p == nil {
return nil
}
p.mu.Lock()
out := cloneOfflineItems(p.deliveries[accountID])
p.mu.Unlock()
return out
}
func (p *MemoryOfflinePipeline) keepUndelivered(accountID string, remaining []OfflineDelivery) {
p.mu.Lock()
defer p.mu.Unlock()
if len(remaining) == 0 {
delete(p.deliveries, accountID)
return
}
p.deliveries[accountID] = cloneOfflineItems(remaining)
}
func (p *MemoryOfflinePipeline) nowUTC() time.Time {
if p == nil || p.now == nil {
return time.Now().UTC()
}
return p.now().UTC()
}
func normOfflineDelivery(delivery OfflineDelivery, now time.Time) (OfflineDelivery, error) {
delivery.AccountID = strings.TrimSpace(delivery.AccountID)
delivery.Kind = strings.TrimSpace(delivery.Kind)
delivery.IdempotencyKey = strings.TrimSpace(delivery.IdempotencyKey)
if delivery.AccountID == "" || delivery.Kind == "" {
return OfflineDelivery{}, fmt.Errorf("offline delivery account_id and kind are required")
}
if delivery.CreatedAt.IsZero() {
delivery.CreatedAt = now
} else {
delivery.CreatedAt = delivery.CreatedAt.UTC()
}
delivery.Payload = append([]byte(nil), delivery.Payload...)
delivery.Metadata = cloneRequestMetadata(delivery.Metadata)
return delivery, nil
}
func cloneOfflineDelivery(delivery OfflineDelivery) OfflineDelivery {
delivery.Payload = append([]byte(nil), delivery.Payload...)
delivery.Metadata = cloneRequestMetadata(delivery.Metadata)
return delivery
}
func cloneOfflineItems(in []OfflineDelivery) []OfflineDelivery {
if len(in) == 0 {
return nil
}
out := make([]OfflineDelivery, len(in))
for idx, delivery := range in {
out[idx] = cloneOfflineDelivery(delivery)
}
return out
}
func ctxErr(ctx context.Context) error {
if ctx == nil {
return nil
}
return ctx.Err()
}
@@ -0,0 +1,171 @@
package profile
import (
"strings"
"sync"
"time"
)
type ValidState string
const (
ValidStateInvalid ValidState = "invalid"
ValidStateLoading ValidState = "loading"
ValidStateValid ValidState = "valid"
ValidStateClosing ValidState = "closing"
)
type OnlineState string
const (
OnlineStateOffline OnlineState = "offline"
OnlineStateConnecting OnlineState = "connecting"
OnlineStateOnline OnlineState = "online"
OnlineStateDisconnecting OnlineState = "disconnecting"
)
type AccountOnlineState struct {
AccountID string `json:"account_id"`
SessionID string `json:"session_id,omitempty"`
Valid ValidState `json:"valid"`
Online OnlineState `json:"online"`
UpdatedAt time.Time `json:"updated_at"`
}
func (s AccountOnlineState) CanHandleRequest(sessionID string) bool {
sessionID = strings.TrimSpace(sessionID)
if s.Valid != ValidStateValid || s.Online != OnlineStateOnline {
return false
}
return s.SessionID == "" || sessionID == "" || s.SessionID == sessionID
}
type OnlineStateMachine struct {
mu sync.RWMutex
states map[string]AccountOnlineState
now func() time.Time
}
func NewOnlineStateMachine() *OnlineStateMachine {
return NewOnlineStateClock(time.Now)
}
func NewOnlineStateClock(now func() time.Time) *OnlineStateMachine {
if now == nil {
now = time.Now
}
return &OnlineStateMachine{
states: make(map[string]AccountOnlineState),
now: now,
}
}
func (m *OnlineStateMachine) BeginLoad(accountID string) AccountOnlineState {
return m.set(accountID, "", ValidStateLoading, OnlineStateOffline)
}
func (m *OnlineStateMachine) MarkValid(accountID string) AccountOnlineState {
return m.update(accountID, func(state AccountOnlineState) AccountOnlineState {
state.Valid = ValidStateValid
if state.Online == "" {
state.Online = OnlineStateOffline
}
return state
})
}
func (m *OnlineStateMachine) BindSession(accountID, sessionID string) AccountOnlineState {
sessionID = strings.TrimSpace(sessionID)
return m.update(accountID, func(state AccountOnlineState) AccountOnlineState {
state.Valid = ValidStateValid
state.Online = OnlineStateOnline
state.SessionID = sessionID
return state
})
}
func (m *OnlineStateMachine) Disconnect(accountID, sessionID string) AccountOnlineState {
sessionID = strings.TrimSpace(sessionID)
return m.update(accountID, func(state AccountOnlineState) AccountOnlineState {
if sessionID != "" && state.SessionID != "" && state.SessionID != sessionID {
return state
}
state.Online = OnlineStateOffline
state.SessionID = ""
if state.Valid == "" {
state.Valid = ValidStateInvalid
}
return state
})
}
func (m *OnlineStateMachine) MarkClosing(accountID string) AccountOnlineState {
return m.update(accountID, func(state AccountOnlineState) AccountOnlineState {
state.Valid = ValidStateClosing
if state.Online == OnlineStateOnline {
state.Online = OnlineStateDisconnecting
}
return state
})
}
func (m *OnlineStateMachine) Invalidate(accountID string) AccountOnlineState {
return m.set(accountID, "", ValidStateInvalid, OnlineStateOffline)
}
func (m *OnlineStateMachine) Get(accountID string) (AccountOnlineState, bool) {
accountID = strings.TrimSpace(accountID)
if accountID == "" || m == nil {
return AccountOnlineState{}, false
}
m.mu.RLock()
defer m.mu.RUnlock()
state, ok := m.states[accountID]
return state, ok
}
func (m *OnlineStateMachine) Snapshot() []AccountOnlineState {
if m == nil {
return nil
}
m.mu.RLock()
defer m.mu.RUnlock()
out := make([]AccountOnlineState, 0, len(m.states))
for _, state := range m.states {
out = append(out, state)
}
return out
}
func (m *OnlineStateMachine) set(accountID, sessionID string, valid ValidState, online OnlineState) AccountOnlineState {
accountID = strings.TrimSpace(accountID)
if accountID == "" || m == nil {
return AccountOnlineState{}
}
state := AccountOnlineState{
AccountID: accountID,
SessionID: strings.TrimSpace(sessionID),
Valid: valid,
Online: online,
UpdatedAt: m.now().UTC(),
}
m.mu.Lock()
m.states[accountID] = state
m.mu.Unlock()
return state
}
func (m *OnlineStateMachine) update(accountID string, mutate func(AccountOnlineState) AccountOnlineState) AccountOnlineState {
accountID = strings.TrimSpace(accountID)
if accountID == "" || m == nil {
return AccountOnlineState{}
}
m.mu.Lock()
defer m.mu.Unlock()
state := m.states[accountID]
state.AccountID = accountID
state = mutate(state)
state.UpdatedAt = m.now().UTC()
m.states[accountID] = state
return state
}
+595
View File
@@ -0,0 +1,595 @@
package profile
import (
"context"
"errors"
"fmt"
"reflect"
"sort"
"strings"
"sync"
"time"
"longheng.io/server/internal/platform/db"
)
const rtRecordLockShards = 64
type SaveFieldsFunc[T any, F comparable] func(context.Context, db.Store[T], T, ...F) error
type PrepareFunc[T any, F comparable] func(T, string, time.Time, bool) (T, []F, error)
type SavePrepareFunc[T any, F comparable] func(T, time.Time) (T, []F, error)
type TouchFunc[T any, F comparable] func(T, time.Time) (T, []F, error)
type SameFunc[T any] func(current, snapshot T) bool
var ErrCloneRequired = errors.New("profile runtime clone function is required")
type Options[T any, F comparable] struct {
Store db.Store[T]
Fields db.FieldRegistry[F]
Clone func(T) T
NewRecord func(string, time.Time) T
PrepareLoaded PrepareFunc[T, F]
PrepareSaved SavePrepareFunc[T, F]
Touch TouchFunc[T, F]
SaveFields SaveFieldsFunc[T, F]
SameActive SameFunc[T]
NormalizeKey func(string) string
}
type Runtime[T any, F comparable] struct {
store db.Store[T]
fields db.FieldRegistry[F]
clone func(T) T
newRecord func(string, time.Time) T
prepareLoaded PrepareFunc[T, F]
prepareSaved SavePrepareFunc[T, F]
touch TouchFunc[T, F]
saveFields SaveFieldsFunc[T, F]
sameActive SameFunc[T]
normalizeKey func(string) string
mu sync.RWMutex
active map[string]T
dirty map[string]map[F]uint64
rev uint64
recordLocks [rtRecordLockShards]sync.Mutex
}
func NewRuntime[T any, F comparable](options Options[T, F]) (*Runtime[T, F], error) {
if options.Store == nil {
return nil, fmt.Errorf("profile runtime store is required")
}
if options.NewRecord == nil {
return nil, fmt.Errorf("profile runtime new record function is required")
}
if options.SaveFields == nil {
return nil, fmt.Errorf("profile runtime save fields function is required")
}
if options.Clone == nil && hasRefFields[T]() {
return nil, fmt.Errorf("%w: %s", ErrCloneRequired, recordTypeName[T]())
}
if options.Clone == nil {
options.Clone = db.IdentityClone[T]
}
return &Runtime[T, F]{
store: options.Store,
fields: options.Fields,
clone: options.Clone,
newRecord: options.NewRecord,
prepareLoaded: options.PrepareLoaded,
prepareSaved: options.PrepareSaved,
touch: options.Touch,
saveFields: options.SaveFields,
sameActive: options.SameActive,
normalizeKey: options.NormalizeKey,
active: make(map[string]T),
dirty: make(map[string]map[F]uint64),
}, nil
}
func hasRefFields[T any]() bool {
var zero T
t := reflect.TypeOf(zero)
if t == nil {
return false
}
return typeHasRefFields(t)
}
func typeHasRefFields(t reflect.Type) bool {
for t.Kind() == reflect.Pointer {
return true
}
switch t.Kind() {
case reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface:
return true
case reflect.Array:
return typeHasRefFields(t.Elem())
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
if directFieldHasRef(t.Field(i).Type) {
return true
}
}
}
return false
}
func directFieldHasRef(t reflect.Type) bool {
for t.Kind() == reflect.Pointer {
return true
}
switch t.Kind() {
case reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface:
return true
case reflect.Array:
return directFieldHasRef(t.Elem())
default:
return false
}
}
func recordTypeName[T any]() string {
var zero T
t := reflect.TypeOf(zero)
if t == nil {
return "<nil>"
}
return t.String()
}
func (r *Runtime[T, F]) LoadOrCreate(ctx context.Context, key string) (T, bool, error) {
if ctx == nil {
ctx = context.Background()
}
var zero T
key = r.normalizeRecordKey(key)
if key == "" {
return zero, false, fmt.Errorf("profile key is required")
}
unlock := r.lockRecord(key)
defer unlock()
return r.loadOrCreateLocked(ctx, key)
}
func (r *Runtime[T, F]) loadOrCreateLocked(ctx context.Context, key string) (T, bool, error) {
var zero T
r.mu.RLock()
if record, ok := r.active[key]; ok {
r.mu.RUnlock()
return r.clone(record), false, nil
}
r.mu.RUnlock()
now := time.Now().UTC()
record, existed, err := r.store.Load(ctx, key)
if err != nil {
return zero, false, err
}
dirty := r.fields.NewSet()
if !existed {
record = r.newRecord(key, now)
dirty.AddAll()
} else {
record = r.clone(record)
}
if r.prepareLoaded != nil {
record, err = r.applyPrepareLoaded(record, key, now, existed, dirty)
if err != nil {
return zero, false, err
}
}
r.mu.Lock()
if existing, ok := r.active[key]; ok {
r.mu.Unlock()
return r.clone(existing), false, nil
}
record = r.clone(record)
r.active[key] = record
r.markDirtyLocked(key, dirty.List()...)
r.mu.Unlock()
return r.clone(record), true, nil
}
// SaveAndUnload 保存当前在线快照,并在记录未被并发修改时卸载。
// 保存链路只操作克隆快照,避免 PrepareSaved 或底层存储污染 active 状态。
func (r *Runtime[T, F]) SaveAndUnload(ctx context.Context, key string) (T, T, bool, error) {
if ctx == nil {
ctx = context.Background()
}
var zero T
key = r.normalizeRecordKey(key)
if key == "" {
return zero, zero, false, fmt.Errorf("profile key is required")
}
now := time.Now().UTC()
r.mu.Lock()
record, ok := r.active[key]
if !ok {
r.mu.Unlock()
return zero, zero, false, nil
}
recordSnapshot := r.clone(record)
saveRecord := r.clone(recordSnapshot)
dirtyFields, dirtyRevs := r.dirtySnapshotLocked(key)
if r.prepareSaved != nil {
var fields []F
var err error
saveRecord, fields, err = r.prepareSaved(saveRecord, now)
if err != nil {
r.mu.Unlock()
return zero, zero, false, err
}
dirtyFields = r.fields.Merge(dirtyFields, fields)
}
r.mu.Unlock()
if err := r.saveFields(ctx, r.store, saveRecord, dirtyFields...); err != nil {
return zero, zero, false, err
}
summaryRecord := saveRecord
r.mu.Lock()
if current, exists := r.active[key]; exists {
r.clearDirtyLocked(key, dirtyRevs)
if r.same(current, recordSnapshot) && len(r.dirty[key]) == 0 {
delete(r.active, key)
delete(r.dirty, key)
} else {
summaryRecord = current
}
}
r.mu.Unlock()
return r.clone(saveRecord), r.clone(summaryRecord), true, nil
}
func (r *Runtime[T, F]) Upsert(key string, mutate func(T, bool, time.Time) (T, []F, error)) (T, error) {
var zero T
key = r.normalizeRecordKey(key)
if key == "" {
return zero, fmt.Errorf("profile key is required")
}
if mutate == nil {
return zero, fmt.Errorf("profile upsert mutation is required")
}
now := time.Now().UTC()
unlock := r.lockRecord(key)
defer unlock()
r.mu.RLock()
record, existed := r.active[key]
if !existed {
record = r.newRecord(key, now)
} else {
record = r.clone(record)
}
r.mu.RUnlock()
record, fields, err := mutate(record, existed, now)
if err != nil {
return zero, err
}
record = r.clone(record)
r.mu.Lock()
r.active[key] = record
if !existed {
all := r.fields.All()
fields = append(all, fields...)
}
r.markDirtyLocked(key, fields...)
r.mu.Unlock()
return r.clone(record), nil
}
func (r *Runtime[T, F]) MutateLoaded(ctx context.Context, key string, mutate func(*T, time.Time) ([]F, error)) (T, T, error) {
if ctx == nil {
ctx = context.Background()
}
var zero T
key = r.normalizeRecordKey(key)
if key == "" {
return zero, zero, fmt.Errorf("profile key is required")
}
if mutate == nil {
return zero, zero, fmt.Errorf("profile mutation is required")
}
const attempts = 2
for attempt := 0; attempt < attempts; attempt++ {
unlock := r.lockRecord(key)
if _, _, err := r.loadOrCreateLocked(ctx, key); err != nil {
unlock()
return zero, zero, err
}
now := time.Now().UTC()
r.mu.RLock()
record, ok := r.active[key]
if !ok {
r.mu.RUnlock()
unlock()
if err := ctx.Err(); err != nil {
return zero, zero, err
}
continue
}
before := r.clone(record)
record = r.clone(record)
r.mu.RUnlock()
fields, err := mutate(&record, now)
if err != nil {
unlock()
return zero, zero, err
}
if r.touch != nil {
var touchFields []F
record, touchFields, err = r.touch(record, now)
if err != nil {
unlock()
return zero, zero, err
}
fields = append(fields, touchFields...)
}
record = r.clone(record)
r.mu.Lock()
r.active[key] = record
r.markDirtyLocked(key, fields...)
r.mu.Unlock()
unlock()
return before, r.clone(record), nil
}
return zero, zero, fmt.Errorf("profile %s was unloaded during mutation", key)
}
func (r *Runtime[T, F]) MutateActive(key string, mutate func(*T, time.Time) ([]F, error)) (T, T, bool, error) {
var zero T
key = r.normalizeRecordKey(key)
if key == "" {
return zero, zero, false, fmt.Errorf("profile key is required")
}
if mutate == nil {
return zero, zero, false, fmt.Errorf("profile mutation is required")
}
now := time.Now().UTC()
unlock := r.lockRecord(key)
defer unlock()
r.mu.RLock()
record, ok := r.active[key]
if !ok {
r.mu.RUnlock()
return zero, zero, false, nil
}
before := r.clone(record)
record = r.clone(record)
r.mu.RUnlock()
fields, err := mutate(&record, now)
if err != nil {
return zero, zero, true, err
}
if r.touch != nil && len(fields) > 0 {
var touchFields []F
record, touchFields, err = r.touch(record, now)
if err != nil {
return zero, zero, true, err
}
fields = append(fields, touchFields...)
}
record = r.clone(record)
r.mu.Lock()
r.active[key] = record
r.markDirtyLocked(key, fields...)
r.mu.Unlock()
return before, r.clone(record), true, nil
}
func (r *Runtime[T, F]) LoadDetached(ctx context.Context, key string) (T, bool, error) {
if ctx == nil {
ctx = context.Background()
}
key = r.normalizeRecordKey(key)
if key == "" {
var zero T
return zero, false, fmt.Errorf("profile key is required")
}
record, ok, err := r.store.Load(ctx, key)
if err != nil || !ok {
return record, ok, err
}
return r.clone(record), true, nil
}
func (r *Runtime[T, F]) NewDetached(key string, now time.Time) T {
key = r.normalizeRecordKey(key)
if now.IsZero() {
now = time.Now().UTC()
}
return r.newRecord(key, now)
}
func (r *Runtime[T, F]) AttachIfAbsent(key string, record T, fields ...F) (T, bool) {
key = r.normalizeRecordKey(key)
if key == "" {
return r.clone(record), false
}
unlock := r.lockRecord(key)
defer unlock()
r.mu.Lock()
if current, ok := r.active[key]; ok {
r.mu.Unlock()
return r.clone(current), false
}
record = r.clone(record)
r.active[key] = record
r.markDirtyLocked(key, fields...)
r.mu.Unlock()
return r.clone(record), true
}
func (r *Runtime[T, F]) Get(key string) (T, bool) {
key = r.normalizeRecordKey(key)
r.mu.RLock()
record, ok := r.active[key]
r.mu.RUnlock()
return r.clone(record), ok
}
func (r *Runtime[T, F]) Snapshot() []T {
r.mu.RLock()
keys := make([]string, 0, len(r.active))
for key := range r.active {
keys = append(keys, key)
}
sort.Strings(keys)
records := make([]T, 0, len(keys))
for _, key := range keys {
records = append(records, r.active[key])
}
r.mu.RUnlock()
out := make([]T, 0, len(records))
for _, record := range records {
out = append(out, r.clone(record))
}
return out
}
func (r *Runtime[T, F]) ActiveKeys() []string {
r.mu.RLock()
keys := make([]string, 0, len(r.active))
for key := range r.active {
keys = append(keys, key)
}
r.mu.RUnlock()
sort.Strings(keys)
return keys
}
func (r *Runtime[T, F]) ActiveByPredicate(match func(T) bool) (T, bool) {
var zero T
if match == nil {
return zero, false
}
r.mu.RLock()
records := make([]T, 0, len(r.active))
for _, record := range r.active {
records = append(records, record)
}
r.mu.RUnlock()
for _, record := range records {
cloned := r.clone(record)
if match(cloned) {
return cloned, true
}
}
return zero, false
}
func (r *Runtime[T, F]) applyPrepareLoaded(record T, key string, now time.Time, existed bool, dirty db.FieldSet[F]) (T, error) {
record, fields, err := r.prepareLoaded(record, key, now, existed)
if err != nil {
return record, err
}
dirty.Add(fields...)
return record, nil
}
func (r *Runtime[T, F]) markDirtyLocked(key string, fields ...F) {
fields = r.fields.Normalize(fields)
if len(fields) == 0 {
return
}
revisions := r.dirty[key]
if revisions == nil {
revisions = make(map[F]uint64, len(fields))
}
for _, field := range fields {
r.rev++
revisions[field] = r.rev
}
r.dirty[key] = revisions
}
func (r *Runtime[T, F]) dirtySnapshotLocked(key string) ([]F, map[F]uint64) {
revisions := r.dirty[key]
fields := r.dirtyFieldsLocked(revisions)
if len(fields) == 0 {
return nil, nil
}
snapshot := make(map[F]uint64, len(fields))
for _, field := range fields {
snapshot[field] = revisions[field]
}
return fields, snapshot
}
func (r *Runtime[T, F]) clearDirtyLocked(key string, saved map[F]uint64) {
if len(saved) == 0 {
return
}
revisions := r.dirty[key]
for field, savedRev := range saved {
if currentRev, ok := revisions[field]; ok && currentRev == savedRev {
delete(revisions, field)
}
}
if len(revisions) == 0 {
delete(r.dirty, key)
return
}
r.dirty[key] = revisions
}
func (r *Runtime[T, F]) dirtyFieldsLocked(revisions map[F]uint64) []F {
if len(revisions) == 0 {
return nil
}
fields := make([]F, 0, len(revisions))
for _, field := range r.fields.All() {
if _, ok := revisions[field]; ok {
fields = append(fields, field)
}
}
return fields
}
func (r *Runtime[T, F]) normalizeRecordKey(key string) string {
if r.normalizeKey != nil {
return r.normalizeKey(key)
}
return strings.TrimSpace(key)
}
func (r *Runtime[T, F]) lockRecord(key string) func() {
shard := rtRecordShard(key)
r.recordLocks[shard].Lock()
return func() {
r.recordLocks[shard].Unlock()
}
}
func rtRecordShard(key string) int {
var hash uint32 = 2166136261
for i := 0; i < len(key); i++ {
hash ^= uint32(key[i])
hash *= 16777619
}
// 分片数固定为 64,索引一定落在 int 安全范围内。
return int(hash & (rtRecordLockShards - 1))
}
func (r *Runtime[T, F]) same(current, snapshot T) bool {
if r.sameActive == nil {
return false
}
return r.sameActive(current, snapshot)
}