DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,318 @@
|
||||
package moderation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidSanction = errors.New("moderation sanction is invalid")
|
||||
ErrSanctionMissing = errors.New("moderation sanction not found")
|
||||
)
|
||||
|
||||
type SanctionKind string
|
||||
|
||||
const (
|
||||
SanctionMute SanctionKind = "mute"
|
||||
SanctionBan SanctionKind = "ban"
|
||||
)
|
||||
|
||||
type Sanction struct {
|
||||
ID string `json:"id"`
|
||||
Subject string `json:"subject"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Kind SanctionKind `json:"kind"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Until time.Time `json:"until,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type SanctionQuery struct {
|
||||
Subject string `json:"subject"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Kinds []SanctionKind `json:"kinds,omitempty"`
|
||||
Now time.Time `json:"now,omitempty"`
|
||||
}
|
||||
|
||||
type SanctionSnapshot struct {
|
||||
Items []Sanction `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type SanctionStore interface {
|
||||
Upsert(context.Context, Sanction) (Sanction, error)
|
||||
Remove(context.Context, string) error
|
||||
Active(context.Context, SanctionQuery) ([]Sanction, error)
|
||||
Snapshot(context.Context) (SanctionSnapshot, error)
|
||||
}
|
||||
|
||||
type MemorySanctionStore struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]Sanction
|
||||
bySubject map[string]map[string]struct{}
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewMemorySanctionStore() *MemorySanctionStore {
|
||||
return &MemorySanctionStore{
|
||||
items: make(map[string]Sanction),
|
||||
bySubject: make(map[string]map[string]struct{}),
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MemorySanctionStore) SetNow(now func() time.Time) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.now = now
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *MemorySanctionStore) Upsert(ctx context.Context, sanction Sanction) (Sanction, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Sanction{}, err
|
||||
}
|
||||
if s == nil {
|
||||
return Sanction{}, ErrSanctionMissing
|
||||
}
|
||||
item, err := normalizeSanction(sanction, s.nowUTC())
|
||||
if err != nil {
|
||||
return Sanction{}, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.items == nil {
|
||||
s.items = make(map[string]Sanction)
|
||||
}
|
||||
if s.bySubject == nil {
|
||||
s.bySubject = make(map[string]map[string]struct{})
|
||||
}
|
||||
if existing, ok := s.items[item.ID]; ok && existing.Subject != item.Subject {
|
||||
s.removeSubjectIdx(existing.Subject, item.ID)
|
||||
}
|
||||
s.items[item.ID] = cloneSanction(item)
|
||||
s.addSubjectIndex(item.Subject, item.ID)
|
||||
s.mu.Unlock()
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *MemorySanctionStore) Remove(ctx context.Context, id string) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil {
|
||||
return ErrSanctionMissing
|
||||
}
|
||||
id = normalizeToken(id)
|
||||
if id == "" {
|
||||
return fmt.Errorf("%w: id is required", ErrInvalidSanction)
|
||||
}
|
||||
s.mu.Lock()
|
||||
if _, ok := s.items[id]; !ok {
|
||||
s.mu.Unlock()
|
||||
return ErrSanctionMissing
|
||||
}
|
||||
s.removeSubjectIdx(s.items[id].Subject, id)
|
||||
delete(s.items, id)
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemorySanctionStore) Active(ctx context.Context, query SanctionQuery) ([]Sanction, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
query.Subject = normalizeToken(query.Subject)
|
||||
query.Scope = normalizeToken(query.Scope)
|
||||
if query.Now.IsZero() {
|
||||
query.Now = s.nowUTC()
|
||||
} else {
|
||||
query.Now = query.Now.UTC()
|
||||
}
|
||||
kindSet := sanctionKindSet(query.Kinds)
|
||||
|
||||
s.mu.RLock()
|
||||
out := make([]Sanction, 0)
|
||||
ids := make([]string, 0, len(s.bySubject[query.Subject]))
|
||||
for id := range s.bySubject[query.Subject] {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Strings(ids)
|
||||
for _, id := range ids {
|
||||
item, ok := s.items[id]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !sanctionScopeApplies(item.Scope, query.Scope) {
|
||||
continue
|
||||
}
|
||||
if len(kindSet) > 0 {
|
||||
if _, ok := kindSet[item.Kind]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !item.ActiveAt(query.Now) {
|
||||
continue
|
||||
}
|
||||
out = append(out, cloneSanction(item))
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
sortSanctions(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MemorySanctionStore) Snapshot(ctx context.Context) (SanctionSnapshot, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return SanctionSnapshot{}, err
|
||||
}
|
||||
if s == nil {
|
||||
return SanctionSnapshot{}, nil
|
||||
}
|
||||
s.mu.RLock()
|
||||
items := make([]Sanction, 0, len(s.items))
|
||||
for _, item := range s.items {
|
||||
items = append(items, cloneSanction(item))
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
sortSanctions(items)
|
||||
return SanctionSnapshot{Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *MemorySanctionStore) addSubjectIndex(subject, id string) {
|
||||
if s.bySubject[subject] == nil {
|
||||
s.bySubject[subject] = make(map[string]struct{})
|
||||
}
|
||||
s.bySubject[subject][id] = struct{}{}
|
||||
}
|
||||
|
||||
func (s *MemorySanctionStore) removeSubjectIdx(subject, id string) {
|
||||
if ids := s.bySubject[subject]; ids != nil {
|
||||
delete(ids, id)
|
||||
if len(ids) == 0 {
|
||||
delete(s.bySubject, subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s Sanction) ActiveAt(now time.Time) bool {
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
} else {
|
||||
now = now.UTC()
|
||||
}
|
||||
return s.Until.IsZero() || s.Until.After(now)
|
||||
}
|
||||
|
||||
func normalizeSanction(sanction Sanction, now time.Time) (Sanction, error) {
|
||||
sanction.ID = normalizeToken(sanction.ID)
|
||||
sanction.Subject = normalizeToken(sanction.Subject)
|
||||
sanction.Scope = normalizeToken(sanction.Scope)
|
||||
sanction.Reason = strings.TrimSpace(sanction.Reason)
|
||||
sanction.Source = strings.TrimSpace(sanction.Source)
|
||||
sanction.Meta = cloneStringMap(sanction.Meta)
|
||||
if sanction.Subject == "" {
|
||||
return Sanction{}, fmt.Errorf("%w: subject is required", ErrInvalidSanction)
|
||||
}
|
||||
if sanction.Kind != SanctionMute && sanction.Kind != SanctionBan {
|
||||
return Sanction{}, fmt.Errorf("%w: invalid kind %q", ErrInvalidSanction, sanction.Kind)
|
||||
}
|
||||
if sanction.ID == "" {
|
||||
if hasDefaultIDDelim(sanction.Subject) || hasDefaultIDDelim(sanction.Scope) {
|
||||
return Sanction{}, fmt.Errorf("%w: subject or scope contains reserved delimiter", ErrInvalidSanction)
|
||||
}
|
||||
sanction.ID = sanction.Subject + ":" + string(sanction.Kind) + ":" + sanction.Scope
|
||||
}
|
||||
if sanction.CreatedAt.IsZero() {
|
||||
sanction.CreatedAt = now.UTC()
|
||||
} else {
|
||||
sanction.CreatedAt = sanction.CreatedAt.UTC()
|
||||
}
|
||||
if !sanction.Until.IsZero() {
|
||||
sanction.Until = sanction.Until.UTC()
|
||||
}
|
||||
return sanction, nil
|
||||
}
|
||||
|
||||
func sanctionKindSet(kinds []SanctionKind) map[SanctionKind]struct{} {
|
||||
if len(kinds) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[SanctionKind]struct{}, len(kinds))
|
||||
for _, kind := range kinds {
|
||||
if kind == "" {
|
||||
continue
|
||||
}
|
||||
out[kind] = struct{}{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sanctionScopeApplies(sanctionScope, requestScope string) bool {
|
||||
sanctionScope = normalizeToken(sanctionScope)
|
||||
requestScope = normalizeToken(requestScope)
|
||||
return sanctionScope == "" || sanctionScope == "global" || sanctionScope == requestScope
|
||||
}
|
||||
|
||||
// hasDefaultIDDelim 拦截会让默认处罚 ID 产生歧义的保留分隔符。
|
||||
func hasDefaultIDDelim(value string) bool {
|
||||
return strings.Contains(value, ":") || strings.ContainsRune(value, '\x00')
|
||||
}
|
||||
|
||||
// encodeModerationKey 用长度前缀拼接多段键,避免字段内容自带分隔符时发生碰撞。
|
||||
func encodeModerationKey(parts ...string) string {
|
||||
var builder strings.Builder
|
||||
for _, part := range parts {
|
||||
builder.WriteString(strconv.Itoa(len(part)))
|
||||
builder.WriteByte(':')
|
||||
builder.WriteString(part)
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func sortSanctions(items []Sanction) {
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].Subject != items[j].Subject {
|
||||
return items[i].Subject < items[j].Subject
|
||||
}
|
||||
if items[i].Scope != items[j].Scope {
|
||||
return items[i].Scope < items[j].Scope
|
||||
}
|
||||
if items[i].Kind != items[j].Kind {
|
||||
return items[i].Kind < items[j].Kind
|
||||
}
|
||||
return items[i].ID < items[j].ID
|
||||
})
|
||||
}
|
||||
|
||||
func cloneSanction(sanction Sanction) Sanction {
|
||||
sanction.Meta = cloneStringMap(sanction.Meta)
|
||||
return sanction
|
||||
}
|
||||
|
||||
func (s *MemorySanctionStore) nowUTC() time.Time {
|
||||
s.mu.RLock()
|
||||
now := s.now
|
||||
s.mu.RUnlock()
|
||||
if now == nil {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
return now().UTC()
|
||||
}
|
||||
|
||||
func ctxErr(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
Reference in new issue
Block a user