Files
90CNServer/server/internal/modules/dnf/repository/mysql/mysql_transaction.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

627 lines
22 KiB
Go

package mysql
import (
"context"
"database/sql"
"errors"
"longheng.io/server/internal/modules/dnf/repository"
"strings"
)
type sqlTxBeginner interface {
BeginTx(context.Context, *sql.TxOptions) (*sql.Tx, error)
}
type sqlTransactionDB struct {
tx *sql.Tx
}
func (db sqlTransactionDB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
return db.tx.ExecContext(ctx, query, args...)
}
func (db sqlTransactionDB) QueryContext(ctx context.Context, query string, args ...any) (SQLRows, error) {
return db.tx.QueryContext(ctx, query, args...)
}
func (db sqlTransactionDB) QueryRowContext(ctx context.Context, query string, args ...any) SQLRow {
return db.tx.QueryRowContext(ctx, query, args...)
}
func (db sqlTransactionDB) PingContext(ctx context.Context) error {
if ctx == nil {
return nil
}
return ctx.Err()
}
type mysqlCharacterItemUnitOfWork struct {
router mysqlRouter
}
type mysqlCharacterTradeUnitOfWork struct {
router mysqlRouter
}
type mysqlAccountCharacterTradeUnitOfWork struct {
router mysqlRouter
}
type mysqlCharacterAssetUnitOfWork struct {
router mysqlRouter
}
type mysqlRentalAssetUnitOfWork struct {
router mysqlRouter
}
type mysqlCeraShopAssetUnitOfWork struct {
router mysqlRouter
}
type mysqlCharacterSkillUnitOfWork struct {
router mysqlRouter
}
type mysqlCharacterProgressionUnitOfWork struct {
router mysqlRouter
}
type mysqlCharacterCreationUnitOfWork struct {
router mysqlRouter
}
func (u *mysqlCharacterCreationUnitOfWork) WithinCharacterCreation(ctx context.Context, characterID string, _ repository.Group, apply func(repository.Group) error) error {
characterID = strings.TrimSpace(characterID)
if characterID == "" || apply == nil {
return repository.ErrCharacterCreationTransactionUnavailable
}
if ctx == nil {
ctx = context.Background()
}
beginner, ok := u.router.db.(sqlTxBeginner)
if !ok {
return repository.ErrCharacterCreationTransactionUnavailable
}
tx, err := beginner.BeginTx(ctx, nil)
if err != nil {
return err
}
txRouter := u.router
txRouter.db = sqlTransactionDB{tx: tx}
txRouter.readDBs = append([]string(nil), u.router.writeDBs...)
txRouter.writeDBs = append([]string(nil), u.router.writeDBs...)
txRouter.lockReads = true
txGroup := newMySQLGroupFromRouter(txRouter, false)
if err := apply(txGroup); err != nil {
return errors.Join(err, tx.Rollback())
}
return tx.Commit()
}
func (u *mysqlCharacterSkillUnitOfWork) WithinCharacterSkill(ctx context.Context, characterID string, apply func(repository.SkillRepository) error) error {
characterID = strings.TrimSpace(characterID)
if characterID == "" || apply == nil {
return repository.ErrCharacterSkillTransactionUnavailable
}
if ctx == nil {
ctx = context.Background()
}
beginner, ok := u.router.db.(sqlTxBeginner)
if !ok {
return repository.ErrCharacterSkillTransactionUnavailable
}
tx, err := beginner.BeginTx(ctx, nil)
if err != nil {
return err
}
writeDatabase := u.router.writeDBs[pickDatabase(characterID, len(u.router.writeDBs))]
txRouter := u.router
txRouter.db = sqlTransactionDB{tx: tx}
txRouter.readDBs = []string{writeDatabase}
txRouter.writeDBs = []string{writeDatabase}
txRouter.lockReads = true
txGroup := newMySQLGroupFromRouter(txRouter, false)
if err := apply(txGroup.Skill); err != nil {
return errors.Join(err, tx.Rollback())
}
return tx.Commit()
}
func (u *mysqlCharacterProgressionUnitOfWork) WithinCharacterProgression(
ctx context.Context,
characterID string,
apply func(repository.CharacterRepository, repository.SkillRepository) error,
) error {
characterID = strings.TrimSpace(characterID)
if characterID == "" || apply == nil {
return repository.ErrCharacterProgressionTransactionUnavailable
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return err
}
beginner, ok := u.router.db.(sqlTxBeginner)
if !ok {
return repository.ErrCharacterProgressionTransactionUnavailable
}
tx, err := beginner.BeginTx(ctx, nil)
if err != nil {
return err
}
writeDatabase := u.router.writeDBs[pickDatabase(characterID, len(u.router.writeDBs))]
txRouter := u.router
txRouter.db = sqlTransactionDB{tx: tx}
txRouter.readDBs = []string{writeDatabase}
txRouter.writeDBs = []string{writeDatabase}
txRouter.lockReads = true
txGroup := newMySQLGroupFromRouter(txRouter, false)
characters := &characterProgressionScopedCharacterRepository{
characterID: characterID,
base: txGroup.Character,
}
skills := &characterProgressionScopedSkillRepository{
characterID: characterID,
base: txGroup.Skill,
}
if err := apply(characters, skills); err != nil {
return errors.Join(err, tx.Rollback())
}
if err := ctx.Err(); err != nil {
return errors.Join(err, tx.Rollback())
}
return tx.Commit()
}
type characterProgressionScopedCharacterRepository struct {
characterID string
base repository.CharacterRepository
}
func (r *characterProgressionScopedCharacterRepository) Load(ctx context.Context, characterID string) (repository.CharacterRecord, bool, error) {
if err := repository.TransactionKeyError(ctx, r.characterID, characterID); err != nil {
return repository.CharacterRecord{}, false, err
}
return r.base.Load(ctx, characterID)
}
func (r *characterProgressionScopedCharacterRepository) Save(ctx context.Context, record repository.CharacterRecord) error {
if err := repository.TransactionKeyError(ctx, r.characterID, record.CharacterID); err != nil {
return err
}
return r.base.Save(ctx, record)
}
func (r *characterProgressionScopedCharacterRepository) SaveFields(ctx context.Context, record repository.CharacterRecord, fields ...repository.CharacterField) error {
if err := repository.TransactionKeyError(ctx, r.characterID, record.CharacterID); err != nil {
return err
}
return repository.SaveCharacterFields(ctx, r.base, record, fields...)
}
func (r *characterProgressionScopedCharacterRepository) ListByAccount(context.Context, string, int) ([]repository.CharacterRecord, error) {
return nil, repository.ErrCharacterProgressionTransactionUnavailable
}
func (r *characterProgressionScopedCharacterRepository) FindIDByName(context.Context, string) (string, bool, error) {
return "", false, repository.ErrCharacterProgressionTransactionUnavailable
}
func (r *characterProgressionScopedCharacterRepository) NextNumericID(context.Context) (int, error) {
return 0, repository.ErrCharacterProgressionTransactionUnavailable
}
type characterProgressionScopedSkillRepository struct {
characterID string
base repository.SkillRepository
}
func (r *characterProgressionScopedSkillRepository) Load(ctx context.Context, characterID string) (repository.SkillRecord, bool, error) {
if err := repository.TransactionKeyError(ctx, r.characterID, characterID); err != nil {
return repository.SkillRecord{}, false, err
}
return r.base.Load(ctx, characterID)
}
func (r *characterProgressionScopedSkillRepository) Save(ctx context.Context, record repository.SkillRecord) error {
if err := repository.TransactionKeyError(ctx, r.characterID, record.CharacterID); err != nil {
return err
}
return r.base.Save(ctx, record)
}
func (r *characterProgressionScopedSkillRepository) SaveFields(ctx context.Context, record repository.SkillRecord, fields ...repository.SkillField) error {
if err := repository.TransactionKeyError(ctx, r.characterID, record.CharacterID); err != nil {
return err
}
return repository.SaveSkillFields(ctx, r.base, record, fields...)
}
func (u *mysqlCharacterItemUnitOfWork) WithinCharacterItems(ctx context.Context, characterID string, apply func(repository.InventoryRepository, repository.EquipmentRepository) error) error {
characterID = strings.TrimSpace(characterID)
if characterID == "" || apply == nil {
return repository.ErrCharacterItemTransactionUnavailable
}
if ctx == nil {
ctx = context.Background()
}
beginner, ok := u.router.db.(sqlTxBeginner)
if !ok {
return repository.ErrCharacterItemTransactionUnavailable
}
tx, err := beginner.BeginTx(ctx, nil)
if err != nil {
return err
}
writeDatabase := u.router.writeDBs[pickDatabase(characterID, len(u.router.writeDBs))]
txRouter := u.router
txRouter.db = sqlTransactionDB{tx: tx}
txRouter.readDBs = []string{writeDatabase}
txRouter.writeDBs = []string{writeDatabase}
txRouter.lockReads = true
txGroup := newMySQLGroupFromRouter(txRouter, false)
if err := apply(txGroup.Inventory, txGroup.Equipment); err != nil {
return errors.Join(err, tx.Rollback())
}
return tx.Commit()
}
func (u *mysqlCharacterTradeUnitOfWork) WithinCharacterTrade(
ctx context.Context,
firstCharacterID string,
secondCharacterID string,
apply func(repository.CharacterRepository, repository.InventoryRepository) error,
) error {
firstCharacterID = strings.TrimSpace(firstCharacterID)
secondCharacterID = strings.TrimSpace(secondCharacterID)
if firstCharacterID == "" || secondCharacterID == "" || firstCharacterID == secondCharacterID || apply == nil {
return repository.ErrCharacterTradeTransactionUnavailable
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return err
}
beginner, ok := u.router.db.(sqlTxBeginner)
if !ok {
return repository.ErrCharacterTradeTransactionUnavailable
}
tx, err := beginner.BeginTx(ctx, nil)
if err != nil {
return err
}
txRouter := u.router
txRouter.db = sqlTransactionDB{tx: tx}
// Character IDs can route to different logical schemas. They remain on
// one physical MySQL connection, so keep the full qualified write plan.
txRouter.readDBs = append([]string(nil), u.router.writeDBs...)
txRouter.writeDBs = append([]string(nil), u.router.writeDBs...)
txRouter.lockReads = true
txGroup := newMySQLGroupFromRouter(txRouter, false)
allowed := map[string]struct{}{firstCharacterID: {}, secondCharacterID: {}}
characters := &characterTradeScopedCharacterRepository{allowed: allowed, base: txGroup.Character}
inventories := &characterTradeScopedInventoryRepository{allowed: allowed, base: txGroup.Inventory}
if err := apply(characters, inventories); err != nil {
return errors.Join(err, tx.Rollback())
}
if err := ctx.Err(); err != nil {
return errors.Join(err, tx.Rollback())
}
return tx.Commit()
}
type characterTradeScopedCharacterRepository struct {
allowed map[string]struct{}
base repository.CharacterRepository
}
func (r *characterTradeScopedCharacterRepository) Load(ctx context.Context, characterID string) (repository.CharacterRecord, bool, error) {
characterID = strings.TrimSpace(characterID)
if _, allowed := r.allowed[characterID]; !allowed {
return repository.CharacterRecord{}, false, repository.ErrCharacterTradeTransactionUnavailable
}
return r.base.Load(ctx, characterID)
}
func (r *characterTradeScopedCharacterRepository) Save(ctx context.Context, record repository.CharacterRecord) error {
if _, allowed := r.allowed[strings.TrimSpace(record.CharacterID)]; !allowed {
return repository.ErrCharacterTradeTransactionUnavailable
}
return r.base.Save(ctx, record)
}
func (r *characterTradeScopedCharacterRepository) ListByAccount(context.Context, string, int) ([]repository.CharacterRecord, error) {
return nil, repository.ErrCharacterTradeTransactionUnavailable
}
func (r *characterTradeScopedCharacterRepository) FindIDByName(context.Context, string) (string, bool, error) {
return "", false, repository.ErrCharacterTradeTransactionUnavailable
}
func (r *characterTradeScopedCharacterRepository) NextNumericID(context.Context) (int, error) {
return 0, repository.ErrCharacterTradeTransactionUnavailable
}
type characterTradeScopedInventoryRepository struct {
allowed map[string]struct{}
base repository.InventoryRepository
}
func (r *characterTradeScopedInventoryRepository) Load(ctx context.Context, characterID string) (repository.InventoryRecord, bool, error) {
characterID = strings.TrimSpace(characterID)
if _, allowed := r.allowed[characterID]; !allowed {
return repository.InventoryRecord{}, false, repository.ErrCharacterTradeTransactionUnavailable
}
return r.base.Load(ctx, characterID)
}
func (r *characterTradeScopedInventoryRepository) Save(ctx context.Context, record repository.InventoryRecord) error {
if _, allowed := r.allowed[strings.TrimSpace(record.CharacterID)]; !allowed {
return repository.ErrCharacterTradeTransactionUnavailable
}
return r.base.Save(ctx, record)
}
func (u *mysqlCharacterAssetUnitOfWork) WithinCharacterAssets(ctx context.Context, characterID string, apply func(repository.CharacterRepository, repository.InventoryRepository, repository.EquipmentRepository) error) error {
characterID = strings.TrimSpace(characterID)
if characterID == "" || apply == nil {
return repository.ErrCharacterAssetTransactionUnavailable
}
if ctx == nil {
ctx = context.Background()
}
beginner, ok := u.router.db.(sqlTxBeginner)
if !ok {
return repository.ErrCharacterAssetTransactionUnavailable
}
tx, err := beginner.BeginTx(ctx, nil)
if err != nil {
return err
}
writeDatabase := u.router.writeDBs[pickDatabase(characterID, len(u.router.writeDBs))]
txRouter := u.router
txRouter.db = sqlTransactionDB{tx: tx}
txRouter.readDBs = []string{writeDatabase}
txRouter.writeDBs = []string{writeDatabase}
txRouter.lockReads = true
txGroup := newMySQLGroupFromRouter(txRouter, false)
if err := apply(txGroup.Character, txGroup.Inventory, txGroup.Equipment); err != nil {
return errors.Join(err, tx.Rollback())
}
return tx.Commit()
}
func (u *mysqlRentalAssetUnitOfWork) WithinRentalAssets(
ctx context.Context,
accountID string,
characterID string,
apply func(repository.AccountRepository, repository.CharacterRepository, repository.InventoryRepository, repository.EquipmentRepository) error,
) error {
accountID = strings.TrimSpace(accountID)
characterID = strings.TrimSpace(characterID)
if accountID == "" || characterID == "" || apply == nil {
return repository.ErrRentalAssetTransactionUnavailable
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return err
}
beginner, ok := u.router.db.(sqlTxBeginner)
if !ok {
return repository.ErrRentalAssetTransactionUnavailable
}
tx, err := beginner.BeginTx(ctx, nil)
if err != nil {
return err
}
// Account and character keys can hash to different logical databases. Keep
// the complete write plan while using one physical MySQL transaction.
txRouter := u.router
txRouter.db = sqlTransactionDB{tx: tx}
txRouter.readDBs = append([]string(nil), u.router.writeDBs...)
txRouter.writeDBs = append([]string(nil), u.router.writeDBs...)
txRouter.lockReads = true
txGroup := newMySQLGroupFromRouter(txRouter, false)
accounts := &rentalScopedAccountRepository{accountID: accountID, base: txGroup.Account}
characters := &rentalScopedCharacterRepository{characterID: characterID, base: txGroup.Character}
inventories := &rentalScopedInventoryRepository{characterID: characterID, base: txGroup.Inventory}
equipment := &rentalScopedEquipmentRepository{characterID: characterID, base: txGroup.Equipment}
if err := apply(accounts, characters, inventories, equipment); err != nil {
return errors.Join(err, tx.Rollback())
}
if err := ctx.Err(); err != nil {
return errors.Join(err, tx.Rollback())
}
return tx.Commit()
}
func (u *mysqlCeraShopAssetUnitOfWork) WithinCeraShopAssets(
ctx context.Context,
accountID string,
characterID string,
settingsScope string,
apply func(repository.AccountRepository, repository.AccountInventoryRepository, repository.CharacterRepository, repository.InventoryRepository, repository.EquipmentRepository, repository.SettingsRepository) error,
) error {
accountID = strings.TrimSpace(accountID)
characterID = strings.TrimSpace(characterID)
settingsScope = strings.TrimSpace(settingsScope)
if accountID == "" || characterID == "" || settingsScope == "" || apply == nil {
return repository.ErrCeraShopAssetTransactionUnavailable
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return err
}
beginner, ok := u.router.db.(sqlTxBeginner)
if !ok {
return repository.ErrCeraShopAssetTransactionUnavailable
}
tx, err := beginner.BeginTx(ctx, nil)
if err != nil {
return err
}
// The active local profile uses one physical MySQL server. Keep the full
// qualified write plan so account, character, and settings keys remain in
// one SQL transaction even when their logical database hashes differ.
txRouter := u.router
txRouter.db = sqlTransactionDB{tx: tx}
txRouter.readDBs = append([]string(nil), u.router.writeDBs...)
txRouter.writeDBs = append([]string(nil), u.router.writeDBs...)
txRouter.lockReads = true
txGroup := newMySQLGroupFromRouter(txRouter, false)
accounts := &rentalScopedAccountRepository{accountID: accountID, base: txGroup.Account}
// 账号共享仓与扣点在同一物理事务里:商城/冒险团商店卖出的无色小晶块、灵魂
// 落的是账号记录的 list 0 固定格(354..365),不能扣了点却没发货。
accountInventories := &characterSettlementAccountInventoryRepository{
characterID: characterID,
characters: txGroup.Character,
accounts: txGroup.AccountInventory,
}
characters := &rentalScopedCharacterRepository{characterID: characterID, base: txGroup.Character}
inventories := &rentalScopedInventoryRepository{characterID: characterID, base: txGroup.Inventory}
equipment := &rentalScopedEquipmentRepository{characterID: characterID, base: txGroup.Equipment}
settings := &ceraShopScopedSettingsRepository{scope: settingsScope, base: txGroup.Settings}
if err := apply(accounts, accountInventories, characters, inventories, equipment, settings); err != nil {
return errors.Join(err, tx.Rollback())
}
if err := ctx.Err(); err != nil {
return errors.Join(err, tx.Rollback())
}
return tx.Commit()
}
type ceraShopScopedSettingsRepository struct {
scope string
base repository.SettingsRepository
}
func (r *ceraShopScopedSettingsRepository) Load(ctx context.Context, scope string) (repository.SettingsRecord, bool, error) {
if err := repository.TransactionKeyError(ctx, r.scope, scope); err != nil {
return repository.SettingsRecord{}, false, err
}
return r.base.Load(ctx, scope)
}
func (r *ceraShopScopedSettingsRepository) Save(ctx context.Context, record repository.SettingsRecord) error {
if err := repository.TransactionKeyError(ctx, r.scope, record.Scope); err != nil {
return err
}
return r.base.Save(ctx, record)
}
type rentalScopedAccountRepository struct {
accountID string
base repository.AccountRepository
}
func (r *rentalScopedAccountRepository) Load(ctx context.Context, accountID string) (repository.AccountRecord, bool, error) {
if err := repository.TransactionKeyError(ctx, r.accountID, accountID); err != nil {
return repository.AccountRecord{}, false, err
}
return r.base.Load(ctx, accountID)
}
func (r *rentalScopedAccountRepository) Save(ctx context.Context, record repository.AccountRecord) error {
if err := repository.TransactionKeyError(ctx, r.accountID, record.AccountID); err != nil {
return err
}
return r.base.Save(ctx, record)
}
type rentalScopedCharacterRepository struct {
characterID string
base repository.CharacterRepository
}
func (r *rentalScopedCharacterRepository) Load(ctx context.Context, characterID string) (repository.CharacterRecord, bool, error) {
if err := repository.TransactionKeyError(ctx, r.characterID, characterID); err != nil {
return repository.CharacterRecord{}, false, err
}
return r.base.Load(ctx, characterID)
}
func (r *rentalScopedCharacterRepository) Save(ctx context.Context, record repository.CharacterRecord) error {
if err := repository.TransactionKeyError(ctx, r.characterID, record.CharacterID); err != nil {
return err
}
return r.base.Save(ctx, record)
}
func (r *rentalScopedCharacterRepository) SaveFields(ctx context.Context, record repository.CharacterRecord, fields ...repository.CharacterField) error {
if err := repository.TransactionKeyError(ctx, r.characterID, record.CharacterID); err != nil {
return err
}
return repository.SaveCharacterFields(ctx, r.base, record, fields...)
}
func (r *rentalScopedCharacterRepository) ListByAccount(context.Context, string, int) ([]repository.CharacterRecord, error) {
return nil, repository.ErrRentalAssetTransactionUnavailable
}
func (r *rentalScopedCharacterRepository) FindIDByName(ctx context.Context, name string) (string, bool, error) {
return r.base.FindIDByName(ctx, name)
}
func (r *rentalScopedCharacterRepository) NextNumericID(context.Context) (int, error) {
return 0, repository.ErrRentalAssetTransactionUnavailable
}
type rentalScopedInventoryRepository struct {
characterID string
base repository.InventoryRepository
}
func (r *rentalScopedInventoryRepository) Load(ctx context.Context, characterID string) (repository.InventoryRecord, bool, error) {
if err := repository.TransactionKeyError(ctx, r.characterID, characterID); err != nil {
return repository.InventoryRecord{}, false, err
}
return r.base.Load(ctx, characterID)
}
func (r *rentalScopedInventoryRepository) Save(ctx context.Context, record repository.InventoryRecord) error {
if err := repository.TransactionKeyError(ctx, r.characterID, record.CharacterID); err != nil {
return err
}
return r.base.Save(ctx, record)
}
func (r *rentalScopedInventoryRepository) SaveFields(ctx context.Context, record repository.InventoryRecord, fields ...repository.InventoryField) error {
if err := repository.TransactionKeyError(ctx, r.characterID, record.CharacterID); err != nil {
return err
}
return repository.SaveInventoryFields(ctx, r.base, record, fields...)
}
type rentalScopedEquipmentRepository struct {
characterID string
base repository.EquipmentRepository
}
func (r *rentalScopedEquipmentRepository) Load(ctx context.Context, characterID string) (repository.EquipmentRecord, bool, error) {
if err := repository.TransactionKeyError(ctx, r.characterID, characterID); err != nil {
return repository.EquipmentRecord{}, false, err
}
return r.base.Load(ctx, characterID)
}
func (r *rentalScopedEquipmentRepository) Save(ctx context.Context, record repository.EquipmentRecord) error {
if err := repository.TransactionKeyError(ctx, r.characterID, record.CharacterID); err != nil {
return err
}
return r.base.Save(ctx, record)
}
func (r *rentalScopedEquipmentRepository) SaveFields(ctx context.Context, record repository.EquipmentRecord, fields ...repository.EquipmentField) error {
if err := repository.TransactionKeyError(ctx, r.characterID, record.CharacterID); err != nil {
return err
}
return repository.SaveEquipmentFields(ctx, r.base, record, fields...)
}