Files
90CNServer/server/internal/modules/dnf/repository/sqlite/state.go
T
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

437 lines
13 KiB
Go

package sqlite
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"longheng.io/server/internal/modules/dnf/repository"
"longheng.io/server/internal/modules/dnf/repository/memory"
)
const (
kindAccount = "account"
kindAccountInventory = "account_inventory"
kindCharacter = "character"
kindInventory = "inventory"
kindEquipment = "equipment"
kindPet = "pet"
kindQuest = "quest"
kindSkill = "skill"
kindDungeonPermission = "dungeon_permission"
kindHellPartyPermission = "hell_party_permission"
kindPacketTemplate = "packet_template"
kindSettings = "settings"
kindMailbox = "mailbox"
kindEpicBook = "epic_book"
kindUnitedFriend = "united_friend"
)
type persistedRecord struct {
kind string
key string
payload []byte
checksum string
}
func stateRecords(state memory.State) (map[string]persistedRecord, error) {
out := make(map[string]persistedRecord)
add := func(kind, key string, value any) error {
record, err := makePersistedRecord(kind, key, value)
if err != nil {
return err
}
out[record.kind+"\x00"+record.key] = record
return nil
}
for _, record := range state.Accounts {
if err := add(kindAccount, record.AccountID, record); err != nil {
return nil, err
}
}
for _, record := range state.AccountInventories {
if err := add(kindAccountInventory, record.AccountID, record); err != nil {
return nil, err
}
}
for _, record := range state.Characters {
if err := add(kindCharacter, record.CharacterID, record); err != nil {
return nil, err
}
}
for _, record := range state.Inventories {
if err := add(kindInventory, record.CharacterID, record); err != nil {
return nil, err
}
}
for _, record := range state.Equipment {
if err := add(kindEquipment, record.CharacterID, record); err != nil {
return nil, err
}
}
for _, record := range state.Pets {
if err := add(kindPet, record.CharacterID, record); err != nil {
return nil, err
}
}
for _, record := range state.Quests {
if err := add(kindQuest, record.CharacterID, record); err != nil {
return nil, err
}
}
for _, record := range state.Skills {
if err := add(kindSkill, record.CharacterID, record); err != nil {
return nil, err
}
}
for _, record := range state.DungeonPermissions {
if err := add(kindDungeonPermission, record.CharacterID, record); err != nil {
return nil, err
}
}
for _, record := range state.HellPartyPermissions {
if err := add(kindHellPartyPermission, record.CharacterID, record); err != nil {
return nil, err
}
}
for _, record := range state.PacketTemplates {
if err := add(kindPacketTemplate, record.TemplateID, record); err != nil {
return nil, err
}
}
for _, record := range state.Settings {
if err := add(kindSettings, record.Scope, record); err != nil {
return nil, err
}
}
for _, record := range state.Mailboxes {
if err := add(kindMailbox, record.CharacterID, record); err != nil {
return nil, err
}
}
for _, record := range state.EpicBooks {
if err := add(kindEpicBook, record.AccountID, record); err != nil {
return nil, err
}
}
for _, record := range state.UnitedFriends {
if err := add(kindUnitedFriend, record.OwnerName, record); err != nil {
return nil, err
}
}
return out, nil
}
func makePersistedRecord(kind, key string, value any) (persistedRecord, error) {
key = strings.TrimSpace(key)
if key == "" {
return persistedRecord{}, fmt.Errorf("sqlite repository %s record key is empty", kind)
}
payload, err := json.Marshal(value)
if err != nil {
return persistedRecord{}, fmt.Errorf("marshal sqlite repository %s %q: %w", kind, key, err)
}
sum := sha256.Sum256(payload)
return persistedRecord{
kind: kind,
key: key,
payload: payload,
checksum: hex.EncodeToString(sum[:]),
}, nil
}
func loadState(ctx context.Context, database *sql.DB) (memory.State, error) {
state := memory.State{Version: memory.StateVersion}
var versionText string
err := database.QueryRowContext(ctx, `SELECT meta_value FROM dnf_repository_meta WHERE meta_key = 'state_version'`).Scan(&versionText)
if err != nil && err != sql.ErrNoRows {
return memory.State{}, fmt.Errorf("read sqlite repository state version: %w", err)
}
if err == nil {
version, parseErr := strconv.Atoi(versionText)
if parseErr != nil || version != memory.StateVersion {
return memory.State{}, fmt.Errorf("unsupported sqlite repository state version %q", versionText)
}
state.Version = version
}
rows, err := database.QueryContext(ctx, `SELECT record_kind, record_key, payload, checksum FROM dnf_repository_records ORDER BY record_kind, record_key`)
if err != nil {
return memory.State{}, fmt.Errorf("query sqlite repository records: %w", err)
}
defer rows.Close()
for rows.Next() {
var kind, key, wantChecksum string
var payload []byte
if err := rows.Scan(&kind, &key, &payload, &wantChecksum); err != nil {
return memory.State{}, fmt.Errorf("scan sqlite repository record: %w", err)
}
sum := sha256.Sum256(payload)
if got := hex.EncodeToString(sum[:]); !strings.EqualFold(got, wantChecksum) {
return memory.State{}, fmt.Errorf("sqlite repository checksum mismatch for %s %q", kind, key)
}
if err := appendStateRecord(&state, kind, key, payload); err != nil {
return memory.State{}, err
}
}
if err := rows.Err(); err != nil {
return memory.State{}, fmt.Errorf("iterate sqlite repository records: %w", err)
}
return state, nil
}
func appendStateRecord(state *memory.State, kind, key string, payload []byte) error {
decode := func(target any) error {
if err := json.Unmarshal(payload, target); err != nil {
return fmt.Errorf("decode sqlite repository %s %q: %w", kind, key, err)
}
return nil
}
switch kind {
case kindAccount:
var record repository.AccountRecord
if err := decode(&record); err != nil {
return err
}
state.Accounts = append(state.Accounts, record)
case kindAccountInventory:
var record repository.AccountInventoryRecord
if err := decode(&record); err != nil {
return err
}
state.AccountInventories = append(state.AccountInventories, record)
case kindCharacter:
var record repository.CharacterRecord
if err := decode(&record); err != nil {
return err
}
state.Characters = append(state.Characters, record)
case kindInventory:
var record repository.InventoryRecord
if err := decode(&record); err != nil {
return err
}
state.Inventories = append(state.Inventories, record)
case kindEquipment:
var record repository.EquipmentRecord
if err := decode(&record); err != nil {
return err
}
state.Equipment = append(state.Equipment, record)
case kindPet:
var record repository.PetRecord
if err := decode(&record); err != nil {
return err
}
state.Pets = append(state.Pets, record)
case kindQuest:
var record repository.QuestRecord
if err := decode(&record); err != nil {
return err
}
state.Quests = append(state.Quests, record)
case kindSkill:
var record repository.SkillRecord
if err := decode(&record); err != nil {
return err
}
state.Skills = append(state.Skills, record)
case kindDungeonPermission:
var record repository.DungeonPermissionRecord
if err := decode(&record); err != nil {
return err
}
state.DungeonPermissions = append(state.DungeonPermissions, record)
case kindHellPartyPermission:
var record repository.HellPartyPermissionRecord
if err := decode(&record); err != nil {
return err
}
state.HellPartyPermissions = append(state.HellPartyPermissions, record)
case kindPacketTemplate:
var record repository.PacketTemplateRecord
if err := decode(&record); err != nil {
return err
}
state.PacketTemplates = append(state.PacketTemplates, record)
case kindSettings:
var record repository.SettingsRecord
if err := decode(&record); err != nil {
return err
}
state.Settings = append(state.Settings, record)
case kindMailbox:
var record repository.MailboxRecord
if err := decode(&record); err != nil {
return err
}
state.Mailboxes = append(state.Mailboxes, record)
case kindEpicBook:
var record repository.EpicBookRecord
if err := decode(&record); err != nil {
return err
}
state.EpicBooks = append(state.EpicBooks, record)
case kindUnitedFriend:
var record repository.UnitedFriendRecord
if err := decode(&record); err != nil {
return err
}
state.UnitedFriends = append(state.UnitedFriends, record)
default:
return fmt.Errorf("unsupported sqlite repository record kind %q", kind)
}
return nil
}
func persistTransition(ctx context.Context, database *sql.DB, before, after memory.State) error {
oldRecords, err := stateRecords(before)
if err != nil {
return err
}
newRecords, err := stateRecords(after)
if err != nil {
return err
}
return persistRecordTransition(ctx, database, oldRecords, newRecords)
}
// persistCharacterAssetsTransition writes only the durable records covered by
// CharacterAssetUnitOfWork. The old side intentionally comes from the last
// persisted state rather than the live runtime: an earlier async mutation may
// still be dirty, and this synchronous asset commit must include the current
// character-owned values without flushing unrelated records.
func persistCharacterAssetsTransition(
ctx context.Context,
database *sql.DB,
persisted memory.State,
after characterAssetsSnapshot,
) error {
oldRecords, err := characterAssetsStateRecords(persisted, after.character.CharacterID)
if err != nil {
return err
}
newRecords, err := characterAssetsSnapshotRecords(after)
if err != nil {
return err
}
return persistRecordTransition(ctx, database, oldRecords, newRecords)
}
func characterAssetsStateRecords(state memory.State, characterID string) (map[string]persistedRecord, error) {
characterID = strings.TrimSpace(characterID)
if characterID == "" {
return nil, fmt.Errorf("sqlite character-asset record key is empty")
}
out := make(map[string]persistedRecord, 3)
for _, record := range state.Characters {
if record.CharacterID != characterID {
continue
}
persisted, err := makePersistedRecord(kindCharacter, record.CharacterID, record)
if err != nil {
return nil, err
}
out[persisted.kind+"\x00"+persisted.key] = persisted
break
}
for _, record := range state.Inventories {
if record.CharacterID != characterID {
continue
}
persisted, err := makePersistedRecord(kindInventory, record.CharacterID, record)
if err != nil {
return nil, err
}
out[persisted.kind+"\x00"+persisted.key] = persisted
break
}
for _, record := range state.Equipment {
if record.CharacterID != characterID {
continue
}
persisted, err := makePersistedRecord(kindEquipment, record.CharacterID, record)
if err != nil {
return nil, err
}
out[persisted.kind+"\x00"+persisted.key] = persisted
break
}
return out, nil
}
func characterAssetsSnapshotRecords(snapshot characterAssetsSnapshot) (map[string]persistedRecord, error) {
out := make(map[string]persistedRecord, 3)
appendRecord := func(kind string, found bool, key string, value any) error {
if !found {
return nil
}
persisted, err := makePersistedRecord(kind, key, value)
if err != nil {
return err
}
out[persisted.kind+"\x00"+persisted.key] = persisted
return nil
}
if err := appendRecord(kindCharacter, snapshot.characterFound, snapshot.character.CharacterID, snapshot.character); err != nil {
return nil, err
}
if err := appendRecord(kindInventory, snapshot.inventoryFound, snapshot.inventory.CharacterID, snapshot.inventory); err != nil {
return nil, err
}
if err := appendRecord(kindEquipment, snapshot.equipmentFound, snapshot.equipment.CharacterID, snapshot.equipment); err != nil {
return nil, err
}
return out, nil
}
func persistRecordTransition(
ctx context.Context,
database *sql.DB,
oldRecords map[string]persistedRecord,
newRecords map[string]persistedRecord,
) error {
tx, err := database.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin sqlite repository transaction: %w", err)
}
defer tx.Rollback()
for id, oldRecord := range oldRecords {
if _, ok := newRecords[id]; ok {
continue
}
if _, err := tx.ExecContext(ctx, `DELETE FROM dnf_repository_records WHERE record_kind = ? AND record_key = ?`, oldRecord.kind, oldRecord.key); err != nil {
return fmt.Errorf("delete sqlite repository %s %q: %w", oldRecord.kind, oldRecord.key, err)
}
}
updatedAt := time.Now().UTC().Format(time.RFC3339Nano)
for id, record := range newRecords {
if oldRecord, ok := oldRecords[id]; ok && oldRecord.checksum == record.checksum {
continue
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO dnf_repository_records (record_kind, record_key, payload, checksum, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(record_kind, record_key) DO UPDATE SET
payload = excluded.payload,
checksum = excluded.checksum,
updated_at = excluded.updated_at`, record.kind, record.key, record.payload, record.checksum, updatedAt); err != nil {
return fmt.Errorf("upsert sqlite repository %s %q: %w", record.kind, record.key, err)
}
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO dnf_repository_meta (meta_key, meta_value) VALUES ('state_version', ?)
ON CONFLICT(meta_key) DO UPDATE SET meta_value = excluded.meta_value`, strconv.Itoa(memory.StateVersion)); err != nil {
return fmt.Errorf("write sqlite repository state version: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit sqlite repository transaction: %w", err)
}
return nil
}