feat(server): 添加节目配置文件管理

- 新增 programs.json 配置文件
- 新增 ProgramConfigService 服务
- 新增节目配置 API 接口 (GET/PUT /api/admin/programs)
- 修改 AdminService 使用配置服务替代硬编码
- 添加单元测试
This commit is contained in:
empty
2026-01-28 13:55:03 +08:00
parent a89d844f7b
commit 66ca67c137
6 changed files with 345 additions and 4 deletions

View File

@@ -0,0 +1,172 @@
/**
* Program Configuration Service
* Loads program config from JSON file and provides API for management
*/
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { logger } from '../utils/logger';
import type { VotingProgram } from '@gala/shared/types';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Default config path
const CONFIG_PATH = path.join(__dirname, '../../config/programs.json');
export interface ProgramConfig {
id: string;
name: string;
teamName: string;
order: number;
}
export interface ProgramSettings {
allowLateCatch: boolean;
maxVotesPerUser: number;
}
export interface ProgramConfigFile {
programs: ProgramConfig[];
settings: ProgramSettings;
}
class ProgramConfigService {
private config: ProgramConfigFile | null = null;
private configPath: string;
constructor() {
this.configPath = CONFIG_PATH;
}
/**
* Load config from file
*/
async load(): Promise<void> {
try {
if (fs.existsSync(this.configPath)) {
const content = fs.readFileSync(this.configPath, 'utf-8');
this.config = JSON.parse(content);
logger.info({
programCount: this.config?.programs.length,
configPath: this.configPath
}, 'Program config loaded');
} else {
logger.warn({ configPath: this.configPath }, 'Program config file not found, using defaults');
this.config = this.getDefaults();
}
} catch (error) {
logger.error({ error, configPath: this.configPath }, 'Failed to load program config');
this.config = this.getDefaults();
}
}
/**
* Get default configuration
*/
private getDefaults(): ProgramConfigFile {
return {
programs: [
{ id: 'p1', name: '龙腾四海', teamName: '市场部', order: 1 },
{ id: 'p2', name: '金马奔腾', teamName: '技术部', order: 2 },
{ id: 'p3', name: '春风得意', teamName: '人力资源部', order: 3 },
{ id: 'p4', name: '鸿运当头', teamName: '财务部', order: 4 },
{ id: 'p5', name: '马到成功', teamName: '运营部', order: 5 },
{ id: 'p6', name: '一马当先', teamName: '产品部', order: 6 },
{ id: 'p7', name: '万马奔腾', teamName: '设计部', order: 7 },
{ id: 'p8', name: '龙马精神', teamName: '销售部', order: 8 },
],
settings: {
allowLateCatch: true,
maxVotesPerUser: 7,
},
};
}
/**
* Get all programs (for display/listing)
*/
getPrograms(): ProgramConfig[] {
return this.config?.programs || this.getDefaults().programs;
}
/**
* Convert config programs to VotingProgram format (with runtime fields)
*/
getVotingPrograms(): VotingProgram[] {
const programs = this.getPrograms();
return programs.map(p => ({
...p,
status: 'pending' as const,
votes: 0,
stamps: [],
}));
}
/**
* Get program by id
*/
getProgramById(id: string): ProgramConfig | undefined {
return this.getPrograms().find(p => p.id === id);
}
/**
* Get settings
*/
getSettings(): ProgramSettings {
return this.config?.settings || this.getDefaults().settings;
}
/**
* Update programs and save to file
*/
async updatePrograms(programs: ProgramConfig[]): Promise<{ success: boolean; error?: string }> {
try {
if (!this.config) {
this.config = this.getDefaults();
}
this.config.programs = programs;
await this.saveToFile();
logger.info({ programCount: programs.length }, 'Programs updated');
return { success: true };
} catch (error) {
logger.error({ error }, 'Failed to update programs');
return { success: false, error: (error as Error).message };
}
}
/**
* Update settings and save to file
*/
async updateSettings(settings: Partial<ProgramSettings>): Promise<{ success: boolean; error?: string }> {
try {
if (!this.config) {
this.config = this.getDefaults();
}
this.config.settings = { ...this.config.settings, ...settings };
await this.saveToFile();
logger.info({ settings: this.config.settings }, 'Program settings updated');
return { success: true };
} catch (error) {
logger.error({ error }, 'Failed to update settings');
return { success: false, error: (error as Error).message };
}
}
/**
* Save config to file
*/
private async saveToFile(): Promise<void> {
const content = JSON.stringify(this.config, null, 2);
fs.writeFileSync(this.configPath, content, 'utf-8');
}
/**
* Get full config for API response
*/
getFullConfig(): ProgramConfigFile {
return this.config || this.getDefaults();
}
}
export const programConfigService = new ProgramConfigService();