Commit 654a48d6 by chenjinjing

no message

parent d920324d
......@@ -4,7 +4,7 @@
<dbServer>http://127.0.0.1:13276</dbServer>
<mysqldb>
<!-- 本地mysql配置 -->
<mysqlHost>localhost</mysqlHost>
<mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>123456</mysqlPwd>
......
import { TABLENAME } from "../config/dbEnum";
import { STATE } from "../config/enum";
import { ERRORENUM } from "../config/errorEnum";
import { selectDataListByParam, selectDataWithCustomOrder } from "../data/findData";
import { BizError } from "../util/bizError";
// ==================== 辅助函数:安全取值 ====================
function getFieldValue(item: any, field: string): any {
if (!item) return undefined;
// Sequelize 实例
if (item.dataValues && item.dataValues[field] !== undefined) {
return item.dataValues[field];
}
// 普通对象
return item[field];
}
// ==================== 核心业务函数 ====================
export async function getStudentAbilityScoresWithComparison(student_id: string) {
if (!student_id) {
throw new BizError(ERRORENUM.参数错误, '学生ID不能为空');
}
// 获取已完成记录,按时间倒序
const allRecords = await selectDataWithCustomOrder(
TABLENAME.答题记录表,
{ student_id, answer_status: STATE. },
["record_id", "student_id", "student_name", "total_score", "answer_time", "answer_status"],
[["answer_time", "DESC"]]
);
if (!allRecords.data || allRecords.data.length === 0) {
return getEmptyResult(student_id);
}
const currentRecord = allRecords.data[0];
const previousRecord = allRecords.data.length >= 2 ? allRecords.data[1] : null;
const currentScores = await calculateDimensionScoresByRecord(getFieldValue(currentRecord, 'record_id'));
let previousScores = null;
if (previousRecord) {
previousScores = await calculateDimensionScoresByRecord(getFieldValue(previousRecord, 'record_id'));
}
return await buildAbilityResult(
student_id,
currentRecord,
currentScores,
previousRecord,
previousScores
);
}
/**
* 根据答题记录ID计算各维度得分
* 修正:使用 %in% 操作符 + 安全取值
*/
async function calculateDimensionScoresByRecord(record_id: string) {
// 1. 获取答题明细
const answerDetails = await selectDataListByParam(
TABLENAME.答题记录明细表,
{ record_id },
["detail_id", "score", "question_id"]
);
if (!answerDetails.data || answerDetails.data.length === 0) {
return {};
}
// 2. 收集所有 question_id(去重)
const questionIds = [...new Set(answerDetails.data.map(d => getFieldValue(d, 'question_id')))];
// 3. 批量查询题目(使用 %in%)
const questions = await selectDataListByParam(
TABLENAME.题目表,
{ question_id: { "%in%": questionIds } },
["question_id", "dimension_id"]
);
const questionDimensionMap = new Map();
questions.data.forEach((q: any) => {
const qid = getFieldValue(q, 'question_id');
const did = getFieldValue(q, 'dimension_id');
if (qid && did) questionDimensionMap.set(qid, did);
});
// 4. 收集 dimension_id 并查询维度名称
const dimensionIds = [...new Set(questions.data.map(q => getFieldValue(q, 'dimension_id')))];
const dimensions = await selectDataListByParam(
TABLENAME.维度表,
{ dimension_id: { "%in%": dimensionIds } },
["dimension_id", "dimension_name"]
);
const dimensionNameMap = new Map();
dimensions.data.forEach((dim: any) => {
const did = getFieldValue(dim, 'dimension_id');
const dname = getFieldValue(dim, 'dimension_name');
if (did && dname) dimensionNameMap.set(did, dname);
});
// 5. 初始化得分对象
const dimensionScores: { [key: string]: number } = {};
dimensions.data.forEach((dim: any) => {
const dname = getFieldValue(dim, 'dimension_name');
if (dname) dimensionScores[dname] = 0;
});
// 6. 累加得分
answerDetails.data.forEach((detail: any) => {
const qid = getFieldValue(detail, 'question_id');
const score = Number(getFieldValue(detail, 'score')) || 0;
const did = questionDimensionMap.get(qid);
if (did) {
const dname = dimensionNameMap.get(did);
if (dname && dimensionScores.hasOwnProperty(dname)) {
dimensionScores[dname] += score;
}
}
});
return dimensionScores;
}
// 根据维度得分计算三大方向得分
function calculateDirectionScoresFromDimensions(dimensionScores: { [key: string]: number }) {
return {
gong: (dimensionScores['家国情怀'] || 0) + (dimensionScores['国际视野'] || 0) + (dimensionScores['责任担当'] || 0),
zhi: (dimensionScores['学业扎实'] || 0) + (dimensionScores['勇于创新'] || 0) + (dimensionScores['善于学习'] || 0),
neng: (dimensionScores['健康生活'] || 0) + (dimensionScores['审美情趣'] || 0) + (dimensionScores['劳动意识'] || 0)
};
}
// 分数描述
function getScoreDescription(score: number): string {
if (score >= 85) return '优秀';
if (score >= 70) return '良好';
if (score >= 60) return '合格';
return '待提升';
}
// 默认狮子形象
function getDefaultLionImage() {
return {
standardName: '',
aliasName: '',
characteristic: '',
magicArtifact: '',
magicArtifactText: '',
suggestion: '',
suggestionText: '',
lionImage: ''
};
}
// 根据方向得分匹配狮子形象(复用 question.ts 逻辑)
async function getLionImageByScores(scores: { : number; : number; : number }) {
const { : publicScore, : intelligenceScore, : abilityScore } = scores;
const allLionImages = await selectDataListByParam(
TABLENAME.狮子形象表,
{},
[
'lion_id', 'standard_name', 'alias_name', 'characteristic',
'judgment_condition', 'magic_artifact', 'magic_artifact_text',
'suggestion', 'suggestion_text', 'lion_image'
]
);
if (!allLionImages.data || allLionImages.data.length === 0) {
return getDefaultLionImage();
}
for (const lion of allLionImages.data) {
const condition = lion.judgment_condition;
if (condition && evaluateCondition(condition, publicScore, intelligenceScore, abilityScore)) {
return {
standardName: lion.standard_name,
aliasName: lion.alias_name,
characteristic: lion.characteristic,
magicArtifact: lion.magic_artifact,
magicArtifactText: lion.magic_artifact_text,
suggestion: lion.suggestion,
suggestionText: lion.suggestion_text,
lionImage: lion.lion_image
};
}
}
return getDefaultLionImage();
}
function evaluateCondition(condition: string, publicScore: number, intelligenceScore: number, abilityScore: number): boolean {
const parts = condition.split('&').map(c => c.trim());
for (const part of parts) {
if (!evaluateSingleCondition(part, publicScore, intelligenceScore, abilityScore)) {
return false;
}
}
return true;
}
function evaluateSingleCondition(cond: string, publicScore: number, intelligenceScore: number, abilityScore: number): boolean {
const match = cond.match(/(公|智|能)([<>≥≤≈=]+)(\d+)/);
if (!match) return false;
const direction = match[1];
const operator = match[2];
const value = parseInt(match[3]);
let score: number;
switch (direction) {
case '公': score = publicScore; break;
case '智': score = intelligenceScore; break;
case '能': score = abilityScore; break;
default: return false;
}
switch (operator) {
case '>': return score > value;
case '<': return score < value;
case '≥': return score >= value;
case '>=': return score >= value;
case '≤': return score <= value;
case '<=': return score <= value;
case '≈': return Math.abs(score - value) <= 5;
case '=': return score === value;
default: return false;
}
}
// 构建最终返回结果
async function buildAbilityResult(
student_id: string,
currentRecord: any,
currentScores: { [key: string]: number },
previousRecord: any,
previousScores: { [key: string]: number } | null
) {
const abilitiesKeys = ['jgqh', 'gjsy', 'zrdd', 'xyzs', 'yycx', 'syxx', 'jksh', 'smqq', 'ldys'];
const abilitiesNames: Record<string, string> = {
jgqh: '家国情怀', gjsy: '国际视野', zrdd: '责任担当',
xyzs: '学业扎实', yycx: '勇于创新', syxx: '善于学习',
jksh: '健康生活', smqq: '审美情趣', ldys: '劳动意识'
};
const abilities: any = {};
for (const key of abilitiesKeys) {
const current = currentScores[abilitiesNames[key]] || 0;
const previous = previousScores ? (previousScores[abilitiesNames[key]] || 0) : null;
abilities[key] = {
name: abilitiesNames[key],
currentScore: current,
previousScore: previous,
yoyChange: previous !== null ? current - previous : null,
yoyPercent: (previous !== null && previous !== 0) ? parseFloat(((current - previous) / previous * 100).toFixed(2)) : null
};
}
const currentDir = calculateDirectionScoresFromDimensions(currentScores);
const previousDir = previousScores ? calculateDirectionScoresFromDimensions(previousScores) : null;
const directionScores = {
gong: { name: '公', currentScore: currentDir.gong, previousScore: previousDir?.gong || null, yoyChange: previousDir ? currentDir.gong - previousDir.gong : null, yoyPercent: (previousDir && previousDir.gong !== 0) ? parseFloat(((currentDir.gong - previousDir.gong) / previousDir.gong * 100).toFixed(2)) : null },
zhi: { name: '智', currentScore: currentDir.zhi, previousScore: previousDir?.zhi || null, yoyChange: previousDir ? currentDir.zhi - previousDir.zhi : null, yoyPercent: (previousDir && previousDir.zhi !== 0) ? parseFloat(((currentDir.zhi - previousDir.zhi) / previousDir.zhi * 100).toFixed(2)) : null },
neng: { name: '能', currentScore: currentDir.neng, previousScore: previousDir?.neng || null, yoyChange: previousDir ? currentDir.neng - previousDir.neng : null, yoyPercent: (previousDir && previousDir.neng !== 0) ? parseFloat(((currentDir.neng - previousDir.neng) / previousDir.neng * 100).toFixed(2)) : null }
};
const currentTotal = getFieldValue(currentRecord, 'total_score') || 0;
const previousTotal = previousRecord ? getFieldValue(previousRecord, 'total_score') : null;
const lionImageInfo = await getLionImageByScores({ : currentDir.gong, : currentDir.zhi, : currentDir.neng });
return {
student_id,
student_name: getFieldValue(currentRecord, 'student_name') || '',
hasData: true,
message: '获取成功',
currentAnswerTime: getFieldValue(currentRecord, 'answer_time'),
previousAnswerTime: previousRecord ? getFieldValue(previousRecord, 'answer_time') : null,
abilities,
directionScores: {
gong: { ...directionScores.gong, description: getScoreDescription(directionScores.gong.currentScore) },
zhi: { ...directionScores.zhi, description: getScoreDescription(directionScores.zhi.currentScore) },
neng: { ...directionScores.neng, description: getScoreDescription(directionScores.neng.currentScore) }
},
totalScore: {
currentScore: currentTotal,
previousScore: previousTotal,
yoyChange: previousTotal !== null ? currentTotal - previousTotal : null,
yoyPercent: (previousTotal !== null && previousTotal !== 0) ? parseFloat(((currentTotal - previousTotal) / previousTotal * 100).toFixed(2)) : null,
description: getScoreDescription(currentTotal)
},
lionImageInfo
};
}
function getEmptyResult(student_id: string) {
const defaultLion = getDefaultLionImage();
const abilitiesKeys = ['jgqh', 'gjsy', 'zrdd', 'xyzs', 'yycx', 'syxx', 'jksh', 'smqq', 'ldys'];
const abilitiesNames: Record<string, string> = {
jgqh: '家国情怀', gjsy: '国际视野', zrdd: '责任担当',
xyzs: '学业扎实', yycx: '勇于创新', syxx: '善于学习',
jksh: '健康生活', smqq: '审美情趣', ldys: '劳动意识'
};
const abilities: any = {};
for (const key of abilitiesKeys) {
abilities[key] = { name: abilitiesNames[key], currentScore: 0, previousScore: null, yoyChange: null, yoyPercent: null };
}
return {
student_id,
student_name: '',
hasData: false,
message: '该学生暂无答题记录',
currentAnswerTime: null,
previousAnswerTime: null,
abilities,
directionScores: {
gong: { name: '公', currentScore: 0, previousScore: null, yoyChange: null, yoyPercent: null, description: '待提升' },
zhi: { name: '智', currentScore: 0, previousScore: null, yoyChange: null, yoyPercent: null, description: '待提升' },
neng: { name: '能', currentScore: 0, previousScore: null, yoyChange: null, yoyPercent: null, description: '待提升' }
},
totalScore: { currentScore: 0, previousScore: null, yoyChange: null, yoyPercent: null, description: '待提升' },
lionImageInfo: defaultLion
};
}
// ==================== 简化版导出(可选) ====================
export async function getStudentAbilityScoresSimple(student_id: string) {
const result = await getStudentAbilityScoresWithComparison(student_id);
if (!result.hasData) {
return {
student_id,
hasData: false,
message: result.message,
currentAnswerTime: null,
previousAnswerTime: null,
currentScores: [0,0,0,0,0,0,0,0,0],
previousScores: null,
yoyChanges: null,
totalScore: result.totalScore,
directionScores: result.directionScores,
lionImageInfo: result.lionImageInfo
};
}
const currentScores = [
result.abilities.jgqh.currentScore,
result.abilities.gjsy.currentScore,
result.abilities.zrdd.currentScore,
result.abilities.xyzs.currentScore,
result.abilities.yycx.currentScore,
result.abilities.syxx.currentScore,
result.abilities.jksh.currentScore,
result.abilities.smqq.currentScore,
result.abilities.ldys.currentScore
];
const previousScores = result.abilities.jgqh.previousScore !== null ? [
result.abilities.jgqh.previousScore,
result.abilities.gjsy.previousScore,
result.abilities.zrdd.previousScore,
result.abilities.xyzs.previousScore,
result.abilities.yycx.previousScore,
result.abilities.syxx.previousScore,
result.abilities.jksh.previousScore,
result.abilities.smqq.previousScore,
result.abilities.ldys.previousScore
] : null;
const yoyChanges = result.abilities.jgqh.yoyChange !== null ? [
result.abilities.jgqh.yoyChange,
result.abilities.gjsy.yoyChange,
result.abilities.zrdd.yoyChange,
result.abilities.xyzs.yoyChange,
result.abilities.yycx.yoyChange,
result.abilities.syxx.yoyChange,
result.abilities.jksh.yoyChange,
result.abilities.smqq.yoyChange,
result.abilities.ldys.yoyChange
] : null;
return {
student_id,
student_name: result.student_name,
hasData: true,
message: result.message,
currentAnswerTime: result.currentAnswerTime,
previousAnswerTime: result.previousAnswerTime,
currentScores,
previousScores,
yoyChanges,
totalScore: result.totalScore,
directionScores: result.directionScores,
lionImageInfo: result.lionImageInfo
};
}
......@@ -20,7 +20,7 @@ async function lanuch() {
console.log('This indicates that the server is started successfully.');
backup();
// backup();
// 应用启动时初始化UAC集成
const xmlRpcServer = await initUACIntegration();
......@@ -29,6 +29,7 @@ async function lanuch() {
} else {
console.error('🔴 XML-RPC服务器启动失败');
}
}
lanuch();
......
......@@ -3,7 +3,9 @@
*/
import * as questionRouter from './question';
import * as throwMethodRouter from './throwMethod';
export function setRouter(httpServer){
questionRouter.setRouter(httpServer);
throwMethodRouter.setRouter(httpServer);
}
/**
* 公智能评价答题 - 能力同比分析路由
*/
import asyncHandler from 'express-async-handler';
import * as throwMethodBiz from '../biz/throwMethod';
import { checkUser } from '../middleware/user';
export function setRouter(httpServer) {
/**学生能力同比分析接口 */
// 获取学生九大能力得分及同比数据(完整版)
httpServer.post('/gzn/throwmethod/abilitycomparison', asyncHandler(getStudentAbilityComparison));
// 获取学生九大能力得分及同比数据(简化版,用于图表)
httpServer.post('/gzn/throwmethod/abilitycomparisonsimple', asyncHandler(getStudentAbilityComparisonSimple));
// 批量获取多个学生的能力同比数据
httpServer.post('/gzn/throwmethod/batchabilitycomparison', asyncHandler(getBatchStudentAbilityComparison));
}
/**
* 获取学生九大能力得分及同比数据(完整版)
* @param req - 请求对象
* @param res - 响应对象
*
* 请求参数:
* - student_id?: string - 学生ID(可选,不传则使用当前登录用户)
*/
async function getStudentAbilityComparison(req, res) {
const UserInfo = req.userInfo;
let { student_id } = req.body;
// 如果没有传student_id,使用当前登录用户的ID
const targetStudentId = student_id || UserInfo.studentId;
let result = await throwMethodBiz.getStudentAbilityScoresWithComparison(targetStudentId);
res.success(result);
}
/**
* 获取学生九大能力得分及同比数据(简化版,仅返回数值数组,用于图表展示)
* @param req - 请求对象
* @param res - 响应对象
*
* 请求参数:
* - student_id?: string - 学生ID(可选,不传则使用当前登录用户)
*/
async function getStudentAbilityComparisonSimple(req, res) {
const UserInfo = req.userInfo;
let { student_id } = req.body;
// 如果没有传student_id,使用当前登录用户的ID
const targetStudentId = student_id || UserInfo.studentId;
let result = await throwMethodBiz.getStudentAbilityScoresSimple(targetStudentId);
res.success(result);
}
/**
* 批量获取多个学生的九大能力得分及同比数据
* @param req - 请求对象
* @param res - 响应对象
*
* 请求参数:
* - student_ids: string[] - 学生ID数组(必填)
*/
async function getBatchStudentAbilityComparison(req, res) {
const UserInfo = req.userInfo;
let { student_ids } = req.body;
if (!student_ids || !Array.isArray(student_ids) || student_ids.length === 0) {
throw new Error('学生ID列表不能为空');
}
const results = [];
for (const student_id of student_ids) {
try {
const result = await throwMethodBiz.getStudentAbilityScoresWithComparison(student_id);
results.push(result);
} catch (error) {
results.push({
student_id,
hasData: false,
message: error.message || '获取数据失败'
});
}
}
res.success({
total: results.length,
list: results
});
}
......@@ -6,7 +6,11 @@
"rootDir":"./src",
"outDir":"./out",
"esModuleInterop": true,
// "strict": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
"types": ["node"]
},
"exclude": [
"node_modules",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment