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,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)
|
||||
}
|
||||
Reference in new issue
Block a user