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,428 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"longheng.io/server/pkg/contracts"
|
||||
)
|
||||
|
||||
type NATS struct {
|
||||
logger *slog.Logger
|
||||
prefix string
|
||||
wireEncoding string
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
conn *nats.Conn
|
||||
metrics *Metrics
|
||||
}
|
||||
|
||||
type NATSOptions struct {
|
||||
Name string
|
||||
Timeout time.Duration
|
||||
PingInterval time.Duration
|
||||
MaxPingsOutstanding int
|
||||
MaxReconnects int
|
||||
ReconnectWait time.Duration
|
||||
RetryOnFailedConnect bool
|
||||
WireEncoding string
|
||||
Token string
|
||||
Username string
|
||||
Password string
|
||||
Credentials string
|
||||
TLSEnabled bool
|
||||
TLSServerName string
|
||||
CAFile string
|
||||
CertFile string
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
func DefaultNATSOptions() NATSOptions {
|
||||
return NATSOptions{
|
||||
Name: "longheng-bus",
|
||||
Timeout: 5 * time.Second,
|
||||
PingInterval: time.Second,
|
||||
MaxPingsOutstanding: 10,
|
||||
MaxReconnects: -1,
|
||||
ReconnectWait: time.Second,
|
||||
RetryOnFailedConnect: true,
|
||||
WireEncoding: NATSWireBinary,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeNATSOptions(options NATSOptions) NATSOptions {
|
||||
defaults := DefaultNATSOptions()
|
||||
if strings.TrimSpace(options.Name) == "" {
|
||||
options.Name = defaults.Name
|
||||
}
|
||||
if options.Timeout <= 0 {
|
||||
options.Timeout = defaults.Timeout
|
||||
}
|
||||
if options.PingInterval <= 0 {
|
||||
options.PingInterval = defaults.PingInterval
|
||||
}
|
||||
if options.MaxPingsOutstanding <= 0 {
|
||||
options.MaxPingsOutstanding = defaults.MaxPingsOutstanding
|
||||
}
|
||||
if options.MaxReconnects == 0 {
|
||||
options.MaxReconnects = defaults.MaxReconnects
|
||||
}
|
||||
if options.ReconnectWait <= 0 {
|
||||
options.ReconnectWait = defaults.ReconnectWait
|
||||
}
|
||||
if !options.RetryOnFailedConnect {
|
||||
options.RetryOnFailedConnect = defaults.RetryOnFailedConnect
|
||||
}
|
||||
if !isNATSWireEncoding(options.WireEncoding) {
|
||||
options.WireEncoding = defaults.WireEncoding
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// SetMetrics 在构造后挂接 Metrics 发射器,可与 Publish/Subscribe 并发调用。
|
||||
func (b *NATS) SetMetrics(m *Metrics) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.metrics = m
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func NewNATS(endpoints []string, namespace string, logger *slog.Logger) (*NATS, error) {
|
||||
return NewNATSWithOptions(endpoints, namespace, logger, DefaultNATSOptions())
|
||||
}
|
||||
|
||||
func NewNATSWithOptions(endpoints []string, namespace string, logger *slog.Logger, options NATSOptions) (*NATS, error) {
|
||||
endpoints = normNATSEndpoints(endpoints)
|
||||
if len(endpoints) == 0 {
|
||||
return nil, fmt.Errorf("nats bus requires at least one endpoint")
|
||||
}
|
||||
options = normalizeNATSOptions(options)
|
||||
|
||||
connectOptions := []nats.Option{
|
||||
nats.Name(options.Name),
|
||||
nats.Timeout(options.Timeout),
|
||||
nats.PingInterval(options.PingInterval),
|
||||
nats.MaxPingsOutstanding(options.MaxPingsOutstanding),
|
||||
nats.RetryOnFailedConnect(options.RetryOnFailedConnect),
|
||||
nats.MaxReconnects(options.MaxReconnects),
|
||||
nats.ReconnectWait(options.ReconnectWait),
|
||||
}
|
||||
connectOptions = appendNATSSecOpts(connectOptions, options)
|
||||
conn, err := nats.Connect(
|
||||
strings.Join(endpoints, ","),
|
||||
connectOptions...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &NATS{
|
||||
logger: logger,
|
||||
prefix: strings.Trim(namespace, "."),
|
||||
wireEncoding: options.WireEncoding,
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normNATSEndpoints(endpoints []string) []string {
|
||||
normalized := make([]string, 0, len(endpoints))
|
||||
for _, endpoint := range endpoints {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if endpoint != "" {
|
||||
normalized = append(normalized, endpoint)
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func appendNATSSecOpts(connectOptions []nats.Option, options NATSOptions) []nats.Option {
|
||||
if options.Token != "" {
|
||||
connectOptions = append(connectOptions, nats.Token(options.Token))
|
||||
}
|
||||
if options.Username != "" || options.Password != "" {
|
||||
connectOptions = append(connectOptions, nats.UserInfo(options.Username, options.Password))
|
||||
}
|
||||
if options.Credentials != "" {
|
||||
connectOptions = append(connectOptions, nats.UserCredentials(options.Credentials))
|
||||
}
|
||||
if options.TLSEnabled {
|
||||
connectOptions = append(connectOptions, nats.Secure(&tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ServerName: options.TLSServerName,
|
||||
}))
|
||||
}
|
||||
if options.CAFile != "" {
|
||||
connectOptions = append(connectOptions, nats.RootCAs(options.CAFile))
|
||||
}
|
||||
if options.CertFile != "" || options.KeyFile != "" {
|
||||
connectOptions = append(connectOptions, nats.ClientCert(options.CertFile, options.KeyFile))
|
||||
}
|
||||
return connectOptions
|
||||
}
|
||||
|
||||
func (b *NATS) Publish(ctx context.Context, topic string, payload any) error {
|
||||
return b.publishEnvelope(ctx, Envelope{Topic: topic, Payload: payload, PublishedAt: time.Now().UTC()})
|
||||
}
|
||||
|
||||
func (b *NATS) publishEnvelope(ctx context.Context, env Envelope) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
if b != nil {
|
||||
b.mu.Lock()
|
||||
metricsRef := b.metrics
|
||||
b.mu.Unlock()
|
||||
metricsRef.recordPublish(classifyHandlerError(err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
if b == nil {
|
||||
return ErrClosed
|
||||
}
|
||||
|
||||
// NATS core publish 只提供在线投递,不保存消息、不等待消费者确认;关键状态变更必须走 EventLog/Outbox。
|
||||
b.mu.Lock()
|
||||
if b.closed {
|
||||
metricsRef := b.metrics
|
||||
b.mu.Unlock()
|
||||
metricsRef.recordPublish(ResultClosed)
|
||||
return ErrClosed
|
||||
}
|
||||
conn := b.conn
|
||||
metricsRef := b.metrics
|
||||
b.mu.Unlock()
|
||||
if conn == nil {
|
||||
metricsRef.recordPublish(ResultClosed)
|
||||
return ErrClosed
|
||||
}
|
||||
|
||||
body, err := contracts.MarshalTopicPayload(env.Payload)
|
||||
if err != nil {
|
||||
metricsRef.recordPublish(ResultError)
|
||||
return err
|
||||
}
|
||||
if env.PublishedAt.IsZero() {
|
||||
env.PublishedAt = time.Now().UTC()
|
||||
}
|
||||
env = envelopeWithTrace(ctx, env)
|
||||
|
||||
wire, err := encodeNATSWire(natsWireMessage{
|
||||
Topic: env.Topic,
|
||||
Payload: body,
|
||||
PublishedAt: env.PublishedAt,
|
||||
Reply: env.Reply,
|
||||
RequestID: env.RequestID,
|
||||
Trace: env.Trace,
|
||||
}, b.wireEncoding)
|
||||
if err != nil {
|
||||
metricsRef.recordPublish(ResultError)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.Publish(b.subject(env.Topic), wire); err != nil {
|
||||
metricsRef.recordPublish(ResultError)
|
||||
return err
|
||||
}
|
||||
metricsRef.recordPublish(ResultOK)
|
||||
metricsRef.recordPublishBytes(int64(len(body)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NATS) Request(ctx context.Context, topic string, payload any, timeout time.Duration) ([]byte, error) {
|
||||
return requestWithPubSub(ctx, topic, payload, timeout, b.Subscribe, b.publishEnvelope)
|
||||
}
|
||||
|
||||
func (b *NATS) Subscribe(topic string, handler Handler) (Subscription, error) {
|
||||
return b.subscribe(topic, "", handler)
|
||||
}
|
||||
|
||||
func (b *NATS) QueueSubscribe(topic string, queue string, handler Handler) (Subscription, error) {
|
||||
queue = normalizeQueueName(queue)
|
||||
if queue == "" {
|
||||
return b.Subscribe(topic, handler)
|
||||
}
|
||||
return b.subscribe(topic, queue, handler)
|
||||
}
|
||||
|
||||
func (b *NATS) subscribe(topic string, queue string, handler Handler) (Subscription, error) {
|
||||
if handler == nil {
|
||||
return nil, ErrHandlerRequired
|
||||
}
|
||||
if b == nil {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
b.mu.Lock()
|
||||
if b.closed {
|
||||
metricsRef := b.metrics
|
||||
b.mu.Unlock()
|
||||
metricsRef.recordSubscribe(ResultClosed)
|
||||
return nil, ErrClosed
|
||||
}
|
||||
conn := b.conn
|
||||
metricsRef := b.metrics
|
||||
b.mu.Unlock()
|
||||
if conn == nil {
|
||||
metricsRef.recordSubscribe(ResultClosed)
|
||||
return nil, ErrClosed
|
||||
}
|
||||
|
||||
// 回调里只做解包和 handler deadline 保护;超时只能记录失败,不能中断已经进入业务 handler 的 goroutine。
|
||||
callback := func(msg *nats.Msg) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
if b.logger != nil {
|
||||
b.logger.Error("nats handler panic recovered", "topic", topic, "panic", rec)
|
||||
}
|
||||
metricsRef.recordHandler(ResultPanic)
|
||||
}
|
||||
}()
|
||||
|
||||
wire, err := decodeNATSWire(msg.Data)
|
||||
if err != nil {
|
||||
if b.logger != nil {
|
||||
b.logger.Error("nats unmarshal failed", "topic", topic, "error", err)
|
||||
}
|
||||
metricsRef.recordHandler(ResultError)
|
||||
return
|
||||
}
|
||||
|
||||
var payload any = json.RawMessage(wire.Payload)
|
||||
if !isRequestReplyTopic(topic) {
|
||||
decoded, err := contracts.UnmarshalTopicPayload(topic, wire.Payload)
|
||||
if err != nil {
|
||||
if b.logger != nil {
|
||||
b.logger.Error("nats payload decode failed", "topic", topic, "error", err)
|
||||
}
|
||||
metricsRef.recordHandler(ResultError)
|
||||
return
|
||||
}
|
||||
payload = decoded
|
||||
}
|
||||
|
||||
publishedAt := wire.PublishedAt
|
||||
if publishedAt.IsZero() {
|
||||
publishedAt = time.Now().UTC()
|
||||
}
|
||||
reply := wire.Reply
|
||||
if reply == "" {
|
||||
reply = msg.Reply
|
||||
}
|
||||
|
||||
env := Envelope{
|
||||
Topic: topic,
|
||||
Payload: payload,
|
||||
PublishedAt: publishedAt,
|
||||
Reply: reply,
|
||||
RequestID: wire.RequestID,
|
||||
Trace: wire.Trace,
|
||||
}
|
||||
deliverWithDeadline(contextWithTrace(context.Background(), env), b.logger, topic, defHandlerTimeout, env, handler, metricsRef)
|
||||
}
|
||||
var sub *nats.Subscription
|
||||
var err error
|
||||
if queue == "" {
|
||||
sub, err = conn.Subscribe(b.subject(topic), callback)
|
||||
} else {
|
||||
sub, err = conn.QueueSubscribe(b.subject(topic), queue, callback)
|
||||
}
|
||||
if err != nil {
|
||||
metricsRef.recordSubscribe(ResultError)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricsRef.recordSubscribe(ResultOK)
|
||||
metricsRef.trackSubscription(1)
|
||||
return &natsSubscription{sub: sub, metrics: metricsRef}, nil
|
||||
}
|
||||
|
||||
func (b *NATS) Check(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if b == nil {
|
||||
return ErrClosed
|
||||
}
|
||||
b.mu.Lock()
|
||||
if b.closed {
|
||||
b.mu.Unlock()
|
||||
return ErrClosed
|
||||
}
|
||||
conn := b.conn
|
||||
b.mu.Unlock()
|
||||
|
||||
if conn == nil {
|
||||
return ErrClosed
|
||||
}
|
||||
return conn.FlushWithContext(ctx)
|
||||
}
|
||||
|
||||
func (b *NATS) Close() error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
b.mu.Lock()
|
||||
if b.closed {
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
b.closed = true
|
||||
conn := b.conn
|
||||
b.conn = nil
|
||||
b.mu.Unlock()
|
||||
|
||||
// Drain 给正在发送的消息一个收敛窗口,但 NATS core 仍不是持久队列;关闭期间丢失的 live 消息不做补偿。
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
if err := conn.Drain(); err != nil {
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NATS) subject(topic string) string {
|
||||
if b.prefix == "" {
|
||||
return topic
|
||||
}
|
||||
return b.prefix + "." + topic
|
||||
}
|
||||
|
||||
type natsSubscription struct {
|
||||
sub *nats.Subscription
|
||||
metrics *Metrics
|
||||
closed bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (s *natsSubscription) Close() error {
|
||||
if s == nil || s.sub == nil {
|
||||
return nil
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
s.closed = true
|
||||
s.mu.Unlock()
|
||||
err := s.sub.Unsubscribe()
|
||||
s.metrics.trackSubscription(-1)
|
||||
return err
|
||||
}
|
||||
Reference in new issue
Block a user