Files
90CNServer/server/internal/platform/hotupdate/eventlog_control.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

516 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package hotupdate
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"sync"
"time"
"longheng.io/server/internal/platform/eventlog"
)
const (
EventLogIntentStream = "hotupdate.intent"
EventLogProgressStream = "hotupdate.progress"
)
var (
ErrControlLogRequired = errors.New("hot update eventlog control requires eventlog")
ErrControlConflict = errors.New("hot update control idempotency key conflicts with existing intent")
ErrInvalidControlEvent = errors.New("hot update control event payload is invalid")
ErrControlPollInterval = errors.New("hot update control poll interval is invalid")
ErrControlInstanceID = errors.New("hot update control instance id generation failed")
defControlPoll = 2 * time.Second
)
var controlRandRead = rand.Read
type EventLogControlOptions struct {
Log *eventlog.Log
PollInterval time.Duration
Now func() time.Time
}
type EventLogControl struct {
log *eventlog.Log
pollInterval time.Duration
now func() time.Time
ops sync.RWMutex
mu sync.Mutex
intents map[string]Intent
progress map[string][]Progress
watchers map[string]map[chan Intent]struct{}
done chan struct{}
closed bool
instanceID string
progressSequence uint64
}
type intentEnvelope struct {
Target string `json:"target"`
Intent Intent `json:"intent"`
}
type progressEnvelope struct {
Progress Progress `json:"progress"`
}
func NewEventLogControl(options EventLogControlOptions) (*EventLogControl, error) {
if options.Log == nil {
return nil, ErrControlLogRequired
}
pollInterval := options.PollInterval
if pollInterval == 0 {
pollInterval = defControlPoll
}
if pollInterval < 0 {
return nil, ErrControlPollInterval
}
now := options.Now
if now == nil {
now = func() time.Time { return time.Now().UTC() }
}
instanceID, err := newControlInstanceID()
if err != nil {
return nil, err
}
return &EventLogControl{
log: options.Log,
pollInterval: pollInterval,
now: now,
intents: make(map[string]Intent),
progress: make(map[string][]Progress),
watchers: make(map[string]map[chan Intent]struct{}),
done: make(chan struct{}),
instanceID: instanceID,
}, nil
}
func (c *EventLogControl) Publish(ctx context.Context, target string, intent Intent) error {
if err := contextErr(ctx); err != nil {
return err
}
now, err := c.ready()
if err != nil {
return err
}
target, err = normalizeTarget(target)
if err != nil {
return err
}
intent, err = normalizeIntent(intent)
if err != nil {
return err
}
release, err := c.beginWrite()
if err != nil {
return err
}
defer release()
// EventLog 控制面把 intent 持久化为事件,再通知 watcher;watcher 丢通知时可通过轮询补回最新 intent。
key := intentIdempotencyKey(target, intent)
existing, ok, err := c.log.GetByIdempotencyKey(ctx, key)
if err != nil {
return err
}
if ok {
existingIntent, err := decodeIntentEvent(existing)
if err != nil {
return err
}
if !reflect.DeepEqual(existingIntent, intent) {
return fmt.Errorf("%w: target=%s version=%s", ErrControlConflict, target, intent.Version)
}
c.storeIntentAndNotify(ctx, target, existingIntent)
return nil
}
payload, err := json.Marshal(intentEnvelope{Target: target, Intent: intent})
if err != nil {
return err
}
if _, err := c.log.Append(ctx, eventlog.Event{
Stream: EventLogIntentStream,
Type: string(intent.Action),
AggregateID: target,
IdempotencyKey: key,
Payload: payload,
Status: eventlog.StatusPublished,
PublishedAt: now().UTC(),
Headers: map[string]string{
"hotupdate_target": target,
"hotupdate_action": string(intent.Action),
"hotupdate_version": intent.Version,
},
}); err != nil {
if errors.Is(err, eventlog.ErrIdempotencyConflict) {
return fmt.Errorf("%w: target=%s version=%s", ErrControlConflict, target, intent.Version)
}
return err
}
c.storeIntentAndNotify(ctx, target, intent)
return nil
}
func (c *EventLogControl) Watch(ctx context.Context, target string) (<-chan Intent, error) {
if err := contextErr(ctx); err != nil {
return nil, err
}
if ctx == nil {
ctx = context.Background()
}
if _, err := c.ready(); err != nil {
return nil, err
}
target, err := normalizeTarget(target)
if err != nil {
return nil, err
}
// Watch 先注册 channel,再加载最新 intent,避免注册窗口内发布的新 intent 被漏掉。
ch := make(chan Intent, 1)
c.mu.Lock()
if c.closed {
c.mu.Unlock()
close(ch)
return ch, nil
}
if c.watchers[target] == nil {
c.watchers[target] = make(map[chan Intent]struct{})
}
c.watchers[target][ch] = struct{}{}
if intent, ok := c.intents[target]; ok {
ch <- intent
}
c.mu.Unlock()
go c.watchLoop(ctx, target, ch)
return ch, nil
}
func (c *EventLogControl) Report(ctx context.Context, progress Progress) error {
if err := contextErr(ctx); err != nil {
return err
}
now, err := c.ready()
if err != nil {
return err
}
progress, err = normalizeProgress(progress, now)
if err != nil {
return err
}
release, err := c.beginWrite()
if err != nil {
return err
}
defer release()
payload, err := json.Marshal(progressEnvelope{Progress: progress})
if err != nil {
return err
}
instanceID, sequence := c.nextProgressKeyParts()
if _, err := c.log.Append(ctx, eventlog.Event{
Stream: EventLogProgressStream,
Type: string(progress.Stage),
AggregateID: progress.Target,
IdempotencyKey: controlProgressKey(progress, instanceID, sequence),
Payload: payload,
Status: eventlog.StatusPublished,
PublishedAt: now().UTC(),
Headers: map[string]string{
"hotupdate_target": progress.Target,
"hotupdate_node": progress.NodeID,
"hotupdate_version": progress.Version,
"hotupdate_stage": string(progress.Stage),
},
}); err != nil {
return err
}
c.mu.Lock()
if !c.closed {
c.progress[progress.Target] = append(c.progress[progress.Target], progress)
}
c.mu.Unlock()
return nil
}
func (c *EventLogControl) beginWrite() (func(), error) {
c.ops.RLock()
c.mu.Lock()
if c.closed {
c.mu.Unlock()
c.ops.RUnlock()
return nil, ErrControlClosed
}
c.mu.Unlock()
return c.ops.RUnlock, nil
}
func (c *EventLogControl) ready() (func() time.Time, error) {
if c == nil || c.log == nil {
return nil, ErrControlLogRequired
}
c.mu.Lock()
defer c.mu.Unlock()
if c.pollInterval < 0 {
return nil, ErrControlPollInterval
}
if c.pollInterval == 0 {
c.pollInterval = defControlPoll
}
if c.now == nil {
c.now = func() time.Time { return time.Now().UTC() }
}
if c.intents == nil {
c.intents = make(map[string]Intent)
}
if c.progress == nil {
c.progress = make(map[string][]Progress)
}
if c.watchers == nil {
c.watchers = make(map[string]map[chan Intent]struct{})
}
if c.done == nil {
c.done = make(chan struct{})
}
if c.instanceID == "" {
instanceID, err := newControlInstanceID()
if err != nil {
return nil, err
}
c.instanceID = instanceID
}
return c.now, nil
}
func (c *EventLogControl) Snapshot() ControlSnapshot {
if c == nil {
return ControlSnapshot{}
}
c.mu.Lock()
intents := make(map[string]Intent, len(c.intents))
for target, intent := range c.intents {
intents[target] = intent
}
progress := make(map[string][]Progress, len(c.progress))
for target, items := range c.progress {
progress[target] = append([]Progress(nil), items...)
}
watchers := make(map[string]int, len(c.watchers))
for target, list := range c.watchers {
watchers[target] = len(list)
}
snapshot := ControlSnapshot{Intents: intents, Progress: progress, Watchers: watchers, Closed: c.closed}
c.mu.Unlock()
if len(snapshot.Intents) == 0 {
snapshot.Intents = nil
}
if len(snapshot.Progress) == 0 {
snapshot.Progress = nil
}
if len(snapshot.Watchers) == 0 {
snapshot.Watchers = nil
}
return snapshot
}
func (c *EventLogControl) Close() error {
if c == nil {
return nil
}
c.ops.Lock()
defer c.ops.Unlock()
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return nil
}
c.closed = true
// Close 关闭所有 watcher channel;EventLog 里的 intent/progress 仍保留用于审计。
if c.done == nil {
c.done = make(chan struct{})
}
close(c.done)
for target, watchers := range c.watchers {
for ch := range watchers {
close(ch)
}
delete(c.watchers, target)
}
c.mu.Unlock()
return nil
}
func (c *EventLogControl) watchLoop(ctx context.Context, target string, ch chan Intent) {
defer c.removeWatcher(target, ch)
// watchLoop 定期从 EventLog 读取最新 intent,作为内存通知丢失后的补偿。
if c.isDone() {
return
}
_ = c.reloadLatestIntent(ctx, target)
if c.pollInterval <= 0 {
select {
case <-ctx.Done():
case <-c.done:
}
return
}
ticker := time.NewTicker(c.pollInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-c.done:
return
case <-ticker.C:
_ = c.reloadLatestIntent(ctx, target)
}
}
}
func (c *EventLogControl) isDone() bool {
select {
case <-c.done:
return true
default:
return false
}
}
func (c *EventLogControl) removeWatcher(target string, ch chan Intent) {
c.mu.Lock()
if watchers := c.watchers[target]; watchers != nil {
if _, ok := watchers[ch]; ok {
delete(watchers, ch)
close(ch)
}
if len(watchers) == 0 {
delete(c.watchers, target)
}
}
c.mu.Unlock()
}
func (c *EventLogControl) reloadLatestIntent(ctx context.Context, target string) error {
events, err := c.log.ListByStreamAggregate(ctx, EventLogIntentStream, target, 1)
if err != nil {
return err
}
if len(events) == 0 {
return nil
}
intent, err := decodeIntentEvent(events[0])
if err != nil {
return err
}
c.storeIntentAndNotify(ctx, target, intent)
return nil
}
func (c *EventLogControl) storeIntentAndNotify(_ context.Context, target string, intent Intent) {
c.mu.Lock()
// 内存 latest 是读优化,权威记录仍是 EventLog;通知失败不会删除持久化 intent。
if c.closed {
c.mu.Unlock()
return
}
if existing, ok := c.intents[target]; ok && reflect.DeepEqual(existing, intent) {
c.mu.Unlock()
return
}
c.intents[target] = intent
for ch := range c.watchers[target] {
sendLatestIntent(ch, intent)
}
c.mu.Unlock()
}
func decodeIntentEvent(event eventlog.Event) (Intent, error) {
var envelope intentEnvelope
if len(event.Payload) == 0 {
return Intent{}, ErrInvalidControlEvent
}
if err := json.Unmarshal(event.Payload, &envelope); err != nil {
return Intent{}, fmt.Errorf("%w: %w", ErrInvalidControlEvent, err)
}
intent, err := normalizeIntent(envelope.Intent)
if err != nil {
return Intent{}, err
}
return intent, nil
}
func intentIdempotencyKey(target string, intent Intent) string {
if intent.Sequence > 0 {
return fmt.Sprintf("hotupdate:intent:v1:%s:%d", encodeEventKeyPart(target), intent.Sequence)
}
return fmt.Sprintf("hotupdate:intent:v1:%s:%s:%s", encodeEventKeyPart(target), encodeEventKeyPart(string(intent.Action)), encodeEventKeyPart(intent.Version))
}
func progressIdemKey(progress Progress) string {
parts := []string{
"hotupdate:progress",
"v1",
encodeEventKeyPart(progress.Target),
encodeEventKeyPart(progress.NodeID),
encodeEventKeyPart(progress.Version),
encodeEventKeyPart(string(progress.Stage)),
progressFingerprint(progress),
}
return strings.Join(parts, ":")
}
func controlProgressKey(progress Progress, instanceID string, sequence uint64) string {
parts := []string{
progressIdemKey(progress),
encodeEventKeyPart(instanceID),
fmt.Sprint(sequence),
}
return strings.Join(parts, ":")
}
func (c *EventLogControl) nextProgressKeyParts() (string, uint64) {
c.mu.Lock()
defer c.mu.Unlock()
c.progressSequence++
return c.instanceID, c.progressSequence
}
func newControlInstanceID() (string, error) {
var raw [8]byte
if _, err := controlRandRead(raw[:]); err != nil {
return "", fmt.Errorf("%w: %w", ErrControlInstanceID, err)
}
return hex.EncodeToString(raw[:]), nil
}
func encodeEventKeyPart(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "_"
}
return base64.RawURLEncoding.EncodeToString([]byte(value))
}
func progressFingerprint(progress Progress) string {
payload := strings.Join([]string{
strings.TrimSpace(progress.Target),
strings.TrimSpace(progress.NodeID),
strings.TrimSpace(progress.Version),
strings.TrimSpace(string(progress.Stage)),
strings.TrimSpace(progress.Status),
strings.TrimSpace(progress.Detail),
progress.UpdatedAt.UTC().Format(time.RFC3339Nano),
}, "\x00")
sum := sha256.Sum256([]byte(payload))
return hex.EncodeToString(sum[:8])
}