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,371 @@
package mysql
import (
"context"
"database/sql"
"longheng.io/server/internal/modules/dnf/repository"
)
const mysqlMailboxTable = "mailboxes"
type mysqlMailboxStore struct {
mysqlStoreBase
}
// Load reads one character mailbox from MySQL.
func (s *mysqlMailboxStore) Load(ctx context.Context, characterID string) (repository.MailboxRecord, bool, error) {
table, err := s.router.readTable(mysqlMailboxTable, characterID)
if err != nil {
return repository.MailboxRecord{}, false, err
}
mailsTable, err := s.router.readTable(mysqlMailsTable, characterID)
if err != nil {
return repository.MailboxRecord{}, false, err
}
metadataTable, err := s.router.readTable(mysqlMailMetadataTable, characterID)
if err != nil {
return repository.MailboxRecord{}, false, err
}
attachmentsTable, err := s.router.readTable(mysqlMailAttachmentsTable, characterID)
if err != nil {
return repository.MailboxRecord{}, false, err
}
attachmentExtraTable, err := s.router.readTable(mysqlMailAttachmentExtraTable, characterID)
if err != nil {
return repository.MailboxRecord{}, false, err
}
query := s.router.selectQuery("SELECT character_id, updated_at FROM " + table + " WHERE character_id = ?")
var record repository.MailboxRecord
var updatedAt sql.NullTime
err = s.router.db.QueryRowContext(ctx, query, characterID).Scan(
&record.CharacterID,
&updatedAt,
)
if err != nil {
ok, scanErr := scanErr(err)
return repository.MailboxRecord{}, ok, scanErr
}
record.Mails, err = loadMails(
ctx,
s.router,
mailsTable,
metadataTable,
attachmentsTable,
attachmentExtraTable,
characterID,
)
if err != nil {
return repository.MailboxRecord{}, false, err
}
record.UpdatedAt = scanTime(updatedAt)
return repository.CloneMailbox(record), true, nil
}
// Save persists the whole mailbox aggregate.
func (s *mysqlMailboxStore) Save(ctx context.Context, record repository.MailboxRecord) error {
return s.SaveFields(ctx, record, repository.AllMailboxFields()...)
}
// SaveFields persists selected mailbox fields.
func (s *mysqlMailboxStore) SaveFields(ctx context.Context, record repository.MailboxRecord, fields ...repository.MailboxField) error {
characterID, err := requireRecordKey(repository.MailboxKey, record, "mailbox")
if err != nil {
return err
}
fields = repository.MailboxFields.Normalize(fields)
if len(fields) == 0 {
return nil
}
table, err := s.router.writeTable(mysqlMailboxTable, characterID)
if err != nil {
return err
}
mailsTable, err := s.router.writeTable(mysqlMailsTable, characterID)
if err != nil {
return err
}
metadataTable, err := s.router.writeTable(mysqlMailMetadataTable, characterID)
if err != nil {
return err
}
attachmentsTable, err := s.router.writeTable(mysqlMailAttachmentsTable, characterID)
if err != nil {
return err
}
attachmentExtraTable, err := s.router.writeTable(mysqlMailAttachmentExtraTable, characterID)
if err != nil {
return err
}
columns := []string{"character_id", "updated_at"}
args := []any{characterID, timeOrNow(record.UpdatedAt, s.router.now)}
updates := []string{updateValue("updated_at")}
saveMails := false
for _, field := range fields {
if field == repository.MailboxFieldMails {
saveMails = true
}
}
return withMySQLWriteExecutor(ctx, s.router.db, func(database SQLDB) error {
if _, execErr := database.ExecContext(ctx, buildUpsert(table, columns, updates), args...); execErr != nil {
return execErr
}
if !saveMails {
return nil
}
return replaceMails(
ctx,
database,
mailsTable,
metadataTable,
attachmentsTable,
attachmentExtraTable,
characterID,
record.Mails,
)
})
}
func loadMails(
ctx context.Context,
router mysqlRouter,
mailsTable, metadataTable, attachmentsTable, attachmentExtraTable, characterID string,
) (map[string]repository.MailRecord, error) {
query := router.selectQuery("SELECT mail_id, sender_character_id, sender_name, recipient_character_id, recipient_name, title, body, gold, read_flag, claimed_flag, deleted_flag, created_at, expire_at FROM " + mailsTable + " WHERE character_id = ? ORDER BY mail_id")
rows, err := router.db.QueryContext(ctx, query, characterID)
if err != nil {
return nil, err
}
var mails map[string]repository.MailRecord
for rows.Next() {
var mail repository.MailRecord
var createdAt, expireAt sql.NullTime
if err := rows.Scan(
&mail.MailID,
&mail.SenderCharacterID,
&mail.SenderName,
&mail.RecipientCharacterID,
&mail.RecipientName,
&mail.Title,
&mail.Body,
&mail.Gold,
&mail.Read,
&mail.Claimed,
&mail.Deleted,
&createdAt,
&expireAt,
); err != nil {
rows.Close()
return nil, err
}
mail.CreatedAt = scanTime(createdAt)
mail.ExpireAt = scanTime(expireAt)
if mails == nil {
mails = make(map[string]repository.MailRecord)
}
mails[mail.MailID] = mail
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, err
}
if err := rows.Close(); err != nil {
return nil, err
}
metadata, err := router.db.QueryContext(
ctx,
router.selectQuery("SELECT mail_id, metadata_key, metadata_value FROM "+metadataTable+" WHERE character_id = ? ORDER BY mail_id, metadata_key"),
characterID,
)
if err != nil {
return nil, err
}
for metadata.Next() {
var mailID, key, value string
if err := metadata.Scan(&mailID, &key, &value); err != nil {
metadata.Close()
return nil, err
}
mail, ok := mails[mailID]
if !ok {
continue
}
if mail.Metadata == nil {
mail.Metadata = make(map[string]string)
}
mail.Metadata[key] = value
mails[mailID] = mail
}
if err := metadata.Err(); err != nil {
metadata.Close()
return nil, err
}
if err := metadata.Close(); err != nil {
return nil, err
}
attachments, err := router.db.QueryContext(
ctx,
router.selectQuery("SELECT mail_id, attachment_index, item_id, item_count, bind_flag, expire_at, raw_entry FROM "+attachmentsTable+" WHERE character_id = ? ORDER BY mail_id, attachment_index"),
characterID,
)
if err != nil {
return nil, err
}
for attachments.Next() {
var mailID string
var index int
var attachment repository.MailAttachment
var expireAt sql.NullTime
var rawEntry []byte
if err := attachments.Scan(&mailID, &index, &attachment.ItemID, &attachment.Count, &attachment.Bind, &expireAt, &rawEntry); err != nil {
attachments.Close()
return nil, err
}
mail, ok := mails[mailID]
if !ok {
continue
}
attachment.ExpireAt = scanTime(expireAt)
attachment.RawEntry = append([]byte(nil), rawEntry...)
for len(mail.Attachments) <= index {
mail.Attachments = append(mail.Attachments, repository.MailAttachment{})
}
mail.Attachments[index] = attachment
mails[mailID] = mail
}
if err := attachments.Err(); err != nil {
attachments.Close()
return nil, err
}
if err := attachments.Close(); err != nil {
return nil, err
}
extras, err := router.db.QueryContext(
ctx,
router.selectQuery("SELECT mail_id, attachment_index, extra_key, extra_value FROM "+attachmentExtraTable+" WHERE character_id = ? ORDER BY mail_id, attachment_index, extra_key"),
characterID,
)
if err != nil {
return nil, err
}
defer extras.Close()
for extras.Next() {
var mailID, key, value string
var index int
if err := extras.Scan(&mailID, &index, &key, &value); err != nil {
return nil, err
}
mail, ok := mails[mailID]
if !ok || index < 0 || index >= len(mail.Attachments) {
continue
}
if mail.Attachments[index].Extra == nil {
mail.Attachments[index].Extra = make(map[string]string)
}
mail.Attachments[index].Extra[key] = value
mails[mailID] = mail
}
if err := extras.Err(); err != nil {
return nil, err
}
return mails, nil
}
func replaceMails(
ctx context.Context,
database SQLDB,
mailsTable, metadataTable, attachmentsTable, attachmentExtraTable, characterID string,
mails map[string]repository.MailRecord,
) error {
for _, table := range []string{attachmentExtraTable, attachmentsTable, metadataTable, mailsTable} {
if _, err := database.ExecContext(ctx, "DELETE FROM "+table+" WHERE character_id = ?", characterID); err != nil {
return err
}
}
// 改前是四层嵌套的逐行 ExecContext:每封邮件 1 条 + 每个元数据键 1 条 +
// 每个附件 1 条 + 每个附件附加键 1 条。邮箱里上百封邮件时,一次存档就是
// 数百条语句。按 128 行一批后,语句数只与「表数」有关,与邮件数无关。
//
// 四张表互不相同,因此把「按邮件交错写四张表」改为「逐表批量写」结果集一致;
// 每张表内部仍按 sortedStringKeys 的排序与原始下标(attachment_index)写入。
mailColumns := []string{
"character_id", "mail_id", "sender_character_id", "sender_name",
"recipient_character_id", "recipient_name", "title", "body", "gold",
"read_flag", "claimed_flag", "deleted_flag", "created_at", "expire_at",
}
metadataColumns := []string{"character_id", "mail_id", "metadata_key", "metadata_value"}
attachmentColumns := []string{
"character_id", "mail_id", "attachment_index", "item_id",
"item_count", "bind_flag", "expire_at", "raw_entry",
}
extraColumns := []string{
"character_id", "mail_id", "attachment_index", "extra_key", "extra_value",
}
mailArgs := make([]any, 0, len(mails)*len(mailColumns))
metadataArgs := make([]any, 0, len(mails)*4*len(metadataColumns))
attachmentArgs := make([]any, 0, len(mails)*4*len(attachmentColumns))
extraArgs := make([]any, 0, len(mails)*8*len(extraColumns))
for _, mailID := range sortedStringKeys(mails) {
mail := mails[mailID]
mailArgs = append(mailArgs,
characterID,
mailID,
mail.SenderCharacterID,
mail.SenderName,
mail.RecipientCharacterID,
mail.RecipientName,
mail.Title,
mail.Body,
mail.Gold,
mail.Read,
mail.Claimed,
mail.Deleted,
sqlTime(mail.CreatedAt),
sqlTime(mail.ExpireAt),
)
for _, key := range sortedStringKeys(mail.Metadata) {
metadataArgs = append(metadataArgs, characterID, mailID, key, mail.Metadata[key])
}
for index, attachment := range mail.Attachments {
attachmentArgs = append(attachmentArgs,
characterID,
mailID,
index,
attachment.ItemID,
attachment.Count,
attachment.Bind,
sqlTime(attachment.ExpireAt),
attachment.RawEntry,
)
for _, key := range sortedStringKeys(attachment.Extra) {
extraArgs = append(extraArgs, characterID, mailID, index, key, attachment.Extra[key])
}
}
}
writeMailBatch := func(table string, columns []string, args []any) error {
rows := len(args) / len(columns)
for start := 0; start < rows; start += mysqlReplaceBatchSize {
end := min(start+mysqlReplaceBatchSize, rows)
if _, err := database.ExecContext(
ctx,
buildMySQLBulkInsert(table, columns, end-start),
args[start*len(columns):end*len(columns)]...,
); err != nil {
return err
}
}
return nil
}
if err := writeMailBatch(mailsTable, mailColumns, mailArgs); err != nil {
return err
}
if err := writeMailBatch(metadataTable, metadataColumns, metadataArgs); err != nil {
return err
}
if err := writeMailBatch(attachmentsTable, attachmentColumns, attachmentArgs); err != nil {
return err
}
return writeMailBatch(attachmentExtraTable, extraColumns, extraArgs)
}