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

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

382 lines
9.1 KiB
Go

package storageobject
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"sort"
"strings"
"sync"
"time"
)
var (
ErrInvalidKey = errors.New("storage object key is invalid")
ErrInvalidObject = errors.New("storage object is invalid")
ErrObjectNotFound = errors.New("storage object not found")
ErrVersionConflict = errors.New("storage object version conflict")
)
// Storage Object 只保存非权威轻量对象,权威玩家资产仍应走 Profile/MySQL。
const (
PermissionNoAccess = 0
PermissionOwner = 1
PermissionPublic = 2
)
const (
OwnerKindUser = "user"
OwnerKindSystem = "system"
)
type Key struct {
Collection string `json:"collection"`
Key string `json:"key"`
UserID string `json:"user_id,omitempty"`
}
func (k Key) OwnerKind() string {
if strings.TrimSpace(k.UserID) == "" {
return OwnerKindSystem
}
return OwnerKindUser
}
type Object struct {
Key Key `json:"key"`
Value json.RawMessage `json:"value"`
Index map[string]string `json:"index,omitempty"`
Version string `json:"version"`
PermissionRead int `json:"permission_read,omitempty"`
PermissionWrite int `json:"permission_write,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (o Object) OwnerKind() string {
return o.Key.OwnerKind()
}
type WriteRequest struct {
Object Object `json:"object"`
IfMatchVersion string `json:"if_match_version,omitempty"`
IfNoneMatch bool `json:"if_none_match,omitempty"`
}
type DeleteRequest struct {
Key Key `json:"key"`
IfMatchVersion string `json:"if_match_version,omitempty"`
}
type BatchReadRequest struct {
Keys []Key `json:"keys"`
}
type BatchWriteRequest struct {
Writes []WriteRequest `json:"writes"`
}
type BatchDeleteRequest struct {
Deletes []DeleteRequest `json:"deletes"`
}
type BatchObjectResult struct {
Object Object `json:"object,omitempty"`
Found bool `json:"found,omitempty"`
Error string `json:"error,omitempty"`
}
type BatchDeleteResult struct {
Key Key `json:"key"`
Deleted bool `json:"deleted,omitempty"`
Error string `json:"error,omitempty"`
}
type BatchReadResult struct {
Results []BatchObjectResult `json:"results"`
}
type BatchWriteResult struct {
Results []BatchObjectResult `json:"results"`
}
type BatchDeleteResults struct {
Results []BatchDeleteResult `json:"results"`
}
type ListRequest struct {
Collection string `json:"collection"`
UserID string `json:"user_id,omitempty"`
Index map[string]string `json:"index,omitempty"`
Limit int `json:"limit,omitempty"`
Cursor string `json:"cursor,omitempty"`
}
type ListResult struct {
Objects []Object `json:"objects"`
NextCursor string `json:"next_cursor,omitempty"`
}
type Store interface {
Write(context.Context, WriteRequest) (Object, error)
Read(context.Context, Key) (Object, bool, error)
Delete(context.Context, Key, string) error
List(context.Context, ListRequest) (ListResult, error)
}
type MemoryStore struct {
mu sync.Mutex
now func() time.Time
objects map[string]Object
}
func NewMemoryStore(now func() time.Time) *MemoryStore {
if now == nil {
now = time.Now
}
return &MemoryStore{now: now, objects: make(map[string]Object)}
}
func (s *MemoryStore) Write(ctx context.Context, request WriteRequest) (Object, error) {
if err := ctxErr(ctx); err != nil {
return Object{}, err
}
if s == nil {
return Object{}, ErrObjectNotFound
}
object, err := normalizeObject(request.Object, s.now().UTC())
if err != nil {
return Object{}, err
}
id := objectID(object.Key)
s.mu.Lock()
defer s.mu.Unlock()
if s.objects == nil {
s.objects = make(map[string]Object)
}
current, exists := s.objects[id]
if request.IfNoneMatch && exists {
return Object{}, ErrVersionConflict
}
if request.IfMatchVersion != "" {
if !exists || current.Version != strings.TrimSpace(request.IfMatchVersion) {
return Object{}, ErrVersionConflict
}
}
if exists {
object.CreatedAt = current.CreatedAt
}
object.Version = versionFor(object)
s.objects[id] = cloneObject(object)
return cloneObject(object), nil
}
func (s *MemoryStore) Read(ctx context.Context, key Key) (Object, bool, error) {
if err := ctxErr(ctx); err != nil {
return Object{}, false, err
}
key, err := normalizeKey(key)
if err != nil {
return Object{}, false, err
}
if s == nil {
return Object{}, false, nil
}
s.mu.Lock()
object, ok := s.objects[objectID(key)]
s.mu.Unlock()
return cloneObject(object), ok, nil
}
func (s *MemoryStore) Delete(ctx context.Context, key Key, ifMatchVersion string) error {
if err := ctxErr(ctx); err != nil {
return err
}
key, err := normalizeKey(key)
if err != nil {
return err
}
if s == nil {
return ErrObjectNotFound
}
s.mu.Lock()
defer s.mu.Unlock()
id := objectID(key)
current, ok := s.objects[id]
if !ok {
return ErrObjectNotFound
}
if ifMatchVersion != "" && current.Version != strings.TrimSpace(ifMatchVersion) {
return ErrVersionConflict
}
delete(s.objects, id)
return nil
}
func (s *MemoryStore) List(ctx context.Context, request ListRequest) (ListResult, error) {
if err := ctxErr(ctx); err != nil {
return ListResult{}, err
}
collection := strings.TrimSpace(request.Collection)
if collection == "" {
return ListResult{}, ErrInvalidKey
}
userID := strings.TrimSpace(request.UserID)
indexFilters := normalizeIndex(request.Index)
limit := normalizeListLimit(request.Limit)
cursor := strings.TrimSpace(request.Cursor)
if s == nil {
return ListResult{}, nil
}
s.mu.Lock()
objects := make([]Object, 0, len(s.objects))
for _, object := range s.objects {
if object.Key.Collection != collection {
continue
}
if userID != "" && object.Key.UserID != userID {
continue
}
if !matchIndex(object, indexFilters) {
continue
}
objects = append(objects, cloneObject(object))
}
s.mu.Unlock()
sort.Slice(objects, func(i, j int) bool { return objectID(objects[i].Key) < objectID(objects[j].Key) })
start := 0
if cursor != "" {
for idx, object := range objects {
if objectID(object.Key) > cursor {
start = idx
break
}
start = idx + 1
}
}
end := start + limit
if end > len(objects) {
end = len(objects)
}
result := ListResult{Objects: objects[start:end]}
if end < len(objects) {
result.NextCursor = objectID(objects[end-1].Key)
}
return result, nil
}
func normalizeObject(object Object, now time.Time) (Object, error) {
key, err := normalizeKey(object.Key)
if err != nil {
return Object{}, err
}
if len(object.Value) == 0 || !json.Valid(object.Value) {
return Object{}, ErrInvalidObject
}
object.Key = key
object.Value = append(json.RawMessage(nil), object.Value...)
object.Index = normalizeIndex(object.Index)
object.PermissionRead = normalizePermission(object.PermissionRead)
object.PermissionWrite = normalizePermission(object.PermissionWrite)
if object.CreatedAt.IsZero() {
object.CreatedAt = now
} else {
object.CreatedAt = object.CreatedAt.UTC()
}
object.UpdatedAt = now
return object, nil
}
func normalizeListLimit(limit int) int {
if limit <= 0 || limit > 100 {
return 100
}
return limit
}
func normalizeKey(key Key) (Key, error) {
key.Collection = strings.TrimSpace(key.Collection)
key.Key = strings.TrimSpace(key.Key)
key.UserID = strings.TrimSpace(key.UserID)
if key.Collection == "" || key.Key == "" || !validKeyPart(key.Collection) || !validKeyPart(key.Key) || !validKeyPart(key.UserID) {
return Key{}, ErrInvalidKey
}
return key, nil
}
func validKeyPart(value string) bool {
return !strings.ContainsAny(value, "/\x00")
}
func normalizePermission(value int) int {
switch value {
case PermissionNoAccess, PermissionOwner, PermissionPublic:
return value
default:
return PermissionOwner
}
}
func normalizeIndex(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 {
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" || value == "" {
continue
}
out[key] = value
}
if len(out) == 0 {
return nil
}
return out
}
func matchIndex(object Object, filters map[string]string) bool {
filters = normalizeIndex(filters)
if len(filters) == 0 {
return true
}
for key, value := range filters {
if object.Index[key] != value {
return false
}
}
return true
}
func versionFor(object Object) string {
indexJSON, _ := json.Marshal(normalizeIndex(object.Index))
sum := sha256.Sum256([]byte(strings.Join([]string{
objectID(object.Key),
string(object.Value),
string(indexJSON),
object.UpdatedAt.Format(time.RFC3339Nano),
}, "\x1f")))
return hex.EncodeToString(sum[:12])
}
func objectID(key Key) string {
key, _ = normalizeKey(key)
return key.Collection + "/" + key.UserID + "/" + key.Key
}
func cloneObject(object Object) Object {
object.Value = append(json.RawMessage(nil), object.Value...)
object.Index = normalizeIndex(object.Index)
return object
}
func ctxErr(ctx context.Context) error {
if ctx == nil {
return nil
}
return ctx.Err()
}