2025-12-18 19:07:21 +08:00
|
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
|
|
import { query, run, get } from '../database';
|
|
|
|
|
|
|
|
|
|
|
|
export interface Question {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
content: string;
|
|
|
|
|
|
type: 'single' | 'multiple' | 'judgment' | 'text';
|
|
|
|
|
|
category: string;
|
|
|
|
|
|
options?: string[];
|
|
|
|
|
|
answer: string | string[];
|
2025-12-25 00:15:14 +08:00
|
|
|
|
analysis: string;
|
2025-12-18 19:07:21 +08:00
|
|
|
|
score: number;
|
|
|
|
|
|
createdAt: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface CreateQuestionData {
|
|
|
|
|
|
content: string;
|
|
|
|
|
|
type: 'single' | 'multiple' | 'judgment' | 'text';
|
|
|
|
|
|
category?: string;
|
|
|
|
|
|
options?: string[];
|
|
|
|
|
|
answer: string | string[];
|
2025-12-25 00:15:14 +08:00
|
|
|
|
analysis?: string;
|
2025-12-18 19:07:21 +08:00
|
|
|
|
score: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface ExcelQuestionData {
|
|
|
|
|
|
content: string;
|
|
|
|
|
|
type: string;
|
|
|
|
|
|
category?: string;
|
|
|
|
|
|
answer: string;
|
2025-12-25 00:15:14 +08:00
|
|
|
|
analysis?: string;
|
2025-12-18 19:07:21 +08:00
|
|
|
|
score: number;
|
|
|
|
|
|
options?: string[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class QuestionModel {
|
|
|
|
|
|
// 创建题目
|
|
|
|
|
|
static async create(data: CreateQuestionData): Promise<Question> {
|
|
|
|
|
|
const id = uuidv4();
|
|
|
|
|
|
const optionsStr = data.options ? JSON.stringify(data.options) : null;
|
|
|
|
|
|
const answerStr = Array.isArray(data.answer) ? JSON.stringify(data.answer) : data.answer;
|
|
|
|
|
|
const category = data.category && data.category.trim() ? data.category.trim() : '通用';
|
2025-12-25 00:15:14 +08:00
|
|
|
|
const analysis = String(data.analysis ?? '').trim().slice(0, 255);
|
2025-12-18 19:07:21 +08:00
|
|
|
|
|
|
|
|
|
|
const sql = `
|
2025-12-25 00:15:14 +08:00
|
|
|
|
INSERT INTO questions (id, content, type, options, answer, analysis, score, category)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
2025-12-18 19:07:21 +08:00
|
|
|
|
`;
|
|
|
|
|
|
|
2025-12-25 00:15:14 +08:00
|
|
|
|
await run(sql, [id, data.content, data.type, optionsStr, answerStr, analysis, data.score, category]);
|
2025-12-18 19:07:21 +08:00
|
|
|
|
return this.findById(id) as Promise<Question>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 00:58:58 +08:00
|
|
|
|
// 批量创建题目 - 优化为使用事务批量插入
|
2025-12-18 19:07:21 +08:00
|
|
|
|
static async createMany(questions: CreateQuestionData[]): Promise<{ success: number; errors: string[] }> {
|
|
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
let success = 0;
|
|
|
|
|
|
|
2025-12-19 00:58:58 +08:00
|
|
|
|
// 使用事务提高性能
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 开始事务
|
|
|
|
|
|
await run('BEGIN TRANSACTION');
|
|
|
|
|
|
|
|
|
|
|
|
const sql = `
|
2025-12-25 00:15:14 +08:00
|
|
|
|
INSERT INTO questions (id, content, type, options, answer, analysis, score, category)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
2025-12-19 00:58:58 +08:00
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < questions.length; i++) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const question = questions[i];
|
|
|
|
|
|
const id = uuidv4();
|
|
|
|
|
|
const optionsStr = question.options ? JSON.stringify(question.options) : null;
|
|
|
|
|
|
const answerStr = Array.isArray(question.answer) ? JSON.stringify(question.answer) : question.answer;
|
|
|
|
|
|
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
|
2025-12-25 00:15:14 +08:00
|
|
|
|
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
|
2025-12-19 00:58:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 直接执行插入,不调用单个create方法
|
2025-12-25 00:15:14 +08:00
|
|
|
|
await run(sql, [id, question.content, question.type, optionsStr, answerStr, analysis, question.score, category]);
|
2025-12-19 00:58:58 +08:00
|
|
|
|
success++;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
errors.push(`第${i + 1}题: ${error.message}`);
|
|
|
|
|
|
}
|
2025-12-18 19:07:21 +08:00
|
|
|
|
}
|
2025-12-19 00:58:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
|
await run('COMMIT');
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
// 回滚事务
|
|
|
|
|
|
await run('ROLLBACK');
|
|
|
|
|
|
errors.push(`事务错误: ${error.message}`);
|
2025-12-18 19:07:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { success, errors };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 00:15:14 +08:00
|
|
|
|
static async importFromText(
|
|
|
|
|
|
mode: 'overwrite' | 'incremental',
|
|
|
|
|
|
questions: CreateQuestionData[],
|
|
|
|
|
|
): Promise<{
|
|
|
|
|
|
inserted: number;
|
|
|
|
|
|
updated: number;
|
|
|
|
|
|
errors: string[];
|
|
|
|
|
|
cleared?: { questions: number; quizRecords: number; quizAnswers: number };
|
|
|
|
|
|
}> {
|
|
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
let inserted = 0;
|
|
|
|
|
|
let updated = 0;
|
|
|
|
|
|
let cleared: { questions: number; quizRecords: number; quizAnswers: number } | undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const insertSql = `
|
|
|
|
|
|
INSERT INTO questions (id, content, type, options, answer, analysis, score, category)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
await run('BEGIN TRANSACTION');
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (mode === 'overwrite') {
|
|
|
|
|
|
const [qCount, rCount, aCount] = await Promise.all([
|
|
|
|
|
|
get(`SELECT COUNT(*) as total FROM questions`),
|
|
|
|
|
|
get(`SELECT COUNT(*) as total FROM quiz_records`),
|
|
|
|
|
|
get(`SELECT COUNT(*) as total FROM quiz_answers`),
|
|
|
|
|
|
]);
|
|
|
|
|
|
cleared = { questions: qCount.total, quizRecords: rCount.total, quizAnswers: aCount.total };
|
|
|
|
|
|
|
|
|
|
|
|
await run(`DELETE FROM quiz_answers`);
|
|
|
|
|
|
await run(`DELETE FROM quiz_records`);
|
|
|
|
|
|
await run(`DELETE FROM questions`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < questions.length; i++) {
|
|
|
|
|
|
const question = questions[i];
|
|
|
|
|
|
try {
|
|
|
|
|
|
const optionsStr = question.options ? JSON.stringify(question.options) : null;
|
|
|
|
|
|
const answerStr = Array.isArray(question.answer) ? JSON.stringify(question.answer) : question.answer;
|
|
|
|
|
|
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
|
|
|
|
|
|
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
|
|
|
|
|
|
|
|
|
|
|
|
if (mode === 'incremental') {
|
|
|
|
|
|
const existing = await get(`SELECT id FROM questions WHERE content = ?`, [question.content]);
|
|
|
|
|
|
if (existing?.id) {
|
|
|
|
|
|
await run(
|
|
|
|
|
|
`UPDATE questions SET content = ?, type = ?, options = ?, answer = ?, analysis = ?, score = ?, category = ? WHERE id = ?`,
|
|
|
|
|
|
[question.content, question.type, optionsStr, answerStr, analysis, question.score, category, existing.id],
|
|
|
|
|
|
);
|
|
|
|
|
|
updated++;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const id = uuidv4();
|
|
|
|
|
|
await run(insertSql, [id, question.content, question.type, optionsStr, answerStr, analysis, question.score, category]);
|
|
|
|
|
|
inserted++;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
errors.push(`第${i + 1}题: ${error.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await run('COMMIT');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
await run('ROLLBACK');
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { inserted, updated, errors, cleared };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 19:07:21 +08:00
|
|
|
|
// 根据ID查找题目
|
|
|
|
|
|
static async findById(id: string): Promise<Question | null> {
|
|
|
|
|
|
const sql = `SELECT * FROM questions WHERE id = ?`;
|
|
|
|
|
|
const question = await get(sql, [id]);
|
|
|
|
|
|
|
|
|
|
|
|
if (!question) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return this.formatQuestion(question);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据题目内容查找题目
|
|
|
|
|
|
static async findByContent(content: string): Promise<Question | null> {
|
|
|
|
|
|
const sql = `SELECT * FROM questions WHERE content = ?`;
|
|
|
|
|
|
const question = await get(sql, [content]);
|
|
|
|
|
|
|
|
|
|
|
|
if (!question) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return this.formatQuestion(question);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取题目列表(支持筛选和分页)
|
|
|
|
|
|
static async findAll(filters: {
|
|
|
|
|
|
type?: string;
|
|
|
|
|
|
category?: string;
|
|
|
|
|
|
keyword?: string;
|
|
|
|
|
|
startDate?: string;
|
|
|
|
|
|
endDate?: string;
|
|
|
|
|
|
limit?: number;
|
|
|
|
|
|
offset?: number;
|
|
|
|
|
|
} = {}): Promise<{ questions: Question[]; total: number }> {
|
|
|
|
|
|
const { type, category, keyword, startDate, endDate, limit = 10, offset = 0 } = filters;
|
|
|
|
|
|
|
|
|
|
|
|
const whereParts: string[] = [];
|
|
|
|
|
|
const params: any[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (type) {
|
|
|
|
|
|
whereParts.push('type = ?');
|
|
|
|
|
|
params.push(type);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (category) {
|
|
|
|
|
|
whereParts.push('category = ?');
|
|
|
|
|
|
params.push(category);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (keyword) {
|
|
|
|
|
|
whereParts.push('content LIKE ?');
|
|
|
|
|
|
params.push(`%${keyword}%`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (startDate) {
|
|
|
|
|
|
whereParts.push('created_at >= ?');
|
|
|
|
|
|
params.push(`${startDate} 00:00:00`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (endDate) {
|
|
|
|
|
|
whereParts.push('created_at <= ?');
|
|
|
|
|
|
params.push(`${endDate} 23:59:59`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : '';
|
|
|
|
|
|
|
|
|
|
|
|
const questionsSql = `
|
|
|
|
|
|
SELECT * FROM questions
|
|
|
|
|
|
${whereClause}
|
|
|
|
|
|
ORDER BY created_at DESC
|
|
|
|
|
|
LIMIT ? OFFSET ?
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const countSql = `
|
|
|
|
|
|
SELECT COUNT(*) as total FROM questions ${whereClause}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const [questions, countResult] = await Promise.all([
|
|
|
|
|
|
query(questionsSql, [...params, limit, offset]),
|
|
|
|
|
|
get(countSql, params)
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
questions: questions.map((q) => this.formatQuestion(q)),
|
|
|
|
|
|
total: countResult.total
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 随机获取题目(按类型和数量)
|
|
|
|
|
|
static async getRandomQuestions(type: string, count: number, categories?: string[]): Promise<Question[]> {
|
|
|
|
|
|
const whereParts: string[] = ['type = ?'];
|
|
|
|
|
|
const params: any[] = [type];
|
|
|
|
|
|
|
|
|
|
|
|
if (categories && categories.length > 0) {
|
|
|
|
|
|
whereParts.push(`category IN (${categories.map(() => '?').join(',')})`);
|
|
|
|
|
|
params.push(...categories);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sql = `
|
|
|
|
|
|
SELECT * FROM questions
|
|
|
|
|
|
WHERE ${whereParts.join(' AND ')}
|
|
|
|
|
|
ORDER BY RANDOM()
|
|
|
|
|
|
LIMIT ?
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const questions = await query(sql, [...params, count]);
|
|
|
|
|
|
return questions.map((q) => this.formatQuestion(q));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新题目
|
|
|
|
|
|
static async update(id: string, data: Partial<CreateQuestionData>): Promise<Question> {
|
|
|
|
|
|
const fields: string[] = [];
|
|
|
|
|
|
const values: any[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (data.content) {
|
|
|
|
|
|
fields.push('content = ?');
|
|
|
|
|
|
values.push(data.content);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.type) {
|
|
|
|
|
|
fields.push('type = ?');
|
|
|
|
|
|
values.push(data.type);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.options !== undefined) {
|
|
|
|
|
|
fields.push('options = ?');
|
|
|
|
|
|
values.push(data.options ? JSON.stringify(data.options) : null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.answer !== undefined) {
|
|
|
|
|
|
const answerStr = Array.isArray(data.answer) ? JSON.stringify(data.answer) : data.answer;
|
|
|
|
|
|
fields.push('answer = ?');
|
|
|
|
|
|
values.push(answerStr);
|
|
|
|
|
|
}
|
2025-12-25 00:15:14 +08:00
|
|
|
|
|
|
|
|
|
|
if (data.analysis !== undefined) {
|
|
|
|
|
|
fields.push('analysis = ?');
|
|
|
|
|
|
values.push(String(data.analysis ?? '').trim().slice(0, 255));
|
|
|
|
|
|
}
|
2025-12-18 19:07:21 +08:00
|
|
|
|
|
|
|
|
|
|
if (data.score !== undefined) {
|
|
|
|
|
|
fields.push('score = ?');
|
|
|
|
|
|
values.push(data.score);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.category !== undefined) {
|
|
|
|
|
|
fields.push('category = ?');
|
|
|
|
|
|
values.push(data.category && data.category.trim() ? data.category.trim() : '通用');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fields.length === 0) {
|
|
|
|
|
|
throw new Error('没有要更新的字段');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
values.push(id);
|
|
|
|
|
|
const sql = `UPDATE questions SET ${fields.join(', ')} WHERE id = ?`;
|
|
|
|
|
|
|
|
|
|
|
|
await run(sql, values);
|
|
|
|
|
|
return this.findById(id) as Promise<Question>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除题目
|
|
|
|
|
|
static async delete(id: string): Promise<boolean> {
|
|
|
|
|
|
const sql = `DELETE FROM questions WHERE id = ?`;
|
|
|
|
|
|
const result = await run(sql, [id]);
|
|
|
|
|
|
return result.id !== undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化题目数据
|
|
|
|
|
|
private static formatQuestion(row: any): Question {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: row.id,
|
|
|
|
|
|
content: row.content,
|
|
|
|
|
|
type: row.type,
|
|
|
|
|
|
category: row.category || '通用',
|
|
|
|
|
|
options: row.options ? JSON.parse(row.options) : undefined,
|
|
|
|
|
|
answer: this.parseAnswer(row.answer, row.type),
|
2025-12-25 00:15:14 +08:00
|
|
|
|
analysis: String(row.analysis ?? ''),
|
2025-12-18 19:07:21 +08:00
|
|
|
|
score: row.score,
|
|
|
|
|
|
createdAt: row.created_at
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析答案
|
|
|
|
|
|
private static parseAnswer(answerStr: string, type: string): string | string[] {
|
|
|
|
|
|
if (type === 'multiple') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.parse(answerStr);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return answerStr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return answerStr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证题目数据
|
|
|
|
|
|
static validateQuestionData(data: CreateQuestionData): string[] {
|
|
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 验证题目内容
|
|
|
|
|
|
if (!data.content || data.content.trim().length === 0) {
|
|
|
|
|
|
errors.push('题目内容不能为空');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证题型
|
|
|
|
|
|
const validTypes = ['single', 'multiple', 'judgment', 'text'];
|
|
|
|
|
|
if (!validTypes.includes(data.type)) {
|
|
|
|
|
|
errors.push('题型必须是 single、multiple、judgment 或 text');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证选项
|
|
|
|
|
|
if (data.type === 'single' || data.type === 'multiple') {
|
|
|
|
|
|
if (!data.options || data.options.length < 2) {
|
|
|
|
|
|
errors.push('单选题和多选题必须至少包含2个选项');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证答案
|
|
|
|
|
|
if (!data.answer) {
|
|
|
|
|
|
errors.push('答案不能为空');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证分值
|
|
|
|
|
|
if (!data.score || data.score <= 0) {
|
|
|
|
|
|
errors.push('分值必须是正数');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.category !== undefined && data.category.trim().length === 0) {
|
|
|
|
|
|
errors.push('题目类别不能为空');
|
|
|
|
|
|
}
|
2025-12-25 00:15:14 +08:00
|
|
|
|
|
|
|
|
|
|
if (data.analysis !== undefined && String(data.analysis).length > 255) {
|
|
|
|
|
|
errors.push('解析长度不能超过255个字符');
|
|
|
|
|
|
}
|
2025-12-18 19:07:21 +08:00
|
|
|
|
|
|
|
|
|
|
return errors;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证Excel数据格式
|
|
|
|
|
|
static validateExcelData(data: ExcelQuestionData[]): { valid: boolean; errors: string[] } {
|
|
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
data.forEach((row, index) => {
|
|
|
|
|
|
if (!row.content) {
|
|
|
|
|
|
errors.push(`第${index + 1}行:题目内容不能为空`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const validTypes = ['single', 'multiple', 'judgment', 'text'];
|
|
|
|
|
|
if (!validTypes.includes(row.type)) {
|
|
|
|
|
|
errors.push(`第${index + 1}行:题型必须是 single、multiple、judgment 或 text`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!row.answer) {
|
|
|
|
|
|
errors.push(`第${index + 1}行:答案不能为空`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!row.score || row.score <= 0) {
|
|
|
|
|
|
errors.push(`第${index + 1}行:分值必须是正数`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
valid: errors.length === 0,
|
|
|
|
|
|
errors
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|