feat(sprint5): audit fixes — transactions, indexes, stat distribution, rest, forge cost
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 35s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 35s
P0 — Race conditions fixées avec pessimistic_write transactions : combat (double-spend endurance), forge (double upgrade), craft (consumeMaterials atomique), equip (item swap). Forge : coût or (50-1000) + endurance (15) ajouté. Combat : item stat bonuses (force/agilite/intelligence/chance) appliqués. P1 — Features manquantes : POST /api/characters/stats — distribution stat points (avec lock). POST /api/characters/rest — repos auberge (+50% HP, -20 endurance). Vitalité : +10 HP max par point distribué. P2 — Indexes DB ajoutés : character_id sur character_items, character_materials, combat_logs, craft_jobs, player_achievements, community_contributions. Composite (characterId, materialId) sur character_materials. period sur hall_of_fame. achievement_id sur player_achievements. P3 — Cleanup : @nestjs/jwt et pg retirés de package.json.
This commit is contained in:
@@ -4,16 +4,19 @@ import {
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { Character } from '../character/entities/character.entity';
|
||||
import { Material } from './material.entity';
|
||||
|
||||
@Entity('character_materials')
|
||||
@Index(['characterId', 'materialId'])
|
||||
export class CharacterMaterial {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'character_id' })
|
||||
@Index()
|
||||
characterId: string;
|
||||
|
||||
@ManyToOne(() => Character)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MoreThan, Repository } from 'typeorm';
|
||||
import { DataSource, MoreThan, Repository } from 'typeorm';
|
||||
import { Material } from './material.entity';
|
||||
import { CharacterMaterial } from './character-material.entity';
|
||||
import { Character } from '../character/entities/character.entity';
|
||||
@@ -15,6 +15,7 @@ export class MaterialService {
|
||||
private readonly charMatRepository: Repository<CharacterMaterial>,
|
||||
@InjectRepository(Character)
|
||||
private readonly characterRepository: Repository<Character>,
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
findAll() {
|
||||
@@ -39,18 +40,27 @@ export class MaterialService {
|
||||
return this.charMatRepository.save(entry);
|
||||
}
|
||||
|
||||
// Appelé par CraftService pour consommer les ingrédients
|
||||
// Appelé par CraftService — consommation atomique en transaction
|
||||
async consumeMaterials(characterId: string, ingredients: { materialId: string; quantity: number }[]): Promise<void> {
|
||||
for (const ing of ingredients) {
|
||||
const entry = await this.charMatRepository.findOne({
|
||||
where: { characterId, materialId: ing.materialId },
|
||||
});
|
||||
if (!entry || entry.quantity < ing.quantity) {
|
||||
throw new BadRequestException('Matériaux insuffisants pour ce craft');
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const charMatRepo = manager.getRepository(CharacterMaterial);
|
||||
|
||||
for (const ing of ingredients) {
|
||||
// SELECT ... FOR UPDATE — lock chaque entrée matériau
|
||||
const entry = await charMatRepo
|
||||
.createQueryBuilder('cm')
|
||||
.setLock('pessimistic_write')
|
||||
.where('cm.character_id = :characterId', { characterId })
|
||||
.andWhere('cm.material_id = :materialId', { materialId: ing.materialId })
|
||||
.getOne();
|
||||
|
||||
if (!entry || entry.quantity < ing.quantity) {
|
||||
throw new BadRequestException('Matériaux insuffisants pour ce craft');
|
||||
}
|
||||
entry.quantity -= ing.quantity;
|
||||
await charMatRepo.save(entry);
|
||||
}
|
||||
entry.quantity -= ing.quantity;
|
||||
await this.charMatRepository.save(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getCharacter(user: User): Promise<Character> {
|
||||
|
||||
Reference in New Issue
Block a user