Add NPC creation status tracking and progress reporting in Admin module
- Implemented getNPCsCreationStatus method in AdminController to retrieve the status of NPC creation jobs. - Enhanced AdminService to manage NPC creation jobs, including job ID generation, progress updates, and error handling. - Updated frontend CreateNPCView to display progress of NPC creation, including estimated time remaining and job status. - Added localization strings for progress reporting in both German and English. - Improved overall user experience by providing real-time feedback during NPC creation processes.
This commit is contained in:
@@ -45,6 +45,7 @@ class AdminController {
|
||||
this.deleteRegionDistance = this.deleteRegionDistance.bind(this);
|
||||
this.createNPCs = this.createNPCs.bind(this);
|
||||
this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this);
|
||||
this.getNPCsCreationStatus = this.getNPCsCreationStatus.bind(this);
|
||||
}
|
||||
|
||||
async getOpenInterests(req, res) {
|
||||
@@ -393,6 +394,7 @@ class AdminController {
|
||||
if (countValue < 1 || countValue > 500) {
|
||||
return res.status(400).json({ error: 'Count must be between 1 and 500' });
|
||||
}
|
||||
console.log('[createNPCs] Request received:', { userId, regionIds, minAge, maxAge, minTitleId, maxTitleId, count: countValue });
|
||||
const result = await AdminService.createNPCs(userId, {
|
||||
regionIds: regionIds && regionIds.length > 0 ? regionIds : null,
|
||||
minAge: parseInt(minAge) || 0,
|
||||
@@ -401,11 +403,13 @@ class AdminController {
|
||||
maxTitleId: parseInt(maxTitleId) || 19,
|
||||
count: countValue
|
||||
});
|
||||
console.log('[createNPCs] Job created:', result);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error('[createNPCs] Error:', error);
|
||||
console.error('[createNPCs] Error stack:', error.stack);
|
||||
const status = error.message === 'noaccess' ? 403 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
res.status(status).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +425,20 @@ class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
async getNPCsCreationStatus(req, res) {
|
||||
try {
|
||||
const { userid: userId } = req.headers;
|
||||
const { jobId } = req.params;
|
||||
const status = await AdminService.getNPCsCreationStatus(userId, jobId);
|
||||
res.status(200).json(status);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const status = error.message === 'noaccess' || error.message === 'Access denied' ? 403 :
|
||||
error.message === 'Job not found' ? 404 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getRoomTypes(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
|
||||
@@ -47,6 +47,7 @@ router.get('/falukant/region-distances', authenticate, adminController.getRegion
|
||||
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
||||
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
|
||||
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
|
||||
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
|
||||
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);
|
||||
|
||||
// --- Minigames Admin ---
|
||||
|
||||
@@ -26,6 +26,8 @@ import Room from '../models/chat/room.js';
|
||||
import UserParam from '../models/community/user_param.js';
|
||||
import TitleOfNobility from "../models/falukant/type/title_of_nobility.js";
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import npcCreationJobService from './npcCreationJobService.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class AdminService {
|
||||
async hasUserAccess(userId, section) {
|
||||
@@ -1113,7 +1115,7 @@ class AdminService {
|
||||
count // Anzahl der zu erstellenden NPCs
|
||||
} = options;
|
||||
|
||||
// Hole alle Städte, wenn keine spezifischen Regionen angegeben
|
||||
// Berechne zuerst die Gesamtanzahl, um den Job richtig zu initialisieren
|
||||
let targetRegions = [];
|
||||
if (regionIds && regionIds.length > 0) {
|
||||
targetRegions = await RegionData.findAll({
|
||||
@@ -1140,7 +1142,6 @@ class AdminService {
|
||||
throw new Error('No cities found');
|
||||
}
|
||||
|
||||
// Hole alle Titles im Bereich
|
||||
const titles = await TitleOfNobility.findAll({
|
||||
where: {
|
||||
id: {
|
||||
@@ -1154,13 +1155,56 @@ class AdminService {
|
||||
throw new Error('No titles found in specified range');
|
||||
}
|
||||
|
||||
const genders = ['male', 'female'];
|
||||
const createdNPCs = [];
|
||||
const totalNPCs = targetRegions.length * titles.length * count;
|
||||
|
||||
// Erstelle NPCs in einer Transaktion
|
||||
// Für jede Stadt-Titel-Kombination wird die angegebene Anzahl erstellt
|
||||
await sequelize.transaction(async (t) => {
|
||||
// Erstelle Job-ID
|
||||
const jobId = uuidv4();
|
||||
npcCreationJobService.createJob(userId, jobId);
|
||||
npcCreationJobService.updateProgress(jobId, 0, totalNPCs);
|
||||
npcCreationJobService.setStatus(jobId, 'running');
|
||||
|
||||
// Starte asynchronen Prozess
|
||||
this._createNPCsAsync(jobId, userId, {
|
||||
regionIds,
|
||||
minAge,
|
||||
maxAge,
|
||||
minTitleId,
|
||||
maxTitleId,
|
||||
count,
|
||||
targetRegions,
|
||||
titles
|
||||
}).catch(error => {
|
||||
console.error('Error in _createNPCsAsync:', error);
|
||||
const errorMessage = error?.message || error?.toString() || 'Unknown error occurred';
|
||||
npcCreationJobService.setError(jobId, errorMessage);
|
||||
});
|
||||
|
||||
return { jobId };
|
||||
}
|
||||
|
||||
async _createNPCsAsync(jobId, userId, options) {
|
||||
try {
|
||||
const {
|
||||
regionIds,
|
||||
minAge,
|
||||
maxAge,
|
||||
minTitleId,
|
||||
maxTitleId,
|
||||
count,
|
||||
targetRegions,
|
||||
titles
|
||||
} = options;
|
||||
|
||||
const genders = ['male', 'female'];
|
||||
const createdNPCs = [];
|
||||
const totalNPCs = targetRegions.length * titles.length * count;
|
||||
let currentNPC = 0;
|
||||
|
||||
console.log(`[NPC Creation Job ${jobId}] Starting creation of ${totalNPCs} NPCs`);
|
||||
|
||||
// Erstelle NPCs in einer Transaktion
|
||||
// Für jede Stadt-Titel-Kombination wird die angegebene Anzahl erstellt
|
||||
await sequelize.transaction(async (t) => {
|
||||
for (const region of targetRegions) {
|
||||
for (const title of titles) {
|
||||
// Erstelle 'count' NPCs für diese Stadt-Titel-Kombination
|
||||
@@ -1218,18 +1262,40 @@ class AdminService {
|
||||
region: region.name,
|
||||
title: title.labelTr
|
||||
});
|
||||
|
||||
// Update Progress
|
||||
currentNPC++;
|
||||
npcCreationJobService.updateProgress(jobId, currentNPC, totalNPCs);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
count: createdNPCs.length,
|
||||
countPerCombination: count,
|
||||
totalCombinations: targetRegions.length * titles.length,
|
||||
npcs: createdNPCs
|
||||
};
|
||||
console.log(`[NPC Creation Job ${jobId}] Completed: ${createdNPCs.length} NPCs created`);
|
||||
|
||||
// Job abschließen
|
||||
npcCreationJobService.setResult(jobId, {
|
||||
success: true,
|
||||
count: createdNPCs.length,
|
||||
countPerCombination: count,
|
||||
totalCombinations: targetRegions.length * titles.length,
|
||||
npcs: createdNPCs
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[NPC Creation Job ${jobId}] Error:`, error);
|
||||
throw error; // Re-throw für den catch-Block in createNPCs
|
||||
}
|
||||
}
|
||||
|
||||
getNPCsCreationStatus(userId, jobId) {
|
||||
const job = npcCreationJobService.getJob(jobId);
|
||||
if (!job) {
|
||||
throw new Error('Job not found');
|
||||
}
|
||||
if (job.userId !== userId) {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return job;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
86
backend/services/npcCreationJobService.js
Normal file
86
backend/services/npcCreationJobService.js
Normal file
@@ -0,0 +1,86 @@
|
||||
// In-Memory Job-Status-Service für NPC-Erstellung
|
||||
// Für Produktion sollte man Redis oder eine Datenbank verwenden
|
||||
|
||||
const jobs = new Map();
|
||||
|
||||
class NPCCreationJobService {
|
||||
createJob(userId, jobId) {
|
||||
jobs.set(jobId, {
|
||||
userId,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
total: 0,
|
||||
current: 0,
|
||||
startTime: Date.now(),
|
||||
estimatedTimeRemaining: null,
|
||||
error: null,
|
||||
result: null
|
||||
});
|
||||
return jobId;
|
||||
}
|
||||
|
||||
getJob(jobId) {
|
||||
return jobs.get(jobId);
|
||||
}
|
||||
|
||||
updateProgress(jobId, current, total) {
|
||||
const job = jobs.get(jobId);
|
||||
if (!job) return;
|
||||
|
||||
job.current = current;
|
||||
job.total = total;
|
||||
job.progress = total > 0 ? Math.round((current / total) * 100) : 0;
|
||||
|
||||
// Berechne verbleibende Zeit basierend auf bisheriger Geschwindigkeit
|
||||
if (current > 0 && job.progress < 100) {
|
||||
const elapsed = Date.now() - job.startTime;
|
||||
const avgTimePerItem = elapsed / current;
|
||||
const remaining = total - current;
|
||||
job.estimatedTimeRemaining = Math.round(remaining * avgTimePerItem);
|
||||
}
|
||||
}
|
||||
|
||||
setStatus(jobId, status) {
|
||||
const job = jobs.get(jobId);
|
||||
if (!job) return;
|
||||
job.status = status;
|
||||
}
|
||||
|
||||
setError(jobId, error) {
|
||||
const job = jobs.get(jobId);
|
||||
if (!job) return;
|
||||
job.status = 'error';
|
||||
job.error = error;
|
||||
}
|
||||
|
||||
setResult(jobId, result) {
|
||||
const job = jobs.get(jobId);
|
||||
if (!job) return;
|
||||
job.status = 'completed';
|
||||
job.result = result;
|
||||
job.progress = 100;
|
||||
job.estimatedTimeRemaining = 0;
|
||||
}
|
||||
|
||||
deleteJob(jobId) {
|
||||
jobs.delete(jobId);
|
||||
}
|
||||
|
||||
// Cleanup alte Jobs (älter als 1 Stunde)
|
||||
cleanupOldJobs() {
|
||||
const oneHourAgo = Date.now() - (60 * 60 * 1000);
|
||||
for (const [jobId, job] of jobs.entries()) {
|
||||
if (job.startTime < oneHourAgo) {
|
||||
jobs.delete(jobId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup alle 10 Minuten
|
||||
setInterval(() => {
|
||||
const service = new NPCCreationJobService();
|
||||
service.cleanupOldJobs();
|
||||
}, 10 * 60 * 1000);
|
||||
|
||||
export default new NPCCreationJobService();
|
||||
Reference in New Issue
Block a user