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,391 @@
|
||||
package eventlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MemoryStoreMaxEvents 是内存事件日志默认保留的最大事件数。
|
||||
const MemoryStoreMaxEvents = 10000
|
||||
|
||||
// MemoryStore 是用于本地开发、测试和轻量部署的内存 eventlog store。
|
||||
type MemoryStore struct {
|
||||
name string
|
||||
|
||||
mu sync.Mutex
|
||||
events map[string]Event
|
||||
order []string
|
||||
idempotency map[string]string
|
||||
maxEvents int
|
||||
}
|
||||
|
||||
// NewMemoryStore 使用默认容量创建内存事件日志 store。
|
||||
func NewMemoryStore(name string) *MemoryStore {
|
||||
return NewMemoryStoreWithLimit(name, MemoryStoreMaxEvents)
|
||||
}
|
||||
|
||||
// NewMemoryStoreWithLimit 使用指定容量创建内存事件日志 store。
|
||||
func NewMemoryStoreWithLimit(name string, maxEvents int) *MemoryStore {
|
||||
if maxEvents <= 0 {
|
||||
maxEvents = MemoryStoreMaxEvents
|
||||
}
|
||||
return &MemoryStore{
|
||||
name: name,
|
||||
events: make(map[string]Event),
|
||||
idempotency: make(map[string]string),
|
||||
maxEvents: maxEvents,
|
||||
}
|
||||
}
|
||||
|
||||
// Append 写入事件,并对相同幂等键的重复写入返回既有事件。
|
||||
func (s *MemoryStore) Append(ctx context.Context, event Event) (Event, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Event{}, err
|
||||
}
|
||||
if s == nil {
|
||||
return Event{}, ErrStoreRequired
|
||||
}
|
||||
event = cloneEvent(event)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.ensureReadyLocked()
|
||||
if event.IdempotencyKey != "" {
|
||||
if id, ok := s.idempotency[event.IdempotencyKey]; ok {
|
||||
existing, exists := s.events[id]
|
||||
if !exists {
|
||||
delete(s.idempotency, event.IdempotencyKey)
|
||||
} else {
|
||||
if !sameIdempotentEvent(existing, event) {
|
||||
return Event{}, ErrIdempotencyConflict
|
||||
}
|
||||
return cloneEvent(existing), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, exists := s.events[event.ID]; exists {
|
||||
return Event{}, ErrEventExists
|
||||
}
|
||||
s.events[event.ID] = event
|
||||
s.order = append(s.order, event.ID)
|
||||
if event.IdempotencyKey != "" {
|
||||
s.idempotency[event.IdempotencyKey] = event.ID
|
||||
}
|
||||
s.evictOverflowLocked()
|
||||
return cloneEvent(event), nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) ensureReadyLocked() {
|
||||
if s.events == nil {
|
||||
s.events = make(map[string]Event)
|
||||
}
|
||||
if s.idempotency == nil {
|
||||
s.idempotency = make(map[string]string)
|
||||
}
|
||||
if s.maxEvents <= 0 {
|
||||
s.maxEvents = MemoryStoreMaxEvents
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MemoryStore) evictOverflowLocked() {
|
||||
for s.maxEvents > 0 && len(s.order) > s.maxEvents {
|
||||
id := s.order[0]
|
||||
s.order = s.order[1:]
|
||||
event, ok := s.events[id]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
delete(s.events, id)
|
||||
if event.IdempotencyKey != "" {
|
||||
delete(s.idempotency, event.IdempotencyKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get 按事件 ID 读取事件副本。
|
||||
func (s *MemoryStore) Get(ctx context.Context, id string) (Event, bool, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Event{}, false, err
|
||||
}
|
||||
if s == nil {
|
||||
return Event{}, false, ErrStoreRequired
|
||||
}
|
||||
s.mu.Lock()
|
||||
event, ok := s.events[id]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
return Event{}, false, nil
|
||||
}
|
||||
return cloneEvent(event), true, nil
|
||||
}
|
||||
|
||||
// GetByIdempotencyKey 按幂等键读取事件副本。
|
||||
func (s *MemoryStore) GetByIdempotencyKey(ctx context.Context, key string) (Event, bool, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Event{}, false, err
|
||||
}
|
||||
if s == nil {
|
||||
return Event{}, false, ErrStoreRequired
|
||||
}
|
||||
s.mu.Lock()
|
||||
id, ok := s.idempotency[key]
|
||||
if ok {
|
||||
event := cloneEvent(s.events[id])
|
||||
s.mu.Unlock()
|
||||
return event, true, nil
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return Event{}, false, nil
|
||||
}
|
||||
|
||||
// ListByStreamAggregate 按事件流和聚合 ID 倒序读取事件。
|
||||
func (s *MemoryStore) ListByStreamAggregate(ctx context.Context, stream, aggregateID string, limit int) ([]Event, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil {
|
||||
return nil, ErrStoreRequired
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = defaultPublishLimit
|
||||
}
|
||||
s.mu.Lock()
|
||||
out := make([]Event, 0, limit)
|
||||
for i := len(s.order) - 1; i >= 0 && len(out) < limit; i-- {
|
||||
event := s.events[s.order[i]]
|
||||
if event.Stream != stream || event.AggregateID != aggregateID {
|
||||
continue
|
||||
}
|
||||
out = append(out, cloneEvent(event))
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ClaimPending 领取一批到期事件用于发布。
|
||||
func (s *MemoryStore) ClaimPending(ctx context.Context, limit int, now time.Time, claimTimeout time.Duration) ([]Event, error) {
|
||||
return s.claimPending(ctx, limit, now, claimTimeout, nil)
|
||||
}
|
||||
|
||||
// ClaimPendingExcept 领取到期事件时排除指定事件流。
|
||||
func (s *MemoryStore) ClaimPendingExcept(ctx context.Context, limit int, now time.Time, claimTimeout time.Duration, excludeStreams []string) ([]Event, error) {
|
||||
excluded := make(map[string]struct{}, len(excludeStreams))
|
||||
for _, stream := range normalizeStreamList(excludeStreams) {
|
||||
excluded[stream] = struct{}{}
|
||||
}
|
||||
return s.claimPending(ctx, limit, now, claimTimeout, excluded)
|
||||
}
|
||||
|
||||
func (s *MemoryStore) claimPending(ctx context.Context, limit int, now time.Time, claimTimeout time.Duration, excluded map[string]struct{}) ([]Event, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil {
|
||||
return nil, ErrStoreRequired
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = defaultPublishLimit
|
||||
}
|
||||
now = now.UTC()
|
||||
if claimTimeout <= 0 {
|
||||
claimTimeout = defaultClaimTimeout
|
||||
}
|
||||
claimUntil := now.Add(claimTimeout)
|
||||
// 内存 store 用 AvailableAt 临时保存 claim 截止时间;worker 更新状态时必须带回同一个 deadline。
|
||||
s.mu.Lock()
|
||||
out := make([]Event, 0, limit)
|
||||
for _, id := range s.order {
|
||||
event := s.events[id]
|
||||
if event.Status != StatusPending && event.Status != StatusFailed && event.Status != StatusProcessing {
|
||||
continue
|
||||
}
|
||||
if _, skip := excluded[event.Stream]; skip {
|
||||
continue
|
||||
}
|
||||
if event.AvailableAt.After(now) {
|
||||
continue
|
||||
}
|
||||
event.Status = StatusProcessing
|
||||
event.AvailableAt = claimUntil
|
||||
event.UpdatedAt = now
|
||||
s.events[id] = event
|
||||
out = append(out, cloneEvent(event))
|
||||
if len(out) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MarkPublished 将当前 claim 持有的事件推进为已发布。
|
||||
func (s *MemoryStore) MarkPublished(ctx context.Context, id string, publishedAt time.Time, claimDeadline time.Time) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil {
|
||||
return ErrStoreRequired
|
||||
}
|
||||
s.mu.Lock()
|
||||
event, ok := s.events[id]
|
||||
if !ok {
|
||||
s.mu.Unlock()
|
||||
return ErrEventNotFound
|
||||
}
|
||||
if !ownsClaim(event, claimDeadline) {
|
||||
s.mu.Unlock()
|
||||
return ErrClaimLost
|
||||
}
|
||||
publishedAt = publishedAt.UTC()
|
||||
// Published 是终态,只有当前 claim 持有者能写入,避免过期 worker 覆盖新 worker 的结果。
|
||||
event.Status = StatusPublished
|
||||
event.PublishedAt = publishedAt
|
||||
event.UpdatedAt = publishedAt
|
||||
event.LastError = ""
|
||||
s.events[id] = event
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkFailed 将当前 claim 持有的事件推进为失败并设置下次重试时间。
|
||||
func (s *MemoryStore) MarkFailed(ctx context.Context, id string, message string, nextAttempt time.Time, updatedAt time.Time, claimDeadline time.Time) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil {
|
||||
return ErrStoreRequired
|
||||
}
|
||||
s.mu.Lock()
|
||||
event, ok := s.events[id]
|
||||
if !ok {
|
||||
s.mu.Unlock()
|
||||
return ErrEventNotFound
|
||||
}
|
||||
if !ownsClaim(event, claimDeadline) {
|
||||
s.mu.Unlock()
|
||||
return ErrClaimLost
|
||||
}
|
||||
// MarkFailed 会把 AvailableAt 改成下一次可重试时间;超过最大次数后由上层转 dead_letter。
|
||||
event.Status = StatusFailed
|
||||
event.Attempts++
|
||||
event.LastError = message
|
||||
event.AvailableAt = nextAttempt.UTC()
|
||||
event.UpdatedAt = updatedAt.UTC()
|
||||
s.events[id] = event
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkDeadLetter 将当前 claim 持有的事件推进为死信。
|
||||
func (s *MemoryStore) MarkDeadLetter(ctx context.Context, id string, message string, updatedAt time.Time, claimDeadline time.Time) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil {
|
||||
return ErrStoreRequired
|
||||
}
|
||||
s.mu.Lock()
|
||||
event, ok := s.events[id]
|
||||
if !ok {
|
||||
s.mu.Unlock()
|
||||
return ErrEventNotFound
|
||||
}
|
||||
if !ownsClaim(event, claimDeadline) {
|
||||
s.mu.Unlock()
|
||||
return ErrClaimLost
|
||||
}
|
||||
updatedAt = updatedAt.UTC()
|
||||
// DeadLetter 是人工介入边界,事件保留在 store 中,等待 requeue 或审计排查。
|
||||
event.Status = StatusDeadLetter
|
||||
event.Attempts++
|
||||
event.LastError = message
|
||||
event.AvailableAt = updatedAt
|
||||
event.UpdatedAt = updatedAt
|
||||
s.events[id] = event
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListDeadLetters 返回当前死信事件列表。
|
||||
func (s *MemoryStore) ListDeadLetters(ctx context.Context, limit int) ([]Event, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil {
|
||||
return nil, ErrStoreRequired
|
||||
}
|
||||
if limit < 0 {
|
||||
return nil, ErrInvalidLimit
|
||||
}
|
||||
if limit == 0 {
|
||||
limit = defDeadLetterLimit
|
||||
}
|
||||
s.mu.Lock()
|
||||
out := make([]Event, 0, limit)
|
||||
for _, id := range s.order {
|
||||
event := s.events[id]
|
||||
if event.Status != StatusDeadLetter {
|
||||
continue
|
||||
}
|
||||
out = append(out, cloneEvent(event))
|
||||
if len(out) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RequeueDeadLetter 把死信事件重新放回待发布队列。
|
||||
func (s *MemoryStore) RequeueDeadLetter(ctx context.Context, id string, availableAt time.Time, updatedAt time.Time, preserveAttempts bool) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil {
|
||||
return ErrStoreRequired
|
||||
}
|
||||
s.mu.Lock()
|
||||
event, ok := s.events[id]
|
||||
if !ok || event.Status != StatusDeadLetter {
|
||||
s.mu.Unlock()
|
||||
return ErrEventNotFound
|
||||
}
|
||||
event.Status = StatusPending
|
||||
event.AvailableAt = availableAt.UTC()
|
||||
event.UpdatedAt = updatedAt.UTC()
|
||||
event.PublishedAt = time.Time{}
|
||||
event.LastError = ""
|
||||
if !preserveAttempts {
|
||||
event.Attempts = 0
|
||||
}
|
||||
s.events[id] = event
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Snapshot 汇总内存 store 当前各状态数量和最早到期时间。
|
||||
func (s *MemoryStore) Snapshot(ctx context.Context) (Snapshot, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
if s == nil {
|
||||
return Snapshot{}, ErrStoreRequired
|
||||
}
|
||||
s.mu.Lock()
|
||||
snapshot := Snapshot{
|
||||
Name: s.name,
|
||||
Total: len(s.events),
|
||||
ByStatus: make(map[string]int),
|
||||
}
|
||||
for _, event := range s.events {
|
||||
snapshot.ByStatus[event.Status]++
|
||||
if isDueStatus(event.Status) && (snapshot.OldestDue.IsZero() || event.AvailableAt.Before(snapshot.OldestDue)) {
|
||||
snapshot.OldestDue = event.AvailableAt
|
||||
}
|
||||
}
|
||||
if len(snapshot.ByStatus) == 0 {
|
||||
snapshot.ByStatus = nil
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return snapshot, nil
|
||||
}
|
||||
Reference in new issue
Block a user