feat(transaction): 添加游戏事务系统 | add game transaction system (#381)
- TransactionManager/TransactionContext 事务管理 - MemoryStorage/RedisStorage/MongoStorage 存储实现 - CurrencyOperation/InventoryOperation/TradeOperation 内置操作 - SagaOrchestrator 分布式 Saga 编排 - withTransactions() Room 集成 - 完整中英文文档
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @zh 操作基类
|
||||
* @en Base operation class
|
||||
*/
|
||||
|
||||
import type {
|
||||
ITransactionOperation,
|
||||
ITransactionContext,
|
||||
OperationResult,
|
||||
} from '../core/types.js'
|
||||
|
||||
/**
|
||||
* @zh 操作基类
|
||||
* @en Base operation class
|
||||
*
|
||||
* @zh 提供通用的操作实现模板
|
||||
* @en Provides common operation implementation template
|
||||
*/
|
||||
export abstract class BaseOperation<TData = unknown, TResult = unknown>
|
||||
implements ITransactionOperation<TData, TResult>
|
||||
{
|
||||
abstract readonly name: string
|
||||
readonly data: TData
|
||||
|
||||
constructor(data: TData) {
|
||||
this.data = data
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 验证前置条件(默认通过)
|
||||
* @en Validate preconditions (passes by default)
|
||||
*/
|
||||
async validate(_ctx: ITransactionContext): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 执行操作
|
||||
* @en Execute operation
|
||||
*/
|
||||
abstract execute(ctx: ITransactionContext): Promise<OperationResult<TResult>>
|
||||
|
||||
/**
|
||||
* @zh 补偿操作
|
||||
* @en Compensate operation
|
||||
*/
|
||||
abstract compensate(ctx: ITransactionContext): Promise<void>
|
||||
|
||||
/**
|
||||
* @zh 创建成功结果
|
||||
* @en Create success result
|
||||
*/
|
||||
protected success(data?: TResult): OperationResult<TResult> {
|
||||
return { success: true, data }
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建失败结果
|
||||
* @en Create failure result
|
||||
*/
|
||||
protected failure(error: string, errorCode?: string): OperationResult<TResult> {
|
||||
return { success: false, error, errorCode }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @zh 货币操作
|
||||
* @en Currency operation
|
||||
*/
|
||||
|
||||
import type { ITransactionContext, OperationResult } from '../core/types.js'
|
||||
import { BaseOperation } from './BaseOperation.js'
|
||||
|
||||
/**
|
||||
* @zh 货币操作类型
|
||||
* @en Currency operation type
|
||||
*/
|
||||
export type CurrencyOperationType = 'add' | 'deduct'
|
||||
|
||||
/**
|
||||
* @zh 货币操作数据
|
||||
* @en Currency operation data
|
||||
*/
|
||||
export interface CurrencyOperationData {
|
||||
/**
|
||||
* @zh 操作类型
|
||||
* @en Operation type
|
||||
*/
|
||||
type: CurrencyOperationType
|
||||
|
||||
/**
|
||||
* @zh 玩家 ID
|
||||
* @en Player ID
|
||||
*/
|
||||
playerId: string
|
||||
|
||||
/**
|
||||
* @zh 货币类型(如 gold, diamond 等)
|
||||
* @en Currency type (e.g., gold, diamond)
|
||||
*/
|
||||
currency: string
|
||||
|
||||
/**
|
||||
* @zh 数量
|
||||
* @en Amount
|
||||
*/
|
||||
amount: number
|
||||
|
||||
/**
|
||||
* @zh 原因/来源
|
||||
* @en Reason/source
|
||||
*/
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 货币操作结果
|
||||
* @en Currency operation result
|
||||
*/
|
||||
export interface CurrencyOperationResult {
|
||||
/**
|
||||
* @zh 操作前余额
|
||||
* @en Balance before operation
|
||||
*/
|
||||
beforeBalance: number
|
||||
|
||||
/**
|
||||
* @zh 操作后余额
|
||||
* @en Balance after operation
|
||||
*/
|
||||
afterBalance: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 货币数据提供者接口
|
||||
* @en Currency data provider interface
|
||||
*/
|
||||
export interface ICurrencyProvider {
|
||||
/**
|
||||
* @zh 获取货币余额
|
||||
* @en Get currency balance
|
||||
*/
|
||||
getBalance(playerId: string, currency: string): Promise<number>
|
||||
|
||||
/**
|
||||
* @zh 设置货币余额
|
||||
* @en Set currency balance
|
||||
*/
|
||||
setBalance(playerId: string, currency: string, amount: number): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 货币操作
|
||||
* @en Currency operation
|
||||
*
|
||||
* @zh 用于处理货币的增加和扣除
|
||||
* @en Used for handling currency addition and deduction
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 扣除金币
|
||||
* tx.addOperation(new CurrencyOperation({
|
||||
* type: 'deduct',
|
||||
* playerId: 'player1',
|
||||
* currency: 'gold',
|
||||
* amount: 100,
|
||||
* reason: 'purchase_item',
|
||||
* }))
|
||||
*
|
||||
* // 增加钻石
|
||||
* tx.addOperation(new CurrencyOperation({
|
||||
* type: 'add',
|
||||
* playerId: 'player1',
|
||||
* currency: 'diamond',
|
||||
* amount: 50,
|
||||
* }))
|
||||
* ```
|
||||
*/
|
||||
export class CurrencyOperation extends BaseOperation<CurrencyOperationData, CurrencyOperationResult> {
|
||||
readonly name = 'currency'
|
||||
|
||||
private _provider: ICurrencyProvider | null = null
|
||||
private _beforeBalance: number = 0
|
||||
|
||||
/**
|
||||
* @zh 设置货币数据提供者
|
||||
* @en Set currency data provider
|
||||
*/
|
||||
setProvider(provider: ICurrencyProvider): this {
|
||||
this._provider = provider
|
||||
return this
|
||||
}
|
||||
|
||||
async validate(ctx: ITransactionContext): Promise<boolean> {
|
||||
if (this.data.amount <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.data.type === 'deduct') {
|
||||
const balance = await this._getBalance(ctx)
|
||||
return balance >= this.data.amount
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async execute(ctx: ITransactionContext): Promise<OperationResult<CurrencyOperationResult>> {
|
||||
const { type, playerId, currency, amount } = this.data
|
||||
|
||||
this._beforeBalance = await this._getBalance(ctx)
|
||||
|
||||
let afterBalance: number
|
||||
|
||||
if (type === 'add') {
|
||||
afterBalance = this._beforeBalance + amount
|
||||
} else {
|
||||
if (this._beforeBalance < amount) {
|
||||
return this.failure('Insufficient balance', 'INSUFFICIENT_BALANCE')
|
||||
}
|
||||
afterBalance = this._beforeBalance - amount
|
||||
}
|
||||
|
||||
await this._setBalance(ctx, afterBalance)
|
||||
|
||||
ctx.set(`currency:${playerId}:${currency}:before`, this._beforeBalance)
|
||||
ctx.set(`currency:${playerId}:${currency}:after`, afterBalance)
|
||||
|
||||
return this.success({
|
||||
beforeBalance: this._beforeBalance,
|
||||
afterBalance,
|
||||
})
|
||||
}
|
||||
|
||||
async compensate(ctx: ITransactionContext): Promise<void> {
|
||||
await this._setBalance(ctx, this._beforeBalance)
|
||||
}
|
||||
|
||||
private async _getBalance(ctx: ITransactionContext): Promise<number> {
|
||||
const { playerId, currency } = this.data
|
||||
|
||||
if (this._provider) {
|
||||
return this._provider.getBalance(playerId, currency)
|
||||
}
|
||||
|
||||
if (ctx.storage) {
|
||||
const balance = await ctx.storage.get<number>(`player:${playerId}:currency:${currency}`)
|
||||
return balance ?? 0
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
private async _setBalance(ctx: ITransactionContext, amount: number): Promise<void> {
|
||||
const { playerId, currency } = this.data
|
||||
|
||||
if (this._provider) {
|
||||
await this._provider.setBalance(playerId, currency, amount)
|
||||
return
|
||||
}
|
||||
|
||||
if (ctx.storage) {
|
||||
await ctx.storage.set(`player:${playerId}:currency:${currency}`, amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建货币操作
|
||||
* @en Create currency operation
|
||||
*/
|
||||
export function createCurrencyOperation(data: CurrencyOperationData): CurrencyOperation {
|
||||
return new CurrencyOperation(data)
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* @zh 背包操作
|
||||
* @en Inventory operation
|
||||
*/
|
||||
|
||||
import type { ITransactionContext, OperationResult } from '../core/types.js'
|
||||
import { BaseOperation } from './BaseOperation.js'
|
||||
|
||||
/**
|
||||
* @zh 背包操作类型
|
||||
* @en Inventory operation type
|
||||
*/
|
||||
export type InventoryOperationType = 'add' | 'remove' | 'update'
|
||||
|
||||
/**
|
||||
* @zh 物品数据
|
||||
* @en Item data
|
||||
*/
|
||||
export interface ItemData {
|
||||
/**
|
||||
* @zh 物品 ID
|
||||
* @en Item ID
|
||||
*/
|
||||
itemId: string
|
||||
|
||||
/**
|
||||
* @zh 数量
|
||||
* @en Quantity
|
||||
*/
|
||||
quantity: number
|
||||
|
||||
/**
|
||||
* @zh 物品属性
|
||||
* @en Item properties
|
||||
*/
|
||||
properties?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 背包操作数据
|
||||
* @en Inventory operation data
|
||||
*/
|
||||
export interface InventoryOperationData {
|
||||
/**
|
||||
* @zh 操作类型
|
||||
* @en Operation type
|
||||
*/
|
||||
type: InventoryOperationType
|
||||
|
||||
/**
|
||||
* @zh 玩家 ID
|
||||
* @en Player ID
|
||||
*/
|
||||
playerId: string
|
||||
|
||||
/**
|
||||
* @zh 物品 ID
|
||||
* @en Item ID
|
||||
*/
|
||||
itemId: string
|
||||
|
||||
/**
|
||||
* @zh 数量
|
||||
* @en Quantity
|
||||
*/
|
||||
quantity: number
|
||||
|
||||
/**
|
||||
* @zh 物品属性(用于更新)
|
||||
* @en Item properties (for update)
|
||||
*/
|
||||
properties?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* @zh 原因/来源
|
||||
* @en Reason/source
|
||||
*/
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 背包操作结果
|
||||
* @en Inventory operation result
|
||||
*/
|
||||
export interface InventoryOperationResult {
|
||||
/**
|
||||
* @zh 操作前的物品数据
|
||||
* @en Item data before operation
|
||||
*/
|
||||
beforeItem?: ItemData
|
||||
|
||||
/**
|
||||
* @zh 操作后的物品数据
|
||||
* @en Item data after operation
|
||||
*/
|
||||
afterItem?: ItemData
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 背包数据提供者接口
|
||||
* @en Inventory data provider interface
|
||||
*/
|
||||
export interface IInventoryProvider {
|
||||
/**
|
||||
* @zh 获取物品
|
||||
* @en Get item
|
||||
*/
|
||||
getItem(playerId: string, itemId: string): Promise<ItemData | null>
|
||||
|
||||
/**
|
||||
* @zh 设置物品
|
||||
* @en Set item
|
||||
*/
|
||||
setItem(playerId: string, itemId: string, item: ItemData | null): Promise<void>
|
||||
|
||||
/**
|
||||
* @zh 检查背包容量
|
||||
* @en Check inventory capacity
|
||||
*/
|
||||
hasCapacity?(playerId: string, count: number): Promise<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 背包操作
|
||||
* @en Inventory operation
|
||||
*
|
||||
* @zh 用于处理物品的添加、移除和更新
|
||||
* @en Used for handling item addition, removal, and update
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 添加物品
|
||||
* tx.addOperation(new InventoryOperation({
|
||||
* type: 'add',
|
||||
* playerId: 'player1',
|
||||
* itemId: 'sword_001',
|
||||
* quantity: 1,
|
||||
* }))
|
||||
*
|
||||
* // 移除物品
|
||||
* tx.addOperation(new InventoryOperation({
|
||||
* type: 'remove',
|
||||
* playerId: 'player1',
|
||||
* itemId: 'potion_hp',
|
||||
* quantity: 5,
|
||||
* }))
|
||||
* ```
|
||||
*/
|
||||
export class InventoryOperation extends BaseOperation<InventoryOperationData, InventoryOperationResult> {
|
||||
readonly name = 'inventory'
|
||||
|
||||
private _provider: IInventoryProvider | null = null
|
||||
private _beforeItem: ItemData | null = null
|
||||
|
||||
/**
|
||||
* @zh 设置背包数据提供者
|
||||
* @en Set inventory data provider
|
||||
*/
|
||||
setProvider(provider: IInventoryProvider): this {
|
||||
this._provider = provider
|
||||
return this
|
||||
}
|
||||
|
||||
async validate(ctx: ITransactionContext): Promise<boolean> {
|
||||
const { type, quantity } = this.data
|
||||
|
||||
if (quantity <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (type === 'remove') {
|
||||
const item = await this._getItem(ctx)
|
||||
return item !== null && item.quantity >= quantity
|
||||
}
|
||||
|
||||
if (type === 'add' && this._provider?.hasCapacity) {
|
||||
return this._provider.hasCapacity(this.data.playerId, 1)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async execute(ctx: ITransactionContext): Promise<OperationResult<InventoryOperationResult>> {
|
||||
const { type, playerId, itemId, quantity, properties } = this.data
|
||||
|
||||
this._beforeItem = await this._getItem(ctx)
|
||||
|
||||
let afterItem: ItemData | null = null
|
||||
|
||||
switch (type) {
|
||||
case 'add': {
|
||||
if (this._beforeItem) {
|
||||
afterItem = {
|
||||
...this._beforeItem,
|
||||
quantity: this._beforeItem.quantity + quantity,
|
||||
}
|
||||
} else {
|
||||
afterItem = {
|
||||
itemId,
|
||||
quantity,
|
||||
properties,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
if (!this._beforeItem || this._beforeItem.quantity < quantity) {
|
||||
return this.failure('Insufficient item quantity', 'INSUFFICIENT_ITEM')
|
||||
}
|
||||
|
||||
const newQuantity = this._beforeItem.quantity - quantity
|
||||
if (newQuantity > 0) {
|
||||
afterItem = {
|
||||
...this._beforeItem,
|
||||
quantity: newQuantity,
|
||||
}
|
||||
} else {
|
||||
afterItem = null
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'update': {
|
||||
if (!this._beforeItem) {
|
||||
return this.failure('Item not found', 'ITEM_NOT_FOUND')
|
||||
}
|
||||
|
||||
afterItem = {
|
||||
...this._beforeItem,
|
||||
quantity: quantity > 0 ? quantity : this._beforeItem.quantity,
|
||||
properties: properties ?? this._beforeItem.properties,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await this._setItem(ctx, afterItem)
|
||||
|
||||
ctx.set(`inventory:${playerId}:${itemId}:before`, this._beforeItem)
|
||||
ctx.set(`inventory:${playerId}:${itemId}:after`, afterItem)
|
||||
|
||||
return this.success({
|
||||
beforeItem: this._beforeItem ?? undefined,
|
||||
afterItem: afterItem ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
async compensate(ctx: ITransactionContext): Promise<void> {
|
||||
await this._setItem(ctx, this._beforeItem)
|
||||
}
|
||||
|
||||
private async _getItem(ctx: ITransactionContext): Promise<ItemData | null> {
|
||||
const { playerId, itemId } = this.data
|
||||
|
||||
if (this._provider) {
|
||||
return this._provider.getItem(playerId, itemId)
|
||||
}
|
||||
|
||||
if (ctx.storage) {
|
||||
return ctx.storage.get<ItemData>(`player:${playerId}:inventory:${itemId}`)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private async _setItem(ctx: ITransactionContext, item: ItemData | null): Promise<void> {
|
||||
const { playerId, itemId } = this.data
|
||||
|
||||
if (this._provider) {
|
||||
await this._provider.setItem(playerId, itemId, item)
|
||||
return
|
||||
}
|
||||
|
||||
if (ctx.storage) {
|
||||
if (item) {
|
||||
await ctx.storage.set(`player:${playerId}:inventory:${itemId}`, item)
|
||||
} else {
|
||||
await ctx.storage.delete(`player:${playerId}:inventory:${itemId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建背包操作
|
||||
* @en Create inventory operation
|
||||
*/
|
||||
export function createInventoryOperation(data: InventoryOperationData): InventoryOperation {
|
||||
return new InventoryOperation(data)
|
||||
}
|
||||
331
packages/framework/transaction/src/operations/TradeOperation.ts
Normal file
331
packages/framework/transaction/src/operations/TradeOperation.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* @zh 交易操作
|
||||
* @en Trade operation
|
||||
*/
|
||||
|
||||
import type { ITransactionContext, OperationResult } from '../core/types.js'
|
||||
import { BaseOperation } from './BaseOperation.js'
|
||||
import { CurrencyOperation, type CurrencyOperationData, type ICurrencyProvider } from './CurrencyOperation.js'
|
||||
import { InventoryOperation, type InventoryOperationData, type IInventoryProvider, type ItemData } from './InventoryOperation.js'
|
||||
|
||||
/**
|
||||
* @zh 交易物品
|
||||
* @en Trade item
|
||||
*/
|
||||
export interface TradeItem {
|
||||
/**
|
||||
* @zh 物品 ID
|
||||
* @en Item ID
|
||||
*/
|
||||
itemId: string
|
||||
|
||||
/**
|
||||
* @zh 数量
|
||||
* @en Quantity
|
||||
*/
|
||||
quantity: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 交易货币
|
||||
* @en Trade currency
|
||||
*/
|
||||
export interface TradeCurrency {
|
||||
/**
|
||||
* @zh 货币类型
|
||||
* @en Currency type
|
||||
*/
|
||||
currency: string
|
||||
|
||||
/**
|
||||
* @zh 数量
|
||||
* @en Amount
|
||||
*/
|
||||
amount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 交易方数据
|
||||
* @en Trade party data
|
||||
*/
|
||||
export interface TradeParty {
|
||||
/**
|
||||
* @zh 玩家 ID
|
||||
* @en Player ID
|
||||
*/
|
||||
playerId: string
|
||||
|
||||
/**
|
||||
* @zh 给出的物品
|
||||
* @en Items to give
|
||||
*/
|
||||
items?: TradeItem[]
|
||||
|
||||
/**
|
||||
* @zh 给出的货币
|
||||
* @en Currencies to give
|
||||
*/
|
||||
currencies?: TradeCurrency[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 交易操作数据
|
||||
* @en Trade operation data
|
||||
*/
|
||||
export interface TradeOperationData {
|
||||
/**
|
||||
* @zh 交易 ID
|
||||
* @en Trade ID
|
||||
*/
|
||||
tradeId: string
|
||||
|
||||
/**
|
||||
* @zh 交易发起方
|
||||
* @en Trade initiator
|
||||
*/
|
||||
partyA: TradeParty
|
||||
|
||||
/**
|
||||
* @zh 交易接收方
|
||||
* @en Trade receiver
|
||||
*/
|
||||
partyB: TradeParty
|
||||
|
||||
/**
|
||||
* @zh 原因/备注
|
||||
* @en Reason/note
|
||||
*/
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 交易操作结果
|
||||
* @en Trade operation result
|
||||
*/
|
||||
export interface TradeOperationResult {
|
||||
/**
|
||||
* @zh 交易 ID
|
||||
* @en Trade ID
|
||||
*/
|
||||
tradeId: string
|
||||
|
||||
/**
|
||||
* @zh 交易是否成功
|
||||
* @en Whether trade succeeded
|
||||
*/
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 交易数据提供者
|
||||
* @en Trade data provider
|
||||
*/
|
||||
export interface ITradeProvider {
|
||||
currencyProvider?: ICurrencyProvider
|
||||
inventoryProvider?: IInventoryProvider
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 交易操作
|
||||
* @en Trade operation
|
||||
*
|
||||
* @zh 用于处理玩家之间的物品和货币交换
|
||||
* @en Used for handling item and currency exchange between players
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* tx.addOperation(new TradeOperation({
|
||||
* tradeId: 'trade_001',
|
||||
* partyA: {
|
||||
* playerId: 'player1',
|
||||
* items: [{ itemId: 'sword', quantity: 1 }],
|
||||
* },
|
||||
* partyB: {
|
||||
* playerId: 'player2',
|
||||
* currencies: [{ currency: 'gold', amount: 1000 }],
|
||||
* },
|
||||
* }))
|
||||
* ```
|
||||
*/
|
||||
export class TradeOperation extends BaseOperation<TradeOperationData, TradeOperationResult> {
|
||||
readonly name = 'trade'
|
||||
|
||||
private _provider: ITradeProvider | null = null
|
||||
private _subOperations: (CurrencyOperation | InventoryOperation)[] = []
|
||||
private _executedCount = 0
|
||||
|
||||
/**
|
||||
* @zh 设置交易数据提供者
|
||||
* @en Set trade data provider
|
||||
*/
|
||||
setProvider(provider: ITradeProvider): this {
|
||||
this._provider = provider
|
||||
return this
|
||||
}
|
||||
|
||||
async validate(ctx: ITransactionContext): Promise<boolean> {
|
||||
this._buildSubOperations()
|
||||
|
||||
for (const op of this._subOperations) {
|
||||
const isValid = await op.validate(ctx)
|
||||
if (!isValid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async execute(ctx: ITransactionContext): Promise<OperationResult<TradeOperationResult>> {
|
||||
this._buildSubOperations()
|
||||
this._executedCount = 0
|
||||
|
||||
try {
|
||||
for (const op of this._subOperations) {
|
||||
const result = await op.execute(ctx)
|
||||
if (!result.success) {
|
||||
await this._compensateExecuted(ctx)
|
||||
return this.failure(result.error ?? 'Trade operation failed', 'TRADE_FAILED')
|
||||
}
|
||||
this._executedCount++
|
||||
}
|
||||
|
||||
return this.success({
|
||||
tradeId: this.data.tradeId,
|
||||
completed: true,
|
||||
})
|
||||
} catch (error) {
|
||||
await this._compensateExecuted(ctx)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return this.failure(errorMessage, 'TRADE_ERROR')
|
||||
}
|
||||
}
|
||||
|
||||
async compensate(ctx: ITransactionContext): Promise<void> {
|
||||
await this._compensateExecuted(ctx)
|
||||
}
|
||||
|
||||
private _buildSubOperations(): void {
|
||||
if (this._subOperations.length > 0) return
|
||||
|
||||
const { partyA, partyB } = this.data
|
||||
|
||||
if (partyA.items) {
|
||||
for (const item of partyA.items) {
|
||||
const removeOp = new InventoryOperation({
|
||||
type: 'remove',
|
||||
playerId: partyA.playerId,
|
||||
itemId: item.itemId,
|
||||
quantity: item.quantity,
|
||||
reason: `trade:${this.data.tradeId}:give`,
|
||||
})
|
||||
const addOp = new InventoryOperation({
|
||||
type: 'add',
|
||||
playerId: partyB.playerId,
|
||||
itemId: item.itemId,
|
||||
quantity: item.quantity,
|
||||
reason: `trade:${this.data.tradeId}:receive`,
|
||||
})
|
||||
|
||||
if (this._provider?.inventoryProvider) {
|
||||
removeOp.setProvider(this._provider.inventoryProvider)
|
||||
addOp.setProvider(this._provider.inventoryProvider)
|
||||
}
|
||||
|
||||
this._subOperations.push(removeOp, addOp)
|
||||
}
|
||||
}
|
||||
|
||||
if (partyA.currencies) {
|
||||
for (const curr of partyA.currencies) {
|
||||
const deductOp = new CurrencyOperation({
|
||||
type: 'deduct',
|
||||
playerId: partyA.playerId,
|
||||
currency: curr.currency,
|
||||
amount: curr.amount,
|
||||
reason: `trade:${this.data.tradeId}:give`,
|
||||
})
|
||||
const addOp = new CurrencyOperation({
|
||||
type: 'add',
|
||||
playerId: partyB.playerId,
|
||||
currency: curr.currency,
|
||||
amount: curr.amount,
|
||||
reason: `trade:${this.data.tradeId}:receive`,
|
||||
})
|
||||
|
||||
if (this._provider?.currencyProvider) {
|
||||
deductOp.setProvider(this._provider.currencyProvider)
|
||||
addOp.setProvider(this._provider.currencyProvider)
|
||||
}
|
||||
|
||||
this._subOperations.push(deductOp, addOp)
|
||||
}
|
||||
}
|
||||
|
||||
if (partyB.items) {
|
||||
for (const item of partyB.items) {
|
||||
const removeOp = new InventoryOperation({
|
||||
type: 'remove',
|
||||
playerId: partyB.playerId,
|
||||
itemId: item.itemId,
|
||||
quantity: item.quantity,
|
||||
reason: `trade:${this.data.tradeId}:give`,
|
||||
})
|
||||
const addOp = new InventoryOperation({
|
||||
type: 'add',
|
||||
playerId: partyA.playerId,
|
||||
itemId: item.itemId,
|
||||
quantity: item.quantity,
|
||||
reason: `trade:${this.data.tradeId}:receive`,
|
||||
})
|
||||
|
||||
if (this._provider?.inventoryProvider) {
|
||||
removeOp.setProvider(this._provider.inventoryProvider)
|
||||
addOp.setProvider(this._provider.inventoryProvider)
|
||||
}
|
||||
|
||||
this._subOperations.push(removeOp, addOp)
|
||||
}
|
||||
}
|
||||
|
||||
if (partyB.currencies) {
|
||||
for (const curr of partyB.currencies) {
|
||||
const deductOp = new CurrencyOperation({
|
||||
type: 'deduct',
|
||||
playerId: partyB.playerId,
|
||||
currency: curr.currency,
|
||||
amount: curr.amount,
|
||||
reason: `trade:${this.data.tradeId}:give`,
|
||||
})
|
||||
const addOp = new CurrencyOperation({
|
||||
type: 'add',
|
||||
playerId: partyA.playerId,
|
||||
currency: curr.currency,
|
||||
amount: curr.amount,
|
||||
reason: `trade:${this.data.tradeId}:receive`,
|
||||
})
|
||||
|
||||
if (this._provider?.currencyProvider) {
|
||||
deductOp.setProvider(this._provider.currencyProvider)
|
||||
addOp.setProvider(this._provider.currencyProvider)
|
||||
}
|
||||
|
||||
this._subOperations.push(deductOp, addOp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _compensateExecuted(ctx: ITransactionContext): Promise<void> {
|
||||
for (let i = this._executedCount - 1; i >= 0; i--) {
|
||||
await this._subOperations[i].compensate(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建交易操作
|
||||
* @en Create trade operation
|
||||
*/
|
||||
export function createTradeOperation(data: TradeOperationData): TradeOperation {
|
||||
return new TradeOperation(data)
|
||||
}
|
||||
36
packages/framework/transaction/src/operations/index.ts
Normal file
36
packages/framework/transaction/src/operations/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @zh 操作模块导出
|
||||
* @en Operations module exports
|
||||
*/
|
||||
|
||||
export { BaseOperation } from './BaseOperation.js'
|
||||
|
||||
export {
|
||||
CurrencyOperation,
|
||||
createCurrencyOperation,
|
||||
type CurrencyOperationType,
|
||||
type CurrencyOperationData,
|
||||
type CurrencyOperationResult,
|
||||
type ICurrencyProvider,
|
||||
} from './CurrencyOperation.js'
|
||||
|
||||
export {
|
||||
InventoryOperation,
|
||||
createInventoryOperation,
|
||||
type InventoryOperationType,
|
||||
type InventoryOperationData,
|
||||
type InventoryOperationResult,
|
||||
type IInventoryProvider,
|
||||
type ItemData,
|
||||
} from './InventoryOperation.js'
|
||||
|
||||
export {
|
||||
TradeOperation,
|
||||
createTradeOperation,
|
||||
type TradeOperationData,
|
||||
type TradeOperationResult,
|
||||
type TradeItem,
|
||||
type TradeCurrency,
|
||||
type TradeParty,
|
||||
type ITradeProvider,
|
||||
} from './TradeOperation.js'
|
||||
Reference in New Issue
Block a user