服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
428 lines
11 KiB
Go
428 lines
11 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ErrObjectCodecReq 表示对象字段编解码器缺失或不合法。
|
|
var ErrObjectCodecReq = errors.New("object field codec is required")
|
|
|
|
// ErrHashObjectStoreReq 表示 hash 对象存储本身缺失。
|
|
var ErrHashObjectStoreReq = errors.New("hash object store is required")
|
|
|
|
// ErrObjectRecordFactoryReq 表示对象记录构造函数缺失。
|
|
var ErrObjectRecordFactoryReq = errors.New("hash object new record function is required")
|
|
|
|
// ObjectFieldCodec 描述对象字段和 hash 字段之间的编解码规则。
|
|
type ObjectFieldCodec[T any, F comparable] struct {
|
|
Field F
|
|
Name string
|
|
Marshal func(T) ([]byte, error)
|
|
Apply func(*T, []byte) error
|
|
}
|
|
|
|
// HashObjectStoreOptions 配置 hash 后端对象存储。
|
|
type HashObjectStoreOptions[T any, F comparable] struct {
|
|
Backend HashBackend
|
|
Fields FieldRegistry[F]
|
|
Codecs []ObjectFieldCodec[T, F]
|
|
Key KeyFunc[T]
|
|
Clone CloneFunc[T]
|
|
NewRecord func(string) T
|
|
NormalizeKey func(string) string
|
|
TTL time.Duration
|
|
}
|
|
|
|
// HashObjectStore 把对象字段拆分保存到 HashBackend。
|
|
type HashObjectStore[T any, F comparable] struct {
|
|
backend HashBackend
|
|
fields FieldRegistry[F]
|
|
codecs map[F]objectFieldCodec[T]
|
|
keyFn KeyFunc[T]
|
|
cloneFn CloneFunc[T]
|
|
newRecord func(string) T
|
|
normalizeKey func(string) string
|
|
ttl time.Duration
|
|
}
|
|
|
|
type objectFieldCodec[T any] struct {
|
|
name string
|
|
marshal func(T) ([]byte, error)
|
|
apply func(*T, []byte) error
|
|
}
|
|
|
|
// NewHashObjectStore 创建 hash 对象存储。
|
|
func NewHashObjectStore[T any, F comparable](options HashObjectStoreOptions[T, F]) (*HashObjectStore[T, F], error) {
|
|
if options.Backend == nil {
|
|
return nil, ErrHashBackendRequired
|
|
}
|
|
if options.Key == nil {
|
|
return nil, fmt.Errorf("%w: key function is nil", ErrRecordKeyRequired)
|
|
}
|
|
if options.NewRecord == nil {
|
|
return nil, ErrObjectRecordFactoryReq
|
|
}
|
|
if options.Clone == nil {
|
|
options.Clone = IdentityClone[T]
|
|
}
|
|
fields := options.Fields
|
|
if len(fields.All()) == 0 && len(options.Codecs) > 0 {
|
|
order := make([]F, 0, len(options.Codecs))
|
|
for _, codec := range options.Codecs {
|
|
order = append(order, codec.Field)
|
|
}
|
|
fields = NewFieldRegistry(order)
|
|
}
|
|
codecs, err := normObjCodecs(fields, options.Codecs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &HashObjectStore[T, F]{
|
|
backend: options.Backend,
|
|
fields: fields,
|
|
codecs: codecs,
|
|
keyFn: options.Key,
|
|
cloneFn: options.Clone,
|
|
newRecord: options.NewRecord,
|
|
normalizeKey: options.NormalizeKey,
|
|
ttl: options.TTL,
|
|
}, nil
|
|
}
|
|
|
|
// Load 从 hash 后端读取并组装对象。
|
|
func (s *HashObjectStore[T, F]) Load(ctx context.Context, key string) (T, bool, error) {
|
|
var zero T
|
|
if err := s.ensureLoadReady(); err != nil {
|
|
return zero, false, err
|
|
}
|
|
ctx = contextOrBackground(ctx)
|
|
key = normalizeLookupKey(key, s.normalizeKey)
|
|
if key == "" {
|
|
return zero, false, ErrRecordKeyRequired
|
|
}
|
|
raw, found, err := s.backend.LoadHash(ctx, key, s.fieldNames(s.fields.All()))
|
|
if err != nil || !found {
|
|
return zero, found, err
|
|
}
|
|
record, err := s.decodeRecord(key, raw)
|
|
if err != nil {
|
|
return zero, false, err
|
|
}
|
|
return s.cloneFn(record), true, nil
|
|
}
|
|
|
|
// LoadBatch 批量读取并组装对象。
|
|
func (s *HashObjectStore[T, F]) LoadBatch(ctx context.Context, keys []string) (map[string]T, error) {
|
|
if err := s.ensureLoadReady(); err != nil {
|
|
return nil, err
|
|
}
|
|
ctx = contextOrBackground(ctx)
|
|
keys = normalizeBatchKeys(keys, s.normalizeKey)
|
|
if len(keys) == 0 {
|
|
return map[string]T{}, ctx.Err()
|
|
}
|
|
if batcher, ok := s.backend.(HashBatchLoaderBackend); ok {
|
|
fieldNames := s.fieldNames(s.fields.All())
|
|
requests := make([]HashLoadRequest, 0, len(keys))
|
|
for _, key := range keys {
|
|
requests = append(requests, HashLoadRequest{
|
|
Key: key,
|
|
Fields: fieldNames,
|
|
})
|
|
}
|
|
results, err := batcher.LoadHashBatch(ctx, requests)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make(map[string]T, len(results))
|
|
for _, key := range keys {
|
|
result := results[key]
|
|
if !result.Found {
|
|
continue
|
|
}
|
|
record, err := s.decodeRecord(key, result.Fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out[key] = s.cloneFn(record)
|
|
}
|
|
return out, nil
|
|
}
|
|
out := make(map[string]T, len(keys))
|
|
for _, key := range keys {
|
|
record, ok, err := s.Load(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ok {
|
|
out[key] = record
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Save 保存对象的全部字段。
|
|
func (s *HashObjectStore[T, F]) Save(ctx context.Context, record T) error {
|
|
if err := s.ensureSaveReady(); err != nil {
|
|
return err
|
|
}
|
|
fields := s.fields.All()
|
|
if len(fields) == 0 {
|
|
return ErrAllFieldsRequired
|
|
}
|
|
return s.SaveFields(ctx, record, fields...)
|
|
}
|
|
|
|
// SaveFields 保存对象的指定字段。
|
|
func (s *HashObjectStore[T, F]) SaveFields(ctx context.Context, record T, fields ...F) error {
|
|
if err := s.ensureSaveReady(); err != nil {
|
|
return err
|
|
}
|
|
ctx = contextOrBackground(ctx)
|
|
fields = s.fields.Normalize(fields)
|
|
if len(fields) == 0 {
|
|
return nil
|
|
}
|
|
key, err := s.recordKey(record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hashFields, err := s.encodeFields(record, fields)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.backend.SaveHash(ctx, key, hashFields, s.ttl)
|
|
}
|
|
|
|
// SaveFieldBatch 批量保存对象字段。
|
|
func (s *HashObjectStore[T, F]) SaveFieldBatch(ctx context.Context, saves []FieldSave[T, F]) error {
|
|
if err := s.ensureSaveReady(); err != nil {
|
|
return err
|
|
}
|
|
ctx = contextOrBackground(ctx)
|
|
batches := make([]HashSaveBatch, 0, len(saves))
|
|
for _, save := range normalizeFieldSaves(s.fields.Normalize, saves) {
|
|
key, err := s.recordKey(save.Record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fields, err := s.encodeFields(save.Record, save.Fields)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(fields) == 0 {
|
|
continue
|
|
}
|
|
batches = append(batches, HashSaveBatch{Key: key, Fields: fields, TTL: s.ttl})
|
|
}
|
|
var err error
|
|
batches, err = mergeHashSaveBatches(batches)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(batches) == 0 {
|
|
return nil
|
|
}
|
|
if batcher, ok := s.backend.(HashBatchBackend); ok {
|
|
return batcher.SaveHashBatch(ctx, batches)
|
|
}
|
|
for _, batch := range batches {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.backend.SaveHash(ctx, batch.Key, batch.Fields, batch.TTL); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Check 检查 hash 后端可用性。
|
|
func (s *HashObjectStore[T, F]) Check(ctx context.Context) error {
|
|
if err := s.ensureBaseReady(); err != nil {
|
|
return err
|
|
}
|
|
return Check(ctx, s.backend)
|
|
}
|
|
|
|
// Fields 返回对象存储支持的字段列表。
|
|
func (s *HashObjectStore[T, F]) Fields() []F {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
return s.fields.All()
|
|
}
|
|
|
|
// FieldNames 返回字段对应的 hash 字段名。
|
|
func (s *HashObjectStore[T, F]) FieldNames(fields ...F) []string {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
if len(fields) == 0 {
|
|
return s.fieldNames(s.fields.All())
|
|
}
|
|
return s.fieldNames(s.fields.Normalize(fields))
|
|
}
|
|
|
|
func (s *HashObjectStore[T, F]) recordKey(record T) (string, error) {
|
|
key, err := RecordKey(s.keyFn, record)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
key = normalizeLookupKey(key, s.normalizeKey)
|
|
if key == "" {
|
|
return "", ErrRecordKeyRequired
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
func (s *HashObjectStore[T, F]) ensureBaseReady() error {
|
|
if s == nil {
|
|
return ErrHashObjectStoreReq
|
|
}
|
|
if s.backend == nil {
|
|
return ErrHashBackendRequired
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *HashObjectStore[T, F]) ensureLoadReady() error {
|
|
if err := s.ensureBaseReady(); err != nil {
|
|
return err
|
|
}
|
|
if s.newRecord == nil {
|
|
return ErrObjectRecordFactoryReq
|
|
}
|
|
s.ensureClone()
|
|
fields := s.fields.All()
|
|
if len(fields) == 0 {
|
|
return ErrAllFieldsRequired
|
|
}
|
|
return s.ensureCodecs(fields, false, true)
|
|
}
|
|
|
|
func (s *HashObjectStore[T, F]) ensureSaveReady() error {
|
|
if err := s.ensureBaseReady(); err != nil {
|
|
return err
|
|
}
|
|
if s.keyFn == nil {
|
|
return fmt.Errorf("%w: key function is nil", ErrRecordKeyRequired)
|
|
}
|
|
s.ensureClone()
|
|
fields := s.fields.All()
|
|
if len(fields) == 0 {
|
|
return ErrAllFieldsRequired
|
|
}
|
|
return s.ensureCodecs(fields, true, false)
|
|
}
|
|
|
|
func (s *HashObjectStore[T, F]) ensureClone() {
|
|
if s.cloneFn == nil {
|
|
s.cloneFn = IdentityClone[T]
|
|
}
|
|
}
|
|
|
|
func (s *HashObjectStore[T, F]) ensureCodecs(fields []F, requireMarshal bool, requireApply bool) error {
|
|
for _, field := range fields {
|
|
codec, ok := s.codecs[field]
|
|
if !ok || strings.TrimSpace(codec.name) == "" {
|
|
return fmt.Errorf("%w: %v", ErrObjectCodecReq, field)
|
|
}
|
|
if requireMarshal && codec.marshal == nil {
|
|
return fmt.Errorf("%w: %v", ErrObjectCodecReq, field)
|
|
}
|
|
if requireApply && codec.apply == nil {
|
|
return fmt.Errorf("%w: %v", ErrObjectCodecReq, field)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *HashObjectStore[T, F]) encodeFields(record T, fields []F) (map[string][]byte, error) {
|
|
out := make(map[string][]byte, len(fields))
|
|
for _, field := range fields {
|
|
codec, ok := s.codecs[field]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %v", ErrObjectCodecReq, field)
|
|
}
|
|
data, err := codec.marshal(s.cloneFn(record))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal object field %q: %w", codec.name, err)
|
|
}
|
|
if data == nil {
|
|
return nil, fmt.Errorf("marshal object field %q returned nil", codec.name)
|
|
}
|
|
out[codec.name] = append([]byte(nil), data...)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *HashObjectStore[T, F]) decodeRecord(key string, raw map[string][]byte) (T, error) {
|
|
record := s.newRecord(key)
|
|
for _, field := range s.fields.All() {
|
|
codec := s.codecs[field]
|
|
data, ok := raw[codec.name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if err := codec.apply(&record, append([]byte(nil), data...)); err != nil {
|
|
return record, fmt.Errorf("apply object field %q: %w", codec.name, err)
|
|
}
|
|
}
|
|
return record, nil
|
|
}
|
|
|
|
func (s *HashObjectStore[T, F]) fieldNames(fields []F) []string {
|
|
if len(fields) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(fields))
|
|
for _, field := range fields {
|
|
if codec, ok := s.codecs[field]; ok {
|
|
out = append(out, codec.name)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func normObjCodecs[T any, F comparable](fields FieldRegistry[F], codecs []ObjectFieldCodec[T, F]) (map[F]objectFieldCodec[T], error) {
|
|
if len(fields.All()) == 0 {
|
|
return nil, ErrAllFieldsRequired
|
|
}
|
|
out := make(map[F]objectFieldCodec[T], len(codecs))
|
|
names := make(map[string]F, len(codecs))
|
|
seenFields := make(map[F]struct{}, len(codecs))
|
|
for _, codec := range codecs {
|
|
if !fields.IsKnown(codec.Field) {
|
|
return nil, fmt.Errorf("%w: unknown field %v", ErrObjectCodecReq, codec.Field)
|
|
}
|
|
if _, exists := seenFields[codec.Field]; exists {
|
|
return nil, fmt.Errorf("duplicate object field codec: %v", codec.Field)
|
|
}
|
|
seenFields[codec.Field] = struct{}{}
|
|
name := strings.TrimSpace(codec.Name)
|
|
if name == "" || codec.Marshal == nil || codec.Apply == nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrObjectCodecReq, codec.Field)
|
|
}
|
|
if existing, exists := names[name]; exists && existing != codec.Field {
|
|
return nil, fmt.Errorf("duplicate object field name %q", name)
|
|
}
|
|
names[name] = codec.Field
|
|
out[codec.Field] = objectFieldCodec[T]{
|
|
name: name,
|
|
marshal: codec.Marshal,
|
|
apply: codec.Apply,
|
|
}
|
|
}
|
|
for _, field := range fields.All() {
|
|
if _, ok := out[field]; !ok {
|
|
return nil, fmt.Errorf("%w: %v", ErrObjectCodecReq, field)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|