DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,295 @@
package admincmdqueue
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"longheng.io/server/internal/platform/admincmd"
"longheng.io/server/internal/platform/eventlog"
)
var (
ErrEventLogRequired = errors.New("admin command eventlog is required")
ErrIdempotencyConflict = errors.New("admin command idempotency key conflicts with an existing command")
ErrInvalidCommandEvent = errors.New("admin command event payload is invalid")
ErrCommandHandlerNeeded = errors.New("admin command handler is required")
)
const (
DefaultStream = "admin.command"
ReceiptStatusAccepted = "accepted"
ReceiptStatusProcessing = "processing"
ReceiptStatusSucceeded = "succeeded"
ReceiptStatusFailed = "failed"
ReceiptStatusDeadLetter = "dead_letter"
)
type ExecutorOptions struct {
Log *eventlog.Log
Stream string
Now func() time.Time
}
type Executor struct {
log *eventlog.Log
stream string
now func() time.Time
}
type SubmitResult struct {
Receipt admincmd.Receipt `json:"receipt"`
Event eventlog.Event `json:"event"`
Duplicate bool `json:"duplicate"`
}
type CommandEnvelope struct {
Command admincmd.Command `json:"command"`
Receipt admincmd.Receipt `json:"receipt"`
CommandHash string `json:"command_hash"`
}
type Handler interface {
ExecuteAdminCommand(context.Context, admincmd.Command) error
}
type HandlerFunc func(context.Context, admincmd.Command) error
func (fn HandlerFunc) ExecuteAdminCommand(ctx context.Context, command admincmd.Command) error {
if fn == nil {
return ErrCommandHandlerNeeded
}
return fn(ctx, command)
}
func NewExecutor(options ExecutorOptions) (*Executor, error) {
if options.Log == nil {
return nil, ErrEventLogRequired
}
stream := strings.TrimSpace(options.Stream)
if stream == "" {
stream = DefaultStream
}
now := options.Now
if now == nil {
now = time.Now
}
return &Executor{log: options.Log, stream: stream, now: now}, nil
}
func (e *Executor) Submit(ctx context.Context, command admincmd.Command, policy admincmd.Policy) (SubmitResult, error) {
if e == nil || e.log == nil {
return SubmitResult{}, ErrEventLogRequired
}
command = admincmd.Normalize(command)
if err := admincmd.Validate(command, policy); err != nil {
return SubmitResult{}, err
}
receipt, err := admincmd.NewReceipt(command, ReceiptStatusAccepted, e.now())
if err != nil {
return SubmitResult{}, err
}
commandHash, err := CommandHash(command)
if err != nil {
return SubmitResult{}, err
}
envelope := CommandEnvelope{
Command: command,
Receipt: receipt,
CommandHash: commandHash,
}
payload, err := json.Marshal(envelope)
if err != nil {
return SubmitResult{}, err
}
idempotencyKey := EventIdempotencyKey(command)
if idempotencyKey != "" {
existing, ok, err := e.log.GetByIdempotencyKey(ctx, idempotencyKey)
if err != nil {
return SubmitResult{}, err
}
if ok {
if err := ensureSameCommand(existing, envelope); err != nil {
return SubmitResult{}, err
}
return SubmitResult{Receipt: receiptForEvent(existing), Event: existing, Duplicate: true}, nil
}
}
event, err := e.log.Append(ctx, eventlog.Event{
ID: receipt.ID,
Stream: e.stream,
Type: "accepted",
AggregateID: command.Target,
IdempotencyKey: idempotencyKey,
Payload: payload,
Headers: map[string]string{
"admin_operation": command.Operation,
"admin_scope": command.Scope,
"admin_actor": command.Actor,
"admin_target": command.Target,
"admin_receipt": receipt.ID,
},
})
if err != nil {
return SubmitResult{}, err
}
if err := ensureSameCommand(event, envelope); err != nil {
return SubmitResult{}, err
}
duplicate := event.ID != receipt.ID
receipt = receiptForEvent(event)
return SubmitResult{Receipt: receipt, Event: event, Duplicate: duplicate}, nil
}
func (e *Executor) Receipt(ctx context.Context, id string) (admincmd.Receipt, bool, error) {
if e == nil || e.log == nil {
return admincmd.Receipt{}, false, ErrEventLogRequired
}
event, ok, err := e.log.Get(ctx, id)
if err != nil || !ok {
return admincmd.Receipt{}, ok, err
}
return receiptForEvent(event), true, nil
}
func (e *Executor) ReceiptByIdempotencyKey(ctx context.Context, key string) (admincmd.Receipt, bool, error) {
if e == nil || e.log == nil {
return admincmd.Receipt{}, false, ErrEventLogRequired
}
key = strings.TrimSpace(key)
if key == "" {
return admincmd.Receipt{}, false, nil
}
event, ok, err := e.log.GetByIdempotencyKey(ctx, "admincmd:"+key)
if err != nil || !ok {
return admincmd.Receipt{}, ok, err
}
return receiptForEvent(event), true, nil
}
func (e *Executor) PublishPending(ctx context.Context, handler Handler, options eventlog.PublishOptions) (eventlog.PublishStats, error) {
if e == nil || e.log == nil {
return eventlog.PublishStats{}, ErrEventLogRequired
}
if handler == nil {
return eventlog.PublishStats{}, ErrCommandHandlerNeeded
}
return e.log.PublishPending(ctx, commandPublisher{handler: handler}, options)
}
func (e *Executor) MarkSucceeded(ctx context.Context, receiptID string) (admincmd.Receipt, error) {
if e == nil || e.log == nil {
return admincmd.Receipt{}, ErrEventLogRequired
}
event, err := e.log.MarkPublished(ctx, receiptID)
if err != nil {
return admincmd.Receipt{}, err
}
return receiptForEvent(event), nil
}
func (e *Executor) MarkFailed(ctx context.Context, receiptID string, message string, nextAttempt time.Time) (admincmd.Receipt, error) {
if e == nil || e.log == nil {
return admincmd.Receipt{}, ErrEventLogRequired
}
event, err := e.log.MarkFailed(ctx, receiptID, message, nextAttempt)
if err != nil {
return admincmd.Receipt{}, err
}
return receiptForEvent(event), nil
}
func EventIdempotencyKey(command admincmd.Command) string {
key := strings.TrimSpace(command.IdempotencyKey)
if key == "" {
return ""
}
return "admincmd:" + key
}
func CommandHash(command admincmd.Command) (string, error) {
command = admincmd.Normalize(command)
payload, err := json.Marshal(command)
if err != nil {
return "", err
}
sum := sha256.Sum256(payload)
return hex.EncodeToString(sum[:]), nil
}
type commandPublisher struct {
handler Handler
}
func (p commandPublisher) Publish(ctx context.Context, event eventlog.Event) error {
if p.handler == nil {
return ErrCommandHandlerNeeded
}
envelope, err := decodeEnvelope(event)
if err != nil {
return err
}
return p.handler.ExecuteAdminCommand(ctx, envelope.Command)
}
func ensureSameCommand(event eventlog.Event, want CommandEnvelope) error {
got, err := decodeEnvelope(event)
if err != nil {
return err
}
if got.CommandHash != "" && want.CommandHash != "" && got.CommandHash != want.CommandHash {
return fmt.Errorf("%w: receipt=%s", ErrIdempotencyConflict, got.Receipt.ID)
}
if got.Receipt.ID != "" && want.Receipt.ID != "" && got.Receipt.ID != want.Receipt.ID {
return fmt.Errorf("%w: receipt=%s", ErrIdempotencyConflict, got.Receipt.ID)
}
return nil
}
func decodeEnvelope(event eventlog.Event) (CommandEnvelope, error) {
var envelope CommandEnvelope
if len(event.Payload) == 0 {
return envelope, ErrInvalidCommandEvent
}
if err := json.Unmarshal(event.Payload, &envelope); err != nil {
return envelope, fmt.Errorf("%w: %w", ErrInvalidCommandEvent, err)
}
envelope.Command = admincmd.Normalize(envelope.Command)
if envelope.Receipt.ID == "" || envelope.Command.Operation == "" {
return envelope, ErrInvalidCommandEvent
}
return envelope, nil
}
func receiptForEvent(event eventlog.Event) admincmd.Receipt {
envelope, err := decodeEnvelope(event)
if err != nil {
return admincmd.Receipt{ID: event.ID, Status: ReceiptStatusFailed, CreatedAt: event.CreatedAt}
}
receipt := envelope.Receipt
receipt.Status = receiptStatusEvent(event.Status)
return receipt
}
func receiptStatusEvent(status string) string {
switch status {
case eventlog.StatusPending:
return ReceiptStatusAccepted
case eventlog.StatusProcessing:
return ReceiptStatusProcessing
case eventlog.StatusPublished:
return ReceiptStatusSucceeded
case eventlog.StatusFailed:
return ReceiptStatusFailed
case eventlog.StatusDeadLetter:
return ReceiptStatusDeadLetter
default:
return strings.TrimSpace(status)
}
}