Files
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

429 lines
10 KiB
Go

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
}