服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
225 lines
5.6 KiB
Go
225 lines
5.6 KiB
Go
package storageobject
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
var ErrPermissionDenied = errors.New("storage object permission denied")
|
|
|
|
type Subject struct {
|
|
UserID string `json:"user_id,omitempty"`
|
|
Admin bool `json:"admin,omitempty"`
|
|
System bool `json:"system,omitempty"`
|
|
}
|
|
|
|
type Service struct {
|
|
store Store
|
|
}
|
|
|
|
func NewService(store Store) *Service {
|
|
return &Service{store: store}
|
|
}
|
|
|
|
func (s *Service) Close() error {
|
|
if s == nil || s.store == nil {
|
|
return nil
|
|
}
|
|
if closer, ok := s.store.(interface{ Close() error }); ok {
|
|
return closer.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) Write(ctx context.Context, subject Subject, request WriteRequest) (Object, error) {
|
|
if s == nil || s.store == nil {
|
|
return Object{}, ErrObjectNotFound
|
|
}
|
|
key, err := normalizeKey(request.Object.Key)
|
|
if err != nil {
|
|
return Object{}, err
|
|
}
|
|
current, ok, err := s.store.Read(ctx, key)
|
|
if err != nil {
|
|
return Object{}, err
|
|
}
|
|
guard := request.Object
|
|
if ok {
|
|
guard = current
|
|
}
|
|
if err := AuthorizeWrite(subject, guard); err != nil {
|
|
return Object{}, err
|
|
}
|
|
return s.store.Write(ctx, request)
|
|
}
|
|
|
|
func (s *Service) BatchWrite(ctx context.Context, subject Subject, request BatchWriteRequest) BatchWriteResult {
|
|
result := BatchWriteResult{Results: make([]BatchObjectResult, 0, len(request.Writes))}
|
|
for _, write := range request.Writes {
|
|
object, err := s.Write(ctx, subject, write)
|
|
item := BatchObjectResult{Object: object, Found: err == nil}
|
|
if err != nil {
|
|
item.Error = err.Error()
|
|
}
|
|
result.Results = append(result.Results, item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *Service) Read(ctx context.Context, subject Subject, key Key) (Object, bool, error) {
|
|
if s == nil || s.store == nil {
|
|
return Object{}, false, nil
|
|
}
|
|
object, ok, err := s.store.Read(ctx, key)
|
|
if err != nil || !ok {
|
|
return object, ok, err
|
|
}
|
|
if err := AuthorizeRead(subject, object); err != nil {
|
|
return Object{}, false, err
|
|
}
|
|
return object, true, nil
|
|
}
|
|
|
|
func (s *Service) BatchRead(ctx context.Context, subject Subject, request BatchReadRequest) BatchReadResult {
|
|
result := BatchReadResult{Results: make([]BatchObjectResult, 0, len(request.Keys))}
|
|
for _, key := range request.Keys {
|
|
object, ok, err := s.Read(ctx, subject, key)
|
|
item := BatchObjectResult{Object: object, Found: ok}
|
|
if err != nil {
|
|
item.Error = err.Error()
|
|
}
|
|
result.Results = append(result.Results, item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *Service) Delete(ctx context.Context, subject Subject, key Key, ifMatchVersion string) error {
|
|
if s == nil || s.store == nil {
|
|
return ErrObjectNotFound
|
|
}
|
|
object, ok, err := s.store.Read(ctx, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return ErrObjectNotFound
|
|
}
|
|
if err := AuthorizeWrite(subject, object); err != nil {
|
|
return err
|
|
}
|
|
return s.store.Delete(ctx, key, ifMatchVersion)
|
|
}
|
|
|
|
func (s *Service) BatchDelete(ctx context.Context, subject Subject, request BatchDeleteRequest) BatchDeleteResults {
|
|
result := BatchDeleteResults{Results: make([]BatchDeleteResult, 0, len(request.Deletes))}
|
|
for _, deletion := range request.Deletes {
|
|
err := s.Delete(ctx, subject, deletion.Key, deletion.IfMatchVersion)
|
|
item := BatchDeleteResult{Key: deletion.Key, Deleted: err == nil}
|
|
if err != nil {
|
|
item.Error = err.Error()
|
|
}
|
|
result.Results = append(result.Results, item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *Service) List(ctx context.Context, subject Subject, request ListRequest) (ListResult, error) {
|
|
if s == nil || s.store == nil {
|
|
return ListResult{}, nil
|
|
}
|
|
if normalizeSubject(subject).Admin {
|
|
result, err := s.store.List(ctx, request)
|
|
if err != nil {
|
|
return ListResult{}, err
|
|
}
|
|
return result, nil
|
|
}
|
|
limit := normalizeListLimit(request.Limit)
|
|
result := ListResult{Objects: make([]Object, 0, limit)}
|
|
scan := request
|
|
scan.Limit = limit
|
|
cursor := strings.TrimSpace(request.Cursor)
|
|
for {
|
|
scan.Cursor = cursor
|
|
page, err := s.store.List(ctx, scan)
|
|
if err != nil {
|
|
return ListResult{}, err
|
|
}
|
|
for _, object := range page.Objects {
|
|
if !CanRead(subject, object) {
|
|
continue
|
|
}
|
|
result.Objects = append(result.Objects, object)
|
|
if len(result.Objects) >= limit {
|
|
result.NextCursor = objectID(object.Key)
|
|
return result, nil
|
|
}
|
|
}
|
|
if strings.TrimSpace(page.NextCursor) == "" {
|
|
return result, nil
|
|
}
|
|
cursor = page.NextCursor
|
|
}
|
|
}
|
|
|
|
func AuthorizeRead(subject Subject, object Object) error {
|
|
if CanRead(subject, object) {
|
|
return nil
|
|
}
|
|
return ErrPermissionDenied
|
|
}
|
|
|
|
func AuthorizeWrite(subject Subject, object Object) error {
|
|
if CanWrite(subject, object) {
|
|
return nil
|
|
}
|
|
return ErrPermissionDenied
|
|
}
|
|
|
|
func CanRead(subject Subject, object Object) bool {
|
|
subject = normalizeSubject(subject)
|
|
if subject.Admin {
|
|
return true
|
|
}
|
|
switch normalizePermission(object.PermissionRead) {
|
|
case PermissionPublic:
|
|
return true
|
|
case PermissionOwner:
|
|
if subject.System {
|
|
return object.OwnerKind() == OwnerKindSystem
|
|
}
|
|
return ownsObject(subject, object)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func CanWrite(subject Subject, object Object) bool {
|
|
subject = normalizeSubject(subject)
|
|
if subject.Admin {
|
|
return true
|
|
}
|
|
switch normalizePermission(object.PermissionWrite) {
|
|
case PermissionPublic:
|
|
return subject.UserID != "" || subject.System
|
|
case PermissionOwner:
|
|
if subject.System {
|
|
return object.OwnerKind() == OwnerKindSystem
|
|
}
|
|
return ownsObject(subject, object)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func ownsObject(subject Subject, object Object) bool {
|
|
owner := strings.TrimSpace(object.Key.UserID)
|
|
return subject.UserID != "" && owner != "" && subject.UserID == owner
|
|
}
|
|
|
|
func normalizeSubject(subject Subject) Subject {
|
|
subject.UserID = strings.TrimSpace(subject.UserID)
|
|
return subject
|
|
}
|