Commit d920324d by chenjinjing

no message

parents
.idea
/out
/node_modules
/test
/public
/logs
/video
*.logs
*.zip
*.mp4
*.png
/nm_backups
/res
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"program": "${workspaceFolder}/src/main.ts",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}
\ No newline at end of file
{
"compile-hero.disable-compile-files-on-did-save-code": true
}
\ No newline at end of file
<?xml version="1.0" encoding="gb2312"?>
<!-- k12.cms.xml -->
<!--
<product> 的各项属性是对产品基本信息的描述:
name 属性为产品名称;
version 属性为产品的版本号,必须是两位小数:如:1.00、3.03、4.10……;
innerHost 属性为产品所在服务器的内部 IP 地址或网址;
outerPort 属性为与 innerHost 对应的 WEB 发布端口,默认值为 80(即 HTTP 的默认端口);
outerHost 属性为产品所在服务器的外部 IP 地址或网址;
outerPort 属性为与 outerHost 对应的 WEB 发布端口,默认值为 80(即 HTTP 的默认端口);
managePath 属性为产品后台管理页面相对于发布目录的路径。
-->
<product name="南模公智能问答" version="1.00" innerHost="10.98.240.51" innerPort="13276"
outerHost="10.98.240.51" outerPort="13276" managePath="/gzn/app/admin/">
<!--
当前产品(南模公智能问答 1.00)需要被UAC自动调用的 XMLRPC 接口函数:
host 属性为提供 XMLRPC 服务文件的主机地址,可使用域名,若与UAC在同一台服务器上,可以使用“localhost”;
port 属性为提供 XMLRPC 服务文件所使用的发布端口;
path 属性为提供 XMLRPC 服务的文件相对于发布目录所在的路径;
username 属性为 XMLRPC 服务验证的用户名;
password 属性为 XMLRPC 服务验证的密码;
<method> 的内容为 XMLRPC 方法名,只能填写UAC规定的方法名(具体的方法定义见后);
若无需UAC调用任何函数,可去掉 <xmlrpc> .... </xmlrpc> 部分
-->
<xmlrpc host="10.98.240.51" port="13277" path="/gzn/rpc/" username="K12RPC" password="K12RPCPwd">
<methods>
<method>user.addUser</method>
<method>user.updateUserCommon</method>
<method>user.updateUserPassword</method>
<method>product.getConfig</method>
<method>product.setConfig</method>
</methods>
</xmlrpc>
<!--
当前产品(南模公智能问答)包含的所有模块设置(UAC中会以模块为单位显示应用入口),每个模块对应一个 <module>:
name 属性为模块名称;
path 属性为产品模块相对于发布目录的路径;
logo 属性是产品模块使用的 LOGO 图片,图片放在“发布目录/platform/data/templates/images/logo/”目录下。
若当前产品为单一模块的产品,则 <modules> 中只需加入一个 <module>, 此 <module> 的 name 属性和产品名称相同。
-->
<modules>
<module name="南模公智能问答" path="/" logo="gzn.gif" />
</modules>
</product>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "screen",
"version": "1.0.0",
"description": "",
"main": "main.ts",
"dependencies": {
"@alicloud/sms-sdk": "^1.1.6",
"@types/node": "^10.12.18",
"bson": "^6.1.0",
"compression": "^1.7.4",
"connect-history-api-fallback": "^2.0.0",
"express": "^4.21.2",
"express-async-handler": "^1.1.4",
"express-history-api-fallback": "^2.2.1",
"formidable": "^1.2.1",
"iconv-lite": "^0.7.0",
"log4js": "^6.6.1",
"lru-cache": "^4.1.5",
"md5": "^2.2.1",
"moment": "^2.24.0",
"mongoose": "^7.6.0",
"mysql": "^2.18.1",
"mysql2": "^3.6.0",
"node-xlsx": "^0.16.1",
"nodemailer": "^6.1.1",
"qs": "^6.11.0",
"request": "^2.88.0",
"sequelize": "^6.32.1",
"ssh2": "^1.17.0",
"svg-captcha": "^1.3.12",
"tencentcloud-sdk-nodejs": "^4.0.562",
"winston": "^3.17.0",
"ws": "^5.2.2",
"xml2js": "^0.4.23",
"xmlrpc": "^1.3.2"
},
"devDependencies": {
"@types/express": "^5.0.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "cjj",
"license": "ISC",
"bin": "./out/main.js",
"pkg": {
"scripts": "out/**/*.js",
"assets": [
"public/**/*",
"res/**/*",
"images/**/*",
"video/**/*"
],
"outputPath": "dist"
}
}
<config>
<port>13276</port>
<sign></sign>
<dbServer>http://127.0.0.1:13276</dbServer>
<mysqldb>
<!-- 本地mysql配置 -->
<mysqlHost>localhost</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>123456</mysqlPwd>
<dataBase>publicIntelligence</dataBase>
<!-- 服务器mysql配置 -->
<!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>123456</mysqlPwd>
<dataBase>publicIntelligence</dataBase> -->
</mysqldb>
<!-- XML-RPC服务器配置 -->
<xmlRpcServer>
<enabled>true</enabled>
<port>13277</port>
<host>localhost</host>
<path>/gzn/rpc</path>
<auth>
<username>K12RPC</username>
<password>K12RPCPwd</password>
</auth>
</xmlRpcServer>
</config>
import { TABLENAME } from '../config/dbEnum';
import { systemConfig } from '../config/serverConfig';
import { addData } from '../data/addData';
import { delData } from '../data/delData';
import { selectDataListByParam, selectOneDataByParam } from '../data/findData';
import { updateManyData } from '../data/updateData';
/**
* UAC用户同步接口实现
*/
// 用户信息结构体类型定义
interface UACUserStruct {
user_id: string;
group_id: number;
user_name: string;
nickname?: string;
gender?: string;
email?: string;
user_type: string;
active_flag: number;
admin_flag: number;
create_date: string;
extend_info?: string;
person_id?: string;
id_type?: number;
id_card?: string;
name?: string;
verified_status?: number;
phone?: string;
birthday?: string;
password?: string;
forbidden_reason?: number;
xhjw_user_id?: number;
is_sx?: number;
order_id?: number;
student_user_id?: string;
is_train?: number;
zw_mark?: number;
eduid?: string;
uoid?: number;
user_mark?: string;
}
// 组信息结构体类型定义
interface UACGroupStruct {
group_id: number;
group_name: string;
parent_id: number;
thread_id: number;
group_flag: number;
sx_org_code?: string;
dec_grade_id?: number;
dec_classroom_id?: number;
group_code?: string;
group_mark?: string;
group_mflag?: number;
order_id?: number;
}
/**
* 新增用户接口 - 由UAC自动调用
*/
export async function userAddUser(userData: UACUserStruct): Promise<boolean> {
try {
console.log('🔵 UAC调用新增用户接口:', userData.user_id);
console.log('📦 用户数据:', JSON.stringify(userData, null, 2));
// 1. 检查用户是否已存在
const existingUser = await getUserFromDatabase(userData.user_id);
if (existingUser && Object.keys(existingUser).length > 0) { // 检查对象是否为空
console.log('🟡 用户已存在,执行更新操作:', userData.user_id);
return await userUpdateUserCommon(userData);
}
// 2. 插入新用户到数据库
const success = await insertUserToDatabase(userData);
if (success) {
console.log('🟢 新增用户成功:', userData.user_id);
return true;
} else {
console.error('🔴 新增用户失败:', userData.user_id);
return false;
}
} catch (error) {
console.error('🔴 新增用户接口异常:', error);
return false;
}
}
/**
* 更新用户基本信息接口 - 由UAC自动调用
*/
export async function userUpdateUserCommon(userData: UACUserStruct): Promise<boolean> {
try {
console.log('🔵 UAC调用更新用户接口:', userData.user_id);
console.log('📦 更新数据:', JSON.stringify(userData, null, 2));
// 更新用户信息到数据库
const success = await updateUserInDatabase(userData);
if (success) {
console.log('🟢 更新用户成功:', userData.user_id);
return true;
} else {
console.error('🔴 更新用户失败:', userData.user_id);
return false;
}
} catch (error) {
console.error('🔴 更新用户接口异常:', error);
return false;
}
}
/**
* 删除用户接口 - 由UAC自动调用
*/
export async function userDeleteUser(userIds: string): Promise<boolean> {
try {
console.log('🔵 UAC调用删除用户接口:', userIds);
// 参数已经是格式:'user1','user2','user3',可以直接用于SQL IN子句
// 但我们的delData函数可能需要数组,所以还是解析为数组
// 注意:文档说明参数已经用单引号括起来,所以这里需要去除单引号并分割
const userIdList = userIds.split(',').map(id => id.replace(/'/g, '').trim());
console.log('📋 要删除的用户ID列表:', userIdList);
// 批量删除用户
const success = await deleteUsersFromDatabase(userIdList);
if (success) {
console.log('🟢 删除用户成功:', userIds);
return true;
} else {
console.error('🔴 删除用户失败:', userIds);
return false;
}
} catch (error) {
console.error('🔴 删除用户接口异常:', error);
return false;
}
}
/**
* 同步用户基本信息接口 - 一次性复制UAC当前用户
*/
export async function userSyncUser(userData: UACUserStruct): Promise<boolean> {
try {
console.log('🔵 UAC调用同步用户接口:', userData.user_id);
console.log('📦 同步数据:', JSON.stringify(userData, null, 2));
// 检查用户是否已存在
const existingUser = await getUserFromDatabase(userData.user_id);
if (existingUser && Object.keys(existingUser).length > 0) {
console.log('🟡 用户存在,执行更新操作');
return await userUpdateUserCommon(userData);
} else {
console.log('🟡 用户不存在,执行新增操作');
return await userAddUser(userData);
}
} catch (error) {
console.error('🔴 同步用户接口异常:', error);
return false;
}
}
/**
* 新增组接口 - 由UAC自动调用
*/
// export async function groupAddGroup(groupData: UACGroupStruct): Promise<boolean> {
// try {
// console.log('🔵 UAC调用新增组接口:', groupData.group_id);
// console.log('📦 组数据:', JSON.stringify(groupData, null, 2));
// // 插入新组到数据库
// const success = await insertGroupToDatabase(groupData);
// if (success) {
// console.log('🟢 新增组成功:', groupData.group_id);
// return true;
// } else {
// console.error('🔴 新增组失败:', groupData.group_id);
// return false;
// }
// } catch (error) {
// console.error('🔴 新增组接口异常:', error);
// return false;
// }
// }
/**
* 更新组信息接口 - 由UAC自动调用
*/
// export async function groupUpdateGroup(groupData: UACGroupStruct): Promise<boolean> {
// try {
// console.log('🔵 UAC调用更新组接口:', groupData.group_id);
// console.log('📦 更新数据:', JSON.stringify(groupData, null, 2));
// // 更新组信息到数据库
// const success = await updateGroupInDatabase(groupData);
// if (success) {
// console.log('🟢 更新组成功:', groupData.group_id);
// return true;
// } else {
// console.error('🔴 更新组失败:', groupData.group_id);
// return false;
// }
// } catch (error) {
// console.error('🔴 更新组接口异常:', error);
// return false;
// }
// }
/**
* 删除组接口 - 由UAC自动调用
*/
// export async function groupDeleteGroup(groupId: number): Promise<boolean> {
// try {
// console.log('🔵 UAC调用删除组接口:', groupId);
// // 删除组
// const success = await deleteGroupFromDatabase(groupId);
// if (success) {
// console.log('🟢 删除组成功:', groupId);
// return true;
// } else {
// console.error('🔴 删除组失败:', groupId);
// return false;
// }
// } catch (error) {
// console.error('🔴 删除组接口异常:', error);
// return false;
// }
// }
/**
* 同步组数据接口 - 一次性复制UAC当前组
*/
// export async function groupSyncGroup(groupData: UACGroupStruct): Promise<boolean> {
// try {
// console.log('🔵 UAC调用同步组接口:', groupData.group_id);
// console.log('📦 同步数据:', JSON.stringify(groupData, null, 2));
// // 检查组是否已存在
// const existingGroup = await getGroupFromDatabase(groupData.group_id);
// if (existingGroup && existingGroup.length > 0) { // 检查数组是否为空
// console.log('🟡 组存在,执行更新操作');
// return await groupUpdateGroup(groupData);
// } else {
// console.log('🟡 组不存在,执行新增操作');
// return await groupAddGroup(groupData);
// }
// } catch (error) {
// console.error('🔴 同步组接口异常:', error);
// return false;
// }
// }
// 数据库操作函数
async function getUserFromDatabase(userId: string): Promise<any> {
try {
console.log('🔍 查询用户是否存在:', userId);
let result = await selectOneDataByParam(TABLENAME.统一用户表, {user_id: userId});
console.log('📊 查询结果:', result.data && Object.keys(result.data).length > 0 ? '用户存在' : '用户不存在');
return result.data; // 返回 data 字段而不是整个 result 对象
} catch (error) {
console.error('🔴 查询用户失败:', error);
return null;
}
}
async function insertUserToDatabase(userData: UACUserStruct): Promise<boolean> {
try {
console.log('💾 开始插入用户到数据库:', userData.user_id);
let addInfo = {
user_id: userData.user_id,
group_id: userData.group_id,
user_name: userData.user_name,
nickname: userData.nickname,
gender: userData.gender,
email: userData.email,
user_type: userData.user_type,
active_flag: userData.active_flag,
admin_flag: userData.admin_flag,
create_date: userData.create_date,
extend_info: userData.extend_info,
person_id: userData.person_id,
id_type: userData.id_type,
id_card: userData.id_card,
name: userData.name,
verified_status: userData.verified_status,
phone: userData.phone,
birthday: userData.birthday,
password: userData.password,
forbidden_reason: userData.forbidden_reason,
xhjw_user_id: userData.xhjw_user_id,
is_sx: userData.is_sx,
order_id: userData.order_id,
student_user_id: userData.student_user_id,
is_train: userData.is_train,
zw_mark: userData.zw_mark,
eduid: userData.eduid,
uoid: userData.uoid,
user_mark: userData.user_mark,
// permission: 0
}
await addData(TABLENAME.统一用户表, addInfo);
console.log('💾 插入用户成功:', userData.user_id);
return true;
} catch (error) {
console.error('🔴 插入用户到数据库失败:', error);
return false;
}
}
async function updateUserInDatabase(userData: UACUserStruct): Promise<boolean> {
try {
console.log('💾 开始更新用户数据:', userData.user_id);
let updateInfo = {
group_id: userData.group_id,
user_name: userData.user_name,
nickname: userData.nickname,
gender: userData.gender,
email: userData.email,
user_type: userData.user_type,
active_flag: userData.active_flag,
admin_flag: userData.admin_flag,
extend_info: userData.extend_info,
person_id: userData.person_id,
id_type: userData.id_type,
id_card: userData.id_card,
name: userData.name,
verified_status: userData.verified_status,
phone: userData.phone,
birthday: userData.birthday,
password: userData.password,
forbidden_reason: userData.forbidden_reason,
xhjw_user_id: userData.xhjw_user_id,
is_sx: userData.is_sx,
order_id: userData.order_id,
student_user_id: userData.student_user_id,
is_train: userData.is_train,
zw_mark: userData.zw_mark,
eduid: userData.eduid,
uoid: userData.uoid,
user_mark: userData.user_mark
};
console.log('📋 更新字段:', Object.keys(updateInfo).filter(key => updateInfo[key] !== undefined));
await updateManyData(TABLENAME.统一用户表, { user_id: userData.user_id }, updateInfo);
console.log('💾 更新用户成功:', userData.user_id);
return true;
} catch (error) {
console.error('🔴 更新用户到数据库失败:', error);
return false;
}
}
async function deleteUsersFromDatabase(userIds: string[]): Promise<boolean> {
try {
console.log('🗑️ 开始删除用户:', userIds);
// 注意:这里假设delData函数可以接受数组形式的user_id
await delData(TABLENAME.统一用户表, { user_id: userIds });
console.log('🗑️ 删除用户成功:', userIds);
return true;
} catch (error) {
console.error('🔴 从数据库删除用户失败:', error);
return false;
}
}
// async function getGroupFromDatabase(groupId: number): Promise<any> {
// try {
// console.log('🔍 查询组是否存在:', groupId);
// let result: any = await selectDataListByParam(TABLENAME.统一用户表, { group_id: groupId });
// console.log('📊 查询结果:', result && result.length > 0 ? `找到${result.length}个组` : '组不存在');
// return result; // 这里返回的是数组,逻辑保持不变
// } catch (error) {
// console.error('🔴 查询组信息失败:', error);
// return null;
// }
// }
// async function insertGroupToDatabase(groupData: UACGroupStruct): Promise<boolean> {
// try {
// console.log('💾 开始插入组到数据库:', groupData.group_id);
// await addData(TABLENAME.统一用户表, groupData);
// console.log('💾 插入组成功:', groupData.group_id);
// return true;
// } catch (error) {
// console.error('🔴 插入组到数据库失败:', error);
// return false;
// }
// }
// async function updateGroupInDatabase(groupData: UACGroupStruct): Promise<boolean> {
// try {
// console.log('💾 开始更新组数据:', groupData.group_id);
// await updateManyData(TABLENAME.统一用户表, { group_id: groupData.group_id }, groupData);
// console.log('💾 更新组成功:', groupData.group_id);
// return true;
// } catch (error) {
// console.error('🔴 更新组到数据库失败:', error);
// return false;
// }
// }
// async function deleteGroupFromDatabase(groupId: number): Promise<boolean> {
// try {
// console.log('🗑️ 开始删除组:', groupId);
// await delData(TABLENAME.统一用户表, { group_id: groupId });
// console.log('🗑️ 删除组成功:', groupId);
// return true;
// } catch (error) {
// console.error('🔴 从数据库删除组失败:', error);
// return false;
// }
// }
/**
* XML-RPC服务器设置
*/
export async function setupXmlRpcServer() {
if (!systemConfig.xmlRpcServer.enabled) {
console.log('⚪ XML-RPC服务器未启用');
return null;
}
const xmlrpc = require('xmlrpc');
const config = systemConfig.xmlRpcServer;
console.log('🚀 正在创建XML-RPC服务器,端口:', config.port);
try {
// 创建XML-RPC服务器 - 使用正确的配置
const server = xmlrpc.createServer({
host: config.host || '0.0.0.0',
port: config.port,
encoding: 'GBK'
}, () => {
console.log('🟢 XML-RPC服务器启动成功:', {
host: config.host,
port: config.port,
encoding: 'GBK'
});
});
// 添加方法处理
server.on('NotFound', (method, params) => {
console.log('❌ 方法未找到:', method);
});
// 修改方法注册 - 正确处理XML-RPC的参数顺序
const methods = {
'user.addUser': safeHandleUserAddUser,
'user.updateUserCommon': safeHandleUserUpdateUserCommon,
'user.deleteUser': safeHandleUserDeleteUser,
'user.syncUser': safeHandleUserSyncUser,
// 'group.addGroup': safeHandleGroupAddGroup,
// 'group.updateGroup': safeHandleGroupUpdateGroup,
// 'group.deleteGroup': safeHandleGroupDeleteGroup,
// 'group.syncGroup': safeHandleGroupSyncGroup,
'system.listMethods': safeHandleSystemListMethods
};
Object.entries(methods).forEach(([methodName, handler]) => {
server.on(methodName, (error, params, callback) => {
console.log('📨 收到XML-RPC调用:', methodName);
// XML-RPC库传递的参数顺序是 (error, params, callback)
// 我们需要调整为 (params, callback)
if (typeof callback === 'function') {
// 如果有错误,直接返回错误
if (error) {
console.error('❌ XML-RPC调用错误:', error);
callback(error, 0);
return;
}
// 调用处理函数,传递正确的参数顺序
handler(params, callback);
} else {
// 如果参数顺序不对,尝试重新解析
console.log('⚠️ 检测到参数顺序问题,尝试重新解析...');
// 可能是 (params, callback) 顺序,尝试调整
const actualParams = error; // 第一个参数实际上是 params
const actualCallback = params; // 第二个参数实际上是 callback
if (typeof actualCallback === 'function') {
handler(actualParams, actualCallback);
} else {
console.error('❌ 无法解析参数顺序');
if (typeof actualCallback === 'function') {
actualCallback(new Error('Invalid parameter order'), 0);
}
}
}
});
});
return server;
} catch (error) {
console.error('🔴 创建XML-RPC服务器失败:', error);
return null;
}
}
// 添加安全包装的处理函数
async function safeHandleUserSyncUser(params: any[], callback: Function) {
try {
console.log('🔵 安全处理 user.syncUser');
// 添加参数验证
if (!params || !Array.isArray(params) || params.length === 0) {
console.error('❌ 参数错误: params 为空或不是数组');
if (typeof callback === 'function') {
callback(new Error('Invalid parameters: params is empty or not an array'), false);
}
return;
}
const userData = params[0] as UACUserStruct;
console.log('📦 用户数据:', JSON.stringify(userData, null, 2));
const result = await userSyncUser(userData);
// console.log('📤 响应结果:', result);
console.log('📤 响应结果:', result ? 1 : 0);
if (typeof callback === 'function') {
callback(null, result); // 改为直接返回布尔值
}
} catch (error) {
console.error('🔴 user.syncUser处理错误:', error);
if (typeof callback === 'function') {
callback(error, false);
}
}
}
async function safeHandleUserAddUser(params: any[], callback: Function) {
try {
console.log('🔵 安全处理 user.addUser');
// 添加参数验证
if (!params || !Array.isArray(params) || params.length === 0) {
console.error('❌ 参数错误: params 为空或不是数组');
if (typeof callback === 'function') {
callback(new Error('Invalid parameters: params is empty or not an array'), 0);
}
return;
}
const userData = params[0] as UACUserStruct;
console.log('📦 用户数据:', JSON.stringify(userData, null, 2));
const result = await userAddUser(userData);
console.log('📤 响应结果:', result ? 1 : 0);
if (typeof callback === 'function') {
callback(null, result);
}
} catch (error) {
console.error('🔴 user.addUser处理错误:', error);
if (typeof callback === 'function') {
callback(error, false);
}
}
}
async function safeHandleUserUpdateUserCommon(params: any[], callback: Function) {
try {
console.log('🔵 安全处理 user.updateUserCommon');
// 添加参数验证
if (!params || !Array.isArray(params) || params.length === 0) {
console.error('❌ 参数错误: params 为空或不是数组');
if (typeof callback === 'function') {
callback(new Error('Invalid parameters: params is empty or not an array'), 0);
}
return;
}
const userData = params[0] as UACUserStruct;
console.log('📦 更新数据:', JSON.stringify(userData, null, 2));
const result = await userUpdateUserCommon(userData);
console.log('📤 响应结果:', result ? 1 : 0);
if (typeof callback === 'function') {
callback(null, result);
}
} catch (error) {
console.error('🔴 user.updateUserCommon处理错误:', error);
if (typeof callback === 'function') {
callback(error, false);
}
}
}
async function safeHandleUserDeleteUser(params: any[], callback: Function) {
try {
console.log('🔵 安全处理 user.deleteUser');
// 添加参数验证
if (!params || !Array.isArray(params) || params.length === 0) {
console.error('❌ 参数错误: params 为空或不是数组');
if (typeof callback === 'function') {
callback(new Error('Invalid parameters: params is empty or not an array'), 0);
}
return;
}
const userIds = params[0] as string;
console.log('📋 要删除的用户ID:', userIds);
const result = await userDeleteUser(userIds);
console.log('📤 响应结果:', result ? 1 : 0);
if (typeof callback === 'function') {
callback(null, result);
}
} catch (error) {
console.error('🔴 user.deleteUser处理错误:', error);
if (typeof callback === 'function') {
callback(error, false);
}
}
}
// async function safeHandleGroupAddGroup(params: any[], callback: Function) {
// try {
// console.log('🔵 安全处理 group.addGroup');
// // 添加参数验证
// if (!params || !Array.isArray(params) || params.length === 0) {
// console.error('❌ 参数错误: params 为空或不是数组');
// if (typeof callback === 'function') {
// callback(new Error('Invalid parameters: params is empty or not an array'), 0);
// }
// return;
// }
// const groupData = params[0] as UACGroupStruct;
// console.log('📦 组数据:', JSON.stringify(groupData, null, 2));
// const result = await groupAddGroup(groupData);
// console.log('📤 响应结果:', result ? 1 : 0);
// if (typeof callback === 'function') {
// callback(null, result);
// }
// } catch (error) {
// console.error('🔴 group.addGroup处理错误:', error);
// if (typeof callback === 'function') {
// callback(error, 0);
// }
// }
// }
// async function safeHandleGroupUpdateGroup(params: any[], callback: Function) {
// try {
// console.log('🔵 安全处理 group.updateGroup');
// // 添加参数验证
// if (!params || !Array.isArray(params) || params.length === 0) {
// console.error('❌ 参数错误: params 为空或不是数组');
// if (typeof callback === 'function') {
// callback(new Error('Invalid parameters: params is empty or not an array'), 0);
// }
// return;
// }
// const groupData = params[0] as UACGroupStruct;
// console.log('📦 更新数据:', JSON.stringify(groupData, null, 2));
// const result = await groupUpdateGroup(groupData);
// console.log('📤 响应结果:', result ? 1 : 0);
// if (typeof callback === 'function') {
// callback(null, result);
// }
// } catch (error) {
// console.error('🔴 group.updateGroup处理错误:', error);
// if (typeof callback === 'function') {
// callback(error, 0);
// }
// }
// }
// async function safeHandleGroupDeleteGroup(params: any[], callback: Function) {
// try {
// console.log('🔵 安全处理 group.deleteGroup');
// // 添加参数验证
// if (!params || !Array.isArray(params) || params.length === 0) {
// console.error('❌ 参数错误: params 为空或不是数组');
// if (typeof callback === 'function') {
// callback(new Error('Invalid parameters: params is empty or not an array'), 0);
// }
// return;
// }
// const groupId = params[0] as number;
// console.log('📋 要删除的组ID:', groupId);
// const result = await groupDeleteGroup(groupId);
// console.log('📤 响应结果:', result ? 1 : 0);
// if (typeof callback === 'function') {
// callback(null, result);
// }
// } catch (error) {
// console.error('🔴 group.deleteGroup处理错误:', error);
// if (typeof callback === 'function') {
// callback(error, 0);
// }
// }
// }
// async function safeHandleGroupSyncGroup(params: any[], callback: Function) {
// try {
// console.log('🔵 安全处理 group.syncGroup');
// // 添加参数验证
// if (!params || !Array.isArray(params) || params.length === 0) {
// console.error('❌ 参数错误: params 为空或不是数组');
// if (typeof callback === 'function') {
// callback(new Error('Invalid parameters: params is empty or not an array'), 0);
// }
// return;
// }
// const groupData = params[0] as UACGroupStruct;
// console.log('📦 同步数据:', JSON.stringify(groupData, null, 2));
// const result = await groupSyncGroup(groupData);
// console.log('📤 响应结果:', result ? 1 : 0);
// if (typeof callback === 'function') {
// callback(null, result);
// }
// } catch (error) {
// console.error('🔴 group.syncGroup处理错误:', error);
// if (typeof callback === 'function') {
// callback(error, 0);
// }
// }
// }
async function safeHandleSystemListMethods(params: any[], callback: Function) {
try {
console.log('🔵 安全处理 system.listMethods');
// 添加参数验证(system.listMethods 可能没有参数,所以这里只检查 callback)
if (typeof callback !== 'function') {
console.error('❌ 参数错误: callback 不是函数');
return;
}
const methods = [
'user.addUser', 'user.updateUserCommon', 'user.deleteUser', 'user.syncUser',
'group.addGroup', 'group.updateGroup', 'group.deleteGroup', 'group.syncGroup'
];
callback(null, methods);
} catch (error) {
console.error('🔴 system.listMethods处理错误:', error);
if (typeof callback === 'function') {
callback(error);
}
}
}
/**
* 初始化UAC集成
*/
export async function initUACIntegration() {
if (!systemConfig.xmlRpcServer.enabled) {
console.log('⚪ UAC用户自动同步功能未启用');
return null;
}
console.log('🚀 正在启动XML-RPC服务器,端口:', systemConfig.xmlRpcServer.port);
console.log('📋 配置详情:', JSON.stringify(systemConfig.xmlRpcServer, null, 2));
try {
// 启动XML-RPC服务器
const xmlRpcServer = await setupXmlRpcServer();
if (xmlRpcServer) {
console.log('🟢 XML-RPC服务器启动成功,端口:', systemConfig.xmlRpcServer.port);
// 添加服务器事件监听
xmlRpcServer.on('error', (err) => {
console.error('🔴 XML-RPC服务器错误:', err);
});
return xmlRpcServer;
} else {
console.error('🔴 XML-RPC服务器启动失败');
return null;
}
} catch (error) {
console.error('🔴 XML-RPC服务器启动异常:', error);
return null;
}
}
import { Buffer } from "buffer";
import { AUTHENTICATIONTYPEENNUM, TYPEENUM } from "../config/enum";
import { ERRORENUM } from "../config/errorEnum";
import { checkStrLeng, checkType } from "../tools/eccParam";
import { convertEncoding, fixDoubleEncoding, hasGarbledCharacters, parseUserInfoXml, parseUserTypeEnhanced } from "../tools/parseXml";
import { BizError } from "../util/bizError";
import * as iconv from 'iconv-lite';
import { getMySqlMs, getUserToken, randomId } from "../tools/systemTools";
import { selectOneDataByParam } from "../data/findData";
import { TABLEID, TABLENAME } from "../config/dbEnum";
import { addData } from "../data/addData";
import { updateManyData } from "../data/updateData";
var xmlrpc = require('xmlrpc');
const https = require('https');
const dns = require('dns');
const { parseString } = require('xml2js');
// XML-RPC认证信息
const XML_RPC_USER = 'K12RPC';
const XML_RPC_PASS = 'K12RPC!Pwd1901';
// 生成Base64认证头
const AUTH_BASE64 = Buffer.from(`${XML_RPC_USER}:${XML_RPC_PASS}`).toString('base64');
/**
* 网络诊断函数
*/
async function networkDiagnosis(): Promise<void> {
console.log('=== 开始网络诊断 ===');
const targetHost = 'platform.nmzx.xhedu.sh.cn';
const targetPort = 443;
try {
// 1. DNS解析测试
console.log('1. DNS解析测试...');
const addresses = await dns.promises.resolve4(targetHost);
console.log('DNS解析结果:', addresses);
// 2. 端口连通性测试
console.log('2. 端口连通性测试...');
await new Promise((resolve, reject) => {
const socket = require('net').createConnection(targetPort, targetHost, () => {
console.log('端口连通性: 成功');
socket.end();
resolve(true);
});
socket.setTimeout(5000);
socket.on('timeout', () => {
console.log('端口连通性: 超时');
socket.destroy();
reject(new Error('连接超时'));
});
socket.on('error', (error) => {
console.log('端口连通性: 失败', error.message);
reject(error);
});
});
// 3. HTTPS请求测试
console.log('3. HTTPS请求测试...');
const testResult = await testHTTPSRequest();
console.log('HTTPS测试结果:', testResult);
} catch (error) {
console.error('网络诊断失败:', error.message);
throw error;
}
}
/**
* 简单的HTTPS请求测试
*/
async function testHTTPSRequest(): Promise<boolean> {
return new Promise((resolve, reject) => {
const options = {
hostname: 'platform.nmzx.xhedu.sh.cn',
port: 443,
path: '/',
method: 'GET',
timeout: 5000,
rejectUnauthorized: false,
headers: {
'Authorization': `Basic ${AUTH_BASE64}`
}
};
const req = https.request(options, (res) => {
console.log('HTTPS测试状态码:', res.statusCode);
resolve(res.statusCode === 200 || res.statusCode === 404);
});
req.on('error', (error) => {
console.error('HTTPS测试错误:', error.message);
reject(error);
});
req.on('timeout', () => {
console.error('HTTPS测试超时');
req.destroy();
reject(new Error('请求超时'));
});
req.end();
});
}
/**
* 创建全局客户端实例(单例模式)
*/
const createUACClient = () => {
const clientConfig = {
host: process.env.UAC_HOST || 'platform.nmzx.xhedu.sh.cn',
port: parseInt(process.env.UAC_PORT || '443'),
path: '/platform/rpc/index.php',
basic_auth: {
user: XML_RPC_USER,
pass: XML_RPC_PASS
},
// encoding: 'GBK',
headers: {
'Content-Type': 'text/xml',
'User-Agent': 'Node.js XML-RPC Client',
'Authorization': `Basic ${AUTH_BASE64}`
}
};
console.log('创建UAC客户端,使用认证用户:', XML_RPC_USER);
return xmlrpc.createSecureClient(clientConfig);
};
let uacClient: any = null;
/**
* 校验session
* @param req
* @param res
*/
export async function checkSession(req, res) {
try {
// 先进行网络诊断
await networkDiagnosis();
const sessid = req.query.sessid ||
req.headers['x-session-id'] ||
req.body.sessid;
console.log(sessid);
if (!checkType(sessid, TYPEENUM.string)) {
throw new BizError(ERRORENUM.参数错误);
}
if (!checkStrLeng(sessid)) {
throw new BizError(ERRORENUM.请求参数错误);
}
console.log('开始验证会话:', sessid.substring(0, 10) + '...');
console.log('使用XML-RPC认证用户:', XML_RPC_USER);
// 使用手动请求方法
const userId = await k12CallWithManualRequest(sessid);
if (!userId) {
throw new BizError(ERRORENUM.您的登录已失效);
}
// 使用新的getUserInfoManual函数
let userInfo = await getUserInfoManual(userId);
// 添加空值检查
if (!userInfo) {
throw new BizError(ERRORENUM.获取用户信息失败);
}
let permission = 0;
let userType:any = parseUserTypeEnhanced(userInfo.user_type);
let type = AUTHENTICATIONTYPEENNUM.一般注册用户;
if (userType.学生 == 1) {
type = AUTHENTICATIONTYPEENNUM.学生;
permission = 0;
} else if (userType.教职员工 == 1) {
permission = 1;
}
//单点登录成功,将用户写入系统用户表
let timestamp = getMySqlMs();
let token = getUserToken(userId + timestamp);
let userDbData = await selectOneDataByParam(TABLENAME.管理后台用户, {loginId:userId}, ["loginId", "permission"]);
if (!Object.keys(userDbData.data).length) {
//系统用户表没有该账号,新建一个
let addUInfo = {
aId:randomId(TABLEID.管理后台用户),
loginId: userId,
permission,
token,
tokenMs:timestamp
};
await addData(TABLENAME.管理后台用户, addUInfo);
} else {
let updateUInfo = {
token,
tokenMs:timestamp
}
await updateManyData(TABLENAME.管理后台用户, {loginId: userId}, updateUInfo);
}
let permissionDbData:any = await selectOneDataByParam(TABLENAME.管理后台用户, {loginId:userId}, ["loginId", "permission"]);
const resultInfo = {
type,
userId: userId,
userInfo, // 添加用户信息
timestamp,
userName: userInfo.user_name,
// adminFlag: userInfo.admin_flag.int, //账号标识:-1:公共帐号,0:一般成员,1:所属组组长,2:超级管理员
userType, //“一般注册用户”、“行政管理人员”、“教职员工”、“学生”、“家长”
token,
tokenMs: timestamp,
permission: permissionDbData.data.permission, //本系统权限-是否管理员(0-否,1-是)
};
res.success(resultInfo);
} catch (error) {
console.error('Session检查异常:', error);
if (error.code === 'ENOTFOUND') {
throw new BizError(ERRORENUM.网络连接失败);
} else if (error.code === 'ECONNREFUSED') {
throw new BizError(ERRORENUM.服务不可用);
} else if (error.code === 'ETIMEDOUT') {
throw new BizError(ERRORENUM.请求超时);
} else {
throw new BizError(ERRORENUM.系统繁忙请稍后重试);
}
}
}
/**
* 获取登录账号的权限
* 手动获取用户信息 调用user.getUserInfo方法,完全控制编码处理
*/
async function getUserInfoManual(userId: string): Promise<any> {
return new Promise((resolve, reject) => {
try {
const xmlBody = `<?xml version="1.0" encoding="GBK"?>
<methodCall>
<methodName>user.getUserInfo</methodName>
<params>
<param>
<value><string>user_id='${userId}'</string></value>
</param>
<param>
<value><string></string></value>
</param>
<param>
<value><int>1</int></value>
</param>
<param>
<value><int>0</int></value>
</param>
</params>
</methodCall>`;
const options = {
hostname: 'platform.nmzx.xhedu.sh.cn',
port: 443,
path: '/platform/rpc/index.php',
method: 'POST',
timeout: 10000,
headers: {
'Content-Type': 'text/xml',
'Authorization': `Basic ${AUTH_BASE64}`,
'Content-Length': Buffer.byteLength(xmlBody, 'gbk'),
'Accept': 'text/xml',
'Connection': 'close'
},
rejectUnauthorized: false
};
const req = https.request(options, async (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk) => {
// 收集所有 chunk 到数组中
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
res.on('end', async () => {
const uint8Arrays = chunks.map(chunk => {
if (chunk instanceof Uint8Array) {
return chunk;
} else {
// 使用类型断言确保 TypeScript 知道这是一个 Buffer
const bufferChunk = chunk as Buffer;
return new Uint8Array(
bufferChunk.buffer,
bufferChunk.byteOffset,
bufferChunk.byteLength
);
}
});
console.log('原始响应十六进制:', uint8Arrays.toString().substring(0, 100));
// 计算总长度
const totalLength = uint8Arrays.reduce((total, chunk) => total + chunk.length, 0);
// 创建合并后的 Uint8Array
const mergedUint8Array = new Uint8Array(totalLength);
let offset = 0;
// 复制所有 chunk 到 mergedUint8Array
for (const chunk of uint8Arrays) {
mergedUint8Array.set(chunk, offset);
offset += chunk.length;
}
// 将 Uint8Array 转换回 Buffer
const mergedBuffer = Buffer.from(mergedUint8Array);
if (res.statusCode !== 200) {
console.error('获取用户信息失败,状态码:', res.statusCode);
return resolve(null);
}
try {
// 直接尝试多种编码方式解析
let decodedResponse;
// 尝试1: 直接作为GBK解码
try {
decodedResponse = iconv.decode(mergedBuffer, 'gbk');
console.log('使用GBK解码成功');
} catch (e) {
console.log('GBK解码失败,尝试UTF-8:', e.message);
// 尝试2: 作为UTF-8解码
decodedResponse = mergedBuffer.toString('utf8');
}
// 检查是否有乱码字符
if (hasGarbledCharacters(decodedResponse)) {
console.log('检测到乱码,尝试修复编码');
// 尝试3: 修复可能的双重编码
decodedResponse = fixDoubleEncoding(decodedResponse);
}
// 解析XML
const result = await parseUserInfoXml(decodedResponse);
resolve(result);
} catch (error) {
console.error('解析用户信息失败:', error);
// 尝试原始响应作为最后手段
try {
const result = await parseUserInfoXml(mergedBuffer.toString('binary'));
resolve(result);
} catch (e) {
console.error('所有解析尝试都失败');
resolve(null);
}
}
});
});
req.on('error', (error) => {
console.error('请求错误:', error);
resolve(null);
});
req.on('timeout', () => {
console.error('请求超时');
req.destroy();
resolve(null);
});
req.write(xmlBody, 'binary');
req.end();
} catch (error) {
console.error('获取用户信息异常:', error);
return null;
}
});
}
/**
* 处理用户信息响应,修复乱码
*/
function processUserInfoResponse(response: any): any {
if (Array.isArray(response) && response.length > 0) {
const userInfo = response[0];
// 递归修复对象中的所有字符串字段
const fixEncodingRecursively = (obj: any): any => {
if (typeof obj === 'string') {
return fixGBKEncoding(obj);
} else if (Array.isArray(obj)) {
return obj.map(item => fixEncodingRecursively(item));
} else if (typeof obj === 'object' && obj !== null) {
const result: any = {};
for (const key in obj) {
result[key] = fixEncodingRecursively(obj[key]);
}
return result;
}
return obj;
};
return fixEncodingRecursively(userInfo);
}
return response;
}
/**
* 修复GBK编码的字符串
*/
function fixGBKEncoding(str: string): string {
if (!str || typeof str !== 'string') return str || '';
// 检查是否包含Unicode替换字符
if (str.includes('�') || /[\uFFFD]/.test(str)) {
try {
// 假设原始字符串是GBK编码被错误解析为UTF-8
// 先转换回Buffer,再重新解码
const buffer = iconv.encode(str, 'utf8');
return iconv.decode(buffer, 'gbk');
} catch (error) {
console.warn('GBK修复失败:', error, '原始字符串:', str);
return str;
}
}
return str;
}
/**
* 改进的手动XML-RPC请求(正确解析响应)
*/
export async function k12CallWithManualRequest(sessid: string): Promise<string | false> {
return new Promise((resolve, reject) => {
try {
const xmlBody = `<?xml version="1.0" encoding="GBK"?>
<methodCall>
<methodName>user.isUserLogin</methodName>
<params>
<param>
<value><string>${sessid}</string></value>
</param>
</params>
</methodCall>`;
const options = {
hostname: 'platform.nmzx.xhedu.sh.cn',
port: 443,
path: '/platform/rpc/index.php',
method: 'POST',
timeout: 10000,
headers: {
'Content-Type': 'text/xml',
'Authorization': `Basic ${AUTH_BASE64}`,
'Content-Length': Buffer.byteLength(xmlBody, 'gbk'),
'Accept': 'text/xml',
'Connection': 'close'
},
rejectUnauthorized: false
};
console.log('发送请求到:', options.hostname + options.path);
const req = https.request(options, (res) => {
let responseData = '';
console.log('响应状态码:', res.statusCode);
console.log('响应头:', JSON.stringify(res.headers));
res.setEncoding('binary');
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', async () => {
console.log('完整响应长度:', responseData.length);
console.log('响应预览:', responseData.substring(0, 200));
if (res.statusCode === 404) {
console.error('服务端返回404 Not Found');
resolve(false);
return;
}
if (res.statusCode === 401) {
console.error('服务端返回401 Unauthorized - 认证失败');
resolve(false);
return;
}
try {
// 先将响应数据从GBK转换为UTF-8
const utf8Response = convertGBKToString(Buffer.from(responseData, 'binary'));
// 解析XML响应
const result = await parseXmlResponse(utf8Response);
if (result && typeof result === 'string') {
resolve(result); // 返回解析到的nmzx2401910
} else {
resolve(false);
}
} catch (parseError) {
console.error('XML解析失败:', parseError);
resolve(false);
}
});
});
req.on('error', (error) => {
console.error('请求错误:', error.message, error.code);
resolve(false);
});
req.on('timeout', () => {
console.error('请求超时');
req.destroy();
resolve(false);
});
req.write(xmlBody, 'binary');
req.end();
} catch (error) {
console.error('请求构建错误:', error);
resolve(false);
}
});
}
/**
* 解析XML响应并提取值
*/
async function parseXmlResponse(xmlData: string | Buffer): Promise<string | false> {
return new Promise((resolve, reject) => {
// 确保输入是字符串
const xmlString = typeof xmlData === 'string' ? xmlData : convertEncoding(xmlData, 'gbk');
parseString(xmlString, {
explicitArray: false,
trim: true,
tagNameProcessors: [(name: string) => name]
}, (err, result) => {
if (err) {
console.error('XML解析错误:', err);
return resolve(false);
}
try {
const response = result.methodResponse;
if (!response || !response.params || !response.params.param) {
console.error('无效的XML响应结构');
return resolve(false);
}
const param = response.params.param;
const value = param.value;
if (value && value.string) {
const resultValue = value.string;
console.log('成功提取值:', resultValue);
return resolve(resultValue);
} else {
console.error('未找到string值在响应中');
return resolve(false);
}
} catch (parseError) {
console.error('响应解析错误:', parseError);
resolve(false);
}
});
});
}
/**
* 将GBK编码的Buffer或字符串转换为UTF-8字符串
*/
function convertGBKToString(input: any): string {
if (!input) return '';
try {
// 如果输入是Buffer,直接解码
if (Buffer.isBuffer(input)) {
return iconv.decode(input, 'gbk');
}
// 如果输入是字符串,检查是否需要解码
if (typeof input === 'string') {
// 检查是否包含乱码字符(通常是GBK编码被当作UTF-8读取的结果)
if (/[^\u0000-\u007F\u4E00-\u9FFF]/.test(input) && /[\uFFFD]/.test(input)) {
// 先将字符串转换为Buffer,然后从GBK解码
const buffer = Buffer.from(input, 'binary');
return iconv.decode(buffer, 'gbk');
}
return input;
}
// 其他类型转换为字符串
return String(input);
} catch (error) {
console.warn('GBK解码失败:', error, '原始输入:', input);
return typeof input === 'string' ? input : String(input);
}
}
/**
* 使用xmlrpc库的调用方法(也需要解析返回值)
*/
export function k12Call(sessid: string, timeout: number = 8000): Promise<string | false> {
return new Promise((resolve, reject) => {
if (!uacClient) {
uacClient = createUACClient();
}
const timer = setTimeout(() => {
reject(new Error('UAC服务响应超时'));
}, timeout);
console.log('调用UAC服务,会话ID:', sessid.substring(0, 10) + '...');
uacClient.methodCall('user.isUserLogin', [sessid], (error: any, value: any) => {
clearTimeout(timer);
if (error) {
console.error('UAC RPC调用失败:', error.message);
return resolve(false);
}
console.log('UAC RPC调用成功,返回值:', value, '类型:', typeof value);
// 如果xmlrpc库能正确解析,直接返回值
if (typeof value === 'string' && value) {
resolve(value);
} else {
console.log('返回值格式不符合预期,尝试手动解析');
resolve(false);
}
});
});
}
/**
* 测试解析函数
*/
export async function testXmlParsing(): Promise<void> {
const testXml = `<?xml version="1.0" encoding="GBK"?>
<methodResponse>
<params>
<param>
<value><string>nmzx2401910</string></value>
</param>
</params>
</methodResponse>`;
console.log('=== 测试XML解析 ===');
const result = await parseXmlResponse(testXml);
console.log('解析结果:', result);
}
// 在文件末尾添加测试
(async () => {
console.log('=== 测试XML解析功能 ===');
await testXmlParsing();
})();
/**
* 备份数据库
*/
import { Client } from 'ssh2';
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs';
import path from 'path';
import moment from 'moment';
const execAsync = promisify(exec);
// 备份配置
interface BackupConfig {
host: string;
port: number;
user: string;
password: string;
database: string;
localBackupPath: string; // 本地临时备份路径
remoteBackupPath: string; // 远程备份路径
remoteHost: string; // 远程服务器IP
remoteUser: string; // 远程服务器用户名
remotePassword: string; // 远程服务器密码
sshPort?: number; // SSH端口
retentionDays: number;
keepLocalBackup: boolean; // 新增:是否保留本地备份文件
}
const defaultConfig: BackupConfig = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306'),
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '123456',
database: process.env.DB_NAME || 'publicIntelligence',
localBackupPath: process.env.LOCAL_BACKUP_PATH || './nm_backups',
remoteBackupPath: process.env.REMOTE_BACKUP_PATH || '/mnt/nm_gzn/backups',
remoteHost: process.env.REMOTE_HOST || '123.207.147.179',
remoteUser: process.env.REMOTE_USER || 'root',
remotePassword: process.env.REMOTE_PASSWORD || 'GNIWT20110919!@@@',
sshPort: parseInt(process.env.SSH_PORT || '22'),
retentionDays: parseInt(process.env.BACKUP_RETENTION_DAYS || '30'),
keepLocalBackup: process.env.KEEP_LOCAL_BACKUP === 'true' || false, // 默认不保留; // 新增:是否保留本地备份文件
};
export class BackupService {
private config: BackupConfig;
private isBackingUp: boolean = false;
constructor(config?: Partial<BackupConfig>) {
this.config = { ...defaultConfig, ...config };
this.ensureLocalDirectory();
}
private ensureLocalDirectory(): void {
if (!fs.existsSync(this.config.localBackupPath)) {
fs.mkdirSync(this.config.localBackupPath, { recursive: true });
}
}
private generateBackupFileName(): string {
const timestamp = moment().format('YYYYMMDD_HHmmss');
return `${this.config.database}_backup_${timestamp}.sql`;
}
/**
* 使用SSH2执行远程命令
*/
private async executeRemoteCommand(command: string): Promise<string> {
return new Promise((resolve, reject) => {
const conn = new Client();
conn.on('ready', () => {
console.log('SSH连接已建立');
conn.exec(command, (err, stream) => {
if (err) {
conn.end();
return reject(err);
}
let stdout = '';
let stderr = '';
stream.on('close', (code: number) => {
conn.end();
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(`命令执行失败,退出码: ${code}, 错误: ${stderr}`));
}
}).on('data', (data: string) => {
stdout += data;
}).stderr.on('data', (data: string) => {
stderr += data;
});
});
}).on('error', (err) => {
reject(err);
}).connect({
host: this.config.remoteHost,
port: this.config.sshPort!,
username: this.config.remoteUser,
password: this.config.remotePassword
});
});
}
/**
* 使用SCP上传文件到远程服务器
*/
private async uploadToRemote(localFilePath: string): Promise<string> {
const fileName = path.basename(localFilePath);
const remoteFilePath = `${this.config.remoteBackupPath}/${fileName}`;
return new Promise((resolve, reject) => {
const conn = new Client();
conn.on('ready', () => {
conn.sftp((err, sftp) => {
if (err) {
conn.end();
return reject(err);
}
// 确保远程目录存在
this.executeRemoteCommand(`mkdir -p ${this.config.remoteBackupPath} && chmod 755 ${this.config.remoteBackupPath}`)
.then(() => {
const readStream = fs.createReadStream(localFilePath);
const writeStream = sftp.createWriteStream(remoteFilePath);
writeStream.on('close', () => {
conn.end();
console.log(`上传完成: ${remoteFilePath}`);
resolve(remoteFilePath);
});
writeStream.on('error', (err) => {
conn.end();
reject(err);
});
readStream.pipe(writeStream);
})
.catch(reject);
});
}).on('error', (err) => {
reject(err);
}).connect({
host: this.config.remoteHost,
port: this.config.sshPort!,
username: this.config.remoteUser,
password: this.config.remotePassword
});
});
}
/**
* 执行本地数据库备份
*/
private async createLocalBackup(): Promise<string> {
const fileName = this.generateBackupFileName();
const localFilePath = path.join(this.config.localBackupPath, fileName);
console.log(`备份文件路径: ${localFilePath}`);
console.log(`本地备份目录是否存在: ${fs.existsSync(this.config.localBackupPath)}`);
const mysqldumpPath = 'D:\\mysql\\mysql-8.0.29-winx64\\bin\\mysqldump.exe';
// 添加 --column-statistics=0 参数
const command = `"${mysqldumpPath}" --host=${this.config.host} --port=${this.config.port} --user=${this.config.user} --password=${this.config.password} --column-statistics=0 ${this.config.database} > "${localFilePath}"`;
console.log(`执行命令: ${command}`);
console.log(`开始本地备份: ${this.config.database}`);
try {
const { stdout, stderr } = await execAsync(command);
console.log(`命令执行完成,stdout: ${stdout}`);
console.log(`命令执行完成,stderr: ${stderr}`);
// 检查文件是否真的创建了
if (fs.existsSync(localFilePath)) {
const stats = fs.statSync(localFilePath);
console.log(`备份文件已创建,大小: ${stats.size} 字节`);
} else {
console.error(`备份文件未创建: ${localFilePath}`);
// 检查目录内容
const files = fs.readdirSync(this.config.localBackupPath);
console.log(`目录内容: ${files.join(', ')}`);
}
if (stderr && !stderr.includes('Using a password on the command line interface can be insecure')) {
console.warn('备份警告:', stderr);
}
console.log(`本地备份完成: ${localFilePath}`);
return localFilePath;
} catch (error) {
console.error('备份命令执行失败:', error);
throw error;
}
}
/**
* 清理远程服务器的旧备份文件
*/
private async cleanRemoteOldBackups(): Promise<void> {
const cleanCommand = `find ${this.config.remoteBackupPath} -name "${this.config.database}_backup_*.sql" -mtime +${this.config.retentionDays} -delete`;
try {
await this.executeRemoteCommand(cleanCommand);
console.log('远程旧备份清理完成');
} catch (error) {
console.error('远程清理失败:', error);
}
}
/**
* 清理本地临时文件
*/
private cleanLocalTempFiles(): void {
try {
const files = fs.readdirSync(this.config.localBackupPath);
files.forEach(file => {
if (file.endsWith('.sql')) {
const filePath = path.join(this.config.localBackupPath, file);
fs.unlinkSync(filePath);
}
});
} catch (error) {
console.error('清理本地临时文件失败:', error);
}
}
/**
* 完整的备份流程
*/
public async backupDatabase(): Promise<{ localPath: string; remotePath: string }> {
if (this.isBackingUp) {
throw new Error('已有备份任务正在进行中');
}
this.isBackingUp = true;
try {
console.log('=== 开始数据库备份流程 ===');
// 创建本地备份
const localFilePath = await this.createLocalBackup();
// 验证文件确实存在
if (!fs.existsSync(localFilePath)) {
throw new Error(`备份文件未创建: ${localFilePath}`);
}
const fileStats = fs.statSync(localFilePath);
if (fileStats.size === 0) {
console.warn('警告: 备份文件大小为0字节');
}
console.log(`备份文件验证成功,大小: ${fileStats.size} 字节`);
// 上传到远程服务器
console.log('开始上传到远程服务器...');
const remoteFilePath = await this.uploadToRemote(localFilePath);
// 清理远程旧备份
await this.cleanRemoteOldBackups();
// 只在配置不保留本地备份时才清理
if (!this.config.keepLocalBackup) {
this.cleanLocalTempFiles();
}
console.log('=== 数据库备份完成 ===');
return {
localPath: localFilePath,
remotePath: remoteFilePath
};
} catch (error) {
console.error('数据库备份失败:', error);
throw error;
} finally {
this.isBackingUp = false;
}
}
/**
* 测试SSH连接
*/
public async testSSHConnection(): Promise<void> {
try {
console.log('测试SSH连接...');
const result = await this.executeRemoteCommand('echo "SSH连接测试成功"');
console.log('SSH连接测试成功:', result.trim());
} catch (error) {
console.error('SSH连接测试失败:', error);
throw error;
}
}
/**
* 测试数据库连接
*/
public async testDatabaseConnection(): Promise<void> {
try {
console.log('测试数据库连接...');
const mysqlPath = 'D:\\mysql\\mysql-8.0.29-winx64\\bin\\mysql.exe'; // 使用绝对路径
const testCommand = `"${mysqlPath}" --host=${this.config.host} --port=${this.config.port} --user=${this.config.user} --password=${this.config.password} -e "SELECT 1" ${this.config.database}`;
await execAsync(testCommand);
console.log('数据库连接测试成功');
} catch (error) {
console.error('数据库连接测试失败:', error);
throw error;
}
}
/**
* 完整的备份测试
*/
public async testBackup(): Promise<void> {
console.log('开始备份功能测试...');
try {
await this.testSSHConnection();
await this.testDatabaseConnection();
const result = await this.backupDatabase();
console.log('备份测试成功!');
console.log('本地文件:', result.localPath);
console.log('远程文件:', result.remotePath);
} catch (error) {
console.error('备份测试失败:', error);
throw error;
}
}
/**
* 启动定时备份任务
*/
public startScheduledBackup(hour: number = 2, minute: number = 0): void {
console.log(`启动定时备份任务,每天 ${hour}:${minute.toString().padStart(2, '0')} 执行,备份到服务器: ${this.config.remoteHost}`);
const scheduleBackup = async () => {
const now = new Date();
const targetTime = new Date();
targetTime.setHours(hour, minute, 0, 0);
// 如果今天的目标时间已经过去,就设置到明天
if (now.getTime() > targetTime.getTime()) {
targetTime.setDate(targetTime.getDate() + 1);
console.log(`今天 ${hour}:${minute.toString().padStart(2, '0')} 已过,将在明天同一时间执行`);
}
const timeUntilBackup = targetTime.getTime() - now.getTime();
console.log(`下次备份将在 ${Math.round(timeUntilBackup / 1000 / 60)} 分钟后执行`);
setTimeout(async () => {
try {
console.log('开始执行定时备份...');
await this.backupDatabase();
console.log('定时备份完成');
} catch (error) {
console.error('定时备份失败:', error);
}
// 设置下一次备份(24小时后)
setTimeout(scheduleBackup, 24 * 60 * 60 * 1000);
}, timeUntilBackup);
};
// 立即检查并启动第一次调度
scheduleBackup();
}
/**
* 立即执行一次备份
*/
public async backupNow(): Promise<{ localPath: string; remotePath: string }> {
console.log('立即执行备份...');
return await this.backupDatabase();
}
}
import { Request, Response } from 'express';
import moment from 'moment';
import { BizError } from "../util/bizError";
import { TABLENAME, TABLEID } from "../config/dbEnum";
import { selectDataListByParam, selectDataToTableAssociation, selectDataToTableAssociationToPage, selectDataWithCustomOrder, selectOneDataByParam, selectPaginatedDataWithOrder } from "../data/findData";
import { STATE } from "../config/enum";
import { updateManyData } from "../data/updateData";
import { getMySqlMs, randomId } from "../tools/systemTools";
import { addData } from "../data/addData";
import { ERRORENUM } from "../config/errorEnum";
/**
* 检查学生是否有已提交的答题记录
* @param student_id 学生ID
* @returns 如果有已提交的记录返回false,否则返回true
*/
export async function checkStudentHasAnswered(student_id: string) {
if (!student_id) {
// 如果没有学生ID,认为可以答题(返回true)
return { checkanswered: true };
}
// 查找该学生已完成的答题记录
const existingRecord = await selectOneDataByParam(
TABLENAME.答题记录表,
{
student_id: student_id,
answer_status: STATE. // 只检查已完成的记录
},
["record_id"]
);
// 根据注释要求:有已提交记录返回false,没有返回true
const hasAnswered = !!(existingRecord.data && Object.keys(existingRecord.data).length > 0);
// 如果有记录,说明已经答过题,返回false
// 如果没有记录,说明可以答题,返回true
return { checkanswered: !hasAnswered };
}
/**
* 获取第一条服务时间配置
* @returns 第一条服务时间配置信息
*/
export async function getFirstOpeningTimeConfig() {
// 获取所有服务时间配置,按更新时间倒序排列(获取最新的配置)
const configs = await selectDataWithCustomOrder(
TABLENAME.服务开启时间表,
{},
["otId", "startTime", "endTime", "isOpen", "updated_by", "updated_at"],
[["updated_at", "DESC"]] // 按更新时间倒序,获取最新的配置
);
// 如果没有配置,返回默认值
if (!configs.data || configs.data.length === 0) {
return {
startTime: null,
endTime: null,
isOpen: 0,
isEnter: false, // 无配置时不允许进入
hasConfig: false,
message: '未配置服务时间'
};
}
// 获取第一条配置(最新的配置)
const firstConfig = configs.data[0];
// 计算 isEnter 字段
let isEnter = false;
if (firstConfig.isOpen === 1) {
const now = new Date();
const startTime = new Date(firstConfig.startTime);
const endTime = new Date(firstConfig.endTime);
// 判断当前时间是否在服务时间范围内
isEnter = now >= startTime && now <= endTime;
}
// 如果 isOpen 是 0,isEnter 保持 false
return {
startTime: firstConfig.startTime,
endTime: firstConfig.endTime,
isOpen: firstConfig.isOpen,
isEnter, // 是否允许进入
hasConfig: true,
otId: firstConfig.otId
};
}
/**
* 获取当前服务时间配置
*/
export async function getOpeningTimeConfig() {
const config = await selectDataListByParam(
TABLENAME.服务开启时间表,
{},
["otId", "startTime", "endTime", "isOpen", "updated_by", "updated_at"]
);
return config.data || [];
}
/**
* 创建或更新服务时间配置
* @param otId 配置ID(可选,为空时创建新配置)
* @param startTime 开始时间
* @param endTime 结束时间
* @param isOpen 是否启用
* @param updated_by 修改人
*/
export async function setOpeningTimeConfig(startTime: Date, endTime: Date, isOpen: number, updated_by: string, otId?) {
// 参数验证
if (!startTime || !endTime) {
throw new BizError(ERRORENUM.参数错误, '开始时间和结束时间不能为空');
}
if (startTime >= endTime) {
throw new BizError(ERRORENUM.参数错误, '开始时间必须早于结束时间');
}
if (isOpen !== 0 && isOpen !== 1) {
throw new BizError(ERRORENUM.参数错误, 'isOpen参数必须为0或1');
}
if (!updated_by) {
throw new BizError(ERRORENUM.参数错误, '修改人不能为空');
}
let configId = otId;
let isUpdate = false;
// 如果没有提供otId,检查是否已存在配置
if (!configId) {
const existingConfigs = await getOpeningTimeConfig();
if (existingConfigs.length > 0) {
// 使用第一个配置的ID进行更新
configId = existingConfigs[0].otId;
isUpdate = true;
} else {
// 创建新配置
configId = randomId(TABLEID.服务开启时间表); // 使用适当的前缀生成ID
}
} else {
isUpdate = true;
}
const updateInfo = {
startTime: getMySqlMs(startTime),
endTime: getMySqlMs(endTime),
isOpen,
updated_by,
updated_at: getMySqlMs()
};
if (isUpdate) {
// 更新现有配置
await updateManyData(
TABLENAME.服务开启时间表,
{ otId: configId },
updateInfo
);
} else {
// 创建新配置
const addInfo = {
otId: configId,
...updateInfo
};
await addData(TABLENAME.服务开启时间表, addInfo);
}
return {
isSuccess: true,
otId: configId,
message: isUpdate ? '配置更新成功' : '配置创建成功'
};
}
/**
* 启用或禁用服务时间配置
* @param otId 配置ID
* @param isOpen 是否启用 (0-否, 1-是)
* @param updated_by 修改人
*/
export async function toggleOpeningTimeConfig(otId: string, isOpen: number, updated_by: string) {
// 参数验证
if (!otId) {
throw new BizError(ERRORENUM.参数错误, '配置ID不能为空');
}
if (isOpen !== 0 && isOpen !== 1) {
throw new BizError(ERRORENUM.参数错误, 'isOpen参数必须为0或1');
}
if (!updated_by) {
throw new BizError(ERRORENUM.参数错误, '修改人不能为空');
}
// 检查配置是否存在
const existingConfig = await selectOneDataByParam(
TABLENAME.服务开启时间表,
{ otId },
["otId"]
);
if (!existingConfig.data) {
throw new BizError(ERRORENUM.数据不存在, '指定的配置不存在');
}
// 更新配置
await updateManyData(
TABLENAME.服务开启时间表,
{ otId },
{
isOpen,
updated_by,
updated_at: getMySqlMs()
}
);
return {
isSuccess: true,
message: `配置已${isOpen === 1 ? '启用' : '禁用'}`
};
}
/**
* 检查当前是否在服务时间内
*/
export async function checkIsInOpeningTime() {
const configs = await getOpeningTimeConfig();
// 如果没有配置或没有启用的配置,默认允许访问
if (!configs || configs.length === 0) {
return { isInTime: true, message: '未配置服务时间,默认允许访问' };
}
// 查找启用的配置
const activeConfig = configs.find(config => config.isOpen === 1);
if (!activeConfig) {
return { isInTime: false, message: '没有启用的服务时间配置' };
}
const now = new Date();
const startTime = new Date(activeConfig.startTime);
const endTime = new Date(activeConfig.endTime);
const isInTime = now >= startTime && now <= endTime;
return {
isInTime,
message: isInTime ? '当前在服务时间内' : '当前不在服务时间内',
config: activeConfig
};
}
/**
* 查询公/智/能三大方向下,各维度的所有题目(题目顺序随机)
* @returns {Promise<{
* 公: Array<{dimensionName: string, questionList: Array<any>}>,
* 智: Array<{dimensionName: string, questionList: Array<any>}>,
* 能: Array<{dimensionName: string, questionList: Array<any>}>
* }>} 按方向+维度区分的题目数据(题目随机顺序)
*/
// export async function getQuestions() {
// // 1. 配置多表关联:题目表关联维度表,获取维度名称、所属方向(公/智/能)
// const manyTableInfo: any = {};
// manyTableInfo[TABLENAME.维度表] = {
// column: ["dimension_id", "dimension_name", "direction"], // 维度表核心字段(文档关联字段)
// where: {}, // 无筛选,查询文档中所有9个维度
// };
// // 2. 配置题目表返回字段(严格对应文档"量表题目"内容及表结构)
// const questionReturnFields = [
// "question_id", // 题目唯一标识(您定义的VARCHAR类型)
// "question_content", // 题目具体内容(文档中"量表题目"列的文本)
// "question_order" // 题目在维度内的序号(文档中每个维度1-4题的顺序)
// ];
// // 3. 执行多表关联查询:获取所有题目及对应维度信息
// const queryResult = await selectDataToTableAssociation(
// TABLENAME.题目表, // 主表:题目表
// manyTableInfo, // 关联表配置:维度表
// {}, // 主表查询条件:无(查询文档中36道题)
// questionReturnFields // 题目表返回字段
// );
// // 4. 按"维度ID"分组,确保每个维度的题目聚合在一起
// const dimensionQuestionMap = new Map<string, any>();
// queryResult.data.forEach((question: any) => {
// // 提取关联的维度信息(匹配文档中的维度定义)
// const {
// dimension_id,
// dimension_name,
// direction
// } = question.dimension;
// // 整理单题数据(仅保留文档相关信息)
// const formattedQuestion = {
// question_id: question.question_id, //题目ID
// question_content: question.question_content, //题目内容
// question_order: question.question_order //维度内序号
// };
// // 按维度ID分组
// if (!dimensionQuestionMap.has(dimension_id)) {
// dimensionQuestionMap.set(dimension_id, {
// direction: direction, // 所属大方向(公/智/能)
// dimensionName: dimension_name,// 维度名称(如文档中的"家国情怀")
// questions: [] // 该维度下的题目列表
// });
// }
// // 将题目推入对应维度的列表
// dimensionQuestionMap.get(dimension_id).questions.push(formattedQuestion);
// });
// // 5. 对每个维度的题目进行随机排序(打乱原有顺序)
// Array.from(dimensionQuestionMap.values()).forEach(dimension => {
// // 使用Fisher-Yates洗牌算法随机排序
// const questions = dimension.questions;
// for (let i = questions.length - 1; i > 0; i--) {
// const j = Math.floor(Math.random() * (i + 1));
// [questions[i], questions[j]] = [questions[j], questions[i]];
// }
// dimension.questions = questions;
// });
// // 6. 初始化返回结构:按文档3大方向分类,每个方向下按维度区分
// const result = {
// 公: [] as Array<{dimensionName: string, questionList: Array<any>}>,
// 智: [] as Array<{dimensionName: string, questionList: Array<any>}>,
// 能: [] as Array<{dimensionName: string, questionList: Array<any>}>
// };
// // 7. 按方向+维度整理数据(严格匹配文档"公/智/能"对应维度)
// Array.from(dimensionQuestionMap.values()).forEach(dimension => {
// const { direction, dimensionName, questions } = dimension;
// // 仅保留文档中定义的3个方向,避免异常数据
// if (result.hasOwnProperty(direction)) {
// result[direction as keyof typeof result].push({
// dimensionName: dimensionName, // 文档中的维度名称
// questionList: questions // 该维度下随机排序的4道题
// });
// }
// });
// // 8. 返回按"公/智/能+维度"区分的题目数据(题目顺序随机)
// return result;
// }
/**
* 查询公/智/能三大方向下的所有题目(按方向分类,题目随机顺序)
* @returns {Promise<{
* 公(evaluation1): Array<any>,
* 智(evaluation2): Array<any>,
* 能(evaluation3): Array<any>
* }>} 按方向分类的题目数据(题目随机顺序)
*/
export async function getQuestionsByDirection() {
// 1. 配置多表关联:题目表关联维度表,获取维度名称、所属方向(公/智/能)
const manyTableInfo: any = {};
manyTableInfo[TABLENAME.维度表] = {
column: ["dimension_id", "dimension_name", "direction"], // 维度表核心字段
where: {}, // 无筛选,查询文档中所有9个维度
};
// 2. 配置题目表返回字段
const questionReturnFields = [
"question_id", // 题目唯一标识
"question_content", // 题目具体内容
"question_order", // 题目在维度内的序号
"dimension_id" // 维度ID,用于关联
];
// 3. 执行多表关联查询:获取所有题目及对应维度信息
const queryResult = await selectDataToTableAssociation(
TABLENAME.题目表, // 主表:题目表
manyTableInfo, // 关联表配置:维度表
{}, // 主表查询条件:无
questionReturnFields // 题目表返回字段
);
// 4. 按方向分类题目,不按维度分组
const result = {
: [] as Array<any>,
: [] as Array<any>,
: [] as Array<any>
};
// 5. 将题目按方向分类
queryResult.data.forEach((question: any) => {
const direction = question.dimension?.direction;
if (direction && result.hasOwnProperty(direction)) {
const formattedQuestion = {
question_id: question.question_id,
question_content: question.question_content,
question_order: question.question_order,
dimension_id: question.dimension_id,
dimension_name: question.dimension?.dimension_name
};
result[direction as keyof typeof result].push(formattedQuestion);
}
});
// 6. 对每个方向的题目进行随机排序
Object.keys(result).forEach((direction) => {
const questions = result[direction as keyof typeof result];
// 使用Fisher-Yates洗牌算法随机排序
for (let i = questions.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[questions[i], questions[j]] = [questions[j], questions[i]];
}
});
// 7. 返回按方向分类的题目数据(题目随机顺序)
let dataList = {
evaluation1:result.,
evaluation2:result.,
evaluation3:result.
};
return dataList;
}
/**
* 完成答题接口(单题)
* @param student_id 学生ID
* @param question_id 题目ID
* @param score 得分(1-7分)
*/
export async function completeAnswer(student_id: string, student_name: string, question_id: string, score: number) {
// 新增检查:判断学生是否已经答过题
const hasAnswered = await checkStudentHasAnswered(student_id);
if (!hasAnswered.checkanswered) {
throw new BizError(ERRORENUM.重复答题, '您已经完成过答题,不能重复答题');
}
// 1. 参数校验
if (!student_id || !question_id || score === undefined || score === null) {
throw new BizError(ERRORENUM.参数错误);
}
if (score < 1 || score > 7) {
throw new BizError(ERRORENUM.参数错误, '分数必须在1-7之间');
}
// 2. 检查/创建答题记录
const record = await checkOrCreateRecord(student_id, student_name);
// 3. 检查是否已答过本题
const existingDetail:any = await selectOneDataByParam(
TABLENAME.答题记录明细表,
{ question_id, record_id: record.record_id },
[]
);
// 4. 更新或创建明细记录
if (existingDetail && Object.keys(existingDetail).length > 0) {
await updateManyData(
TABLENAME.答题记录明细表,
{ detail_id: existingDetail.detail_id },
{ score }
);
} else {
const addInfo = {
detail_id: randomId(TABLEID.答题记录明细表),
record_id: record.record_id,
question_id,
score
};
await addData(TABLENAME.答题记录明细表, addInfo);
}
// 5. 更新总分
await updateTotalScore(record.record_id);
return { isSuccess: true, record_id: record.record_id };
}
/**
* 批量完成答题接口
* @param student_id 学生ID
* @param answers 答题数组
*/
export async function completeAnswerBatch(student_id: string, student_name: string, answers: Array<{question_id: string, score: number}>) {
// 新增检查:判断学生是否已经答过题
// const hasAnswered = await checkStudentHasAnswered(student_id);
// if (hasAnswered) {
// throw new BizError(ERRORENUM.重复答题, '您已经完成过答题,不能重复答题');
// }
const hasAnswered = await checkStudentHasAnswered(student_id);
if (!hasAnswered.checkanswered) {
throw new BizError(ERRORENUM.重复答题, '您已经完成过答题,不能重复答题');
}
// 1. 参数校验
if (!student_id || !student_name || !answers || !Array.isArray(answers) || answers.length === 0) {
throw new BizError(ERRORENUM.参数错误);
}
// 2. 检查/创建答题记录
const record = await checkOrCreateRecord(student_id, student_name);
if (record) {
}
// 3. 批量处理答题明细
for (const answer of answers) {
const { question_id, score } = answer;
if (!question_id || score === undefined || score === null) {
throw new BizError(ERRORENUM.参数错误, '每个答题项必须包含question_id和score');
}
if (score < 1 || score > 7) {
throw new BizError(ERRORENUM.参数错误, `题目${question_id}的分数必须在1-7之间`);
}
// 检查是否已答过本题
const existingDetail:any = await selectOneDataByParam(
TABLENAME.答题记录明细表,
{ question_id, record_id: record.record_id },
[]
);
if (existingDetail.data && Object.keys(existingDetail.data).length > 0) {
await updateManyData(
TABLENAME.答题记录明细表,
{ detail_id: existingDetail.data.detail_id },
{ score }
);
} else {
const addInfo = {
detail_id: randomId(TABLEID.答题记录明细表),
record_id: record.record_id,
question_id,
score
};
await addData(TABLENAME.答题记录明细表, addInfo);
}
}
// 4. 更新总分
await updateTotalScore(record.record_id);
return { record_id: record.record_id };
}
/**
* 完成全部答题(标记答题状态为已完成)
* @param record_id 答题记录ID
*/
export async function finishAnswer(record_id: string) {
if (!record_id) {
throw new BizError(ERRORENUM.参数错误);
}
// 检查答题记录是否存在
const recordInfo = await selectOneDataByParam(
TABLENAME.答题记录表,
{ record_id },
["record_id"]
);
if (!recordInfo.data) {
throw new BizError(ERRORENUM.答题记录不存在);
}
// 更新答题状态为已完成
const updateInfo = {
answer_status: STATE.,
answer_time: getMySqlMs()
};
await updateManyData(
TABLENAME.答题记录表,
{ record_id },
updateInfo
);
return { isSuccess: true };
}
/**
* 获取答题结果
* @param record_id 答题记录ID
*/
export async function getAnswerResult(record_id: string) {
if (!record_id) {
throw new BizError(ERRORENUM.参数错误);
}
// 获取答题记录和明细
const manyTableInfo: any = {};
manyTableInfo[TABLENAME.答题记录表] = {
column: ["total_score", "answer_time", "answer_status"],
where: {}
};
manyTableInfo[TABLENAME.题目表] = {
column: ["question_id", "dimension_id", "question_content"],
where: {}
};
manyTableInfo[TABLENAME.维度表] = {
column: ["dimension_name", "direction"],
where: {}
};
const answerInfo: any = await selectDataToTableAssociation(
TABLENAME.答题记录明细表,
manyTableInfo,
{ record_id },
["detail_id", "record_id", "question_id", "score"]
);
if (!answerInfo) {
throw new BizError(ERRORENUM.答题记录不存在);
}
// 计算各方向得分
const scores = {
: 0,
: 0,
: 0
};
answerInfo.forEach((detail: any) => {
const direction = detail.gaoxin_维度表?.direction;
if (direction && scores.hasOwnProperty(direction)) {
scores[direction as keyof typeof scores] += parseInt(detail.score || 0);
}
});
const totalScore = parseInt(answerInfo[0]?.gaoxin_答题记录表?.total_score || 0);
return {
record_id,
total_score: totalScore,
scores,
answer_time: answerInfo[0]?.gaoxin_答题记录表?.answer_time,
answer_status: answerInfo[0]?.gaoxin_答题记录表?.answer_status
};
}
/**
* 查找未完成的记录或创建新记录
* @param studentId 学生ID
*/
async function checkOrCreateRecord(studentId: string, studentName: string): Promise<any> {
let addInfo = {};
// 查找未完成的答题记录
let existingRecord = await selectOneDataByParam(
TABLENAME.答题记录表,
{ student_id: studentId, student_name: studentName, answer_status: STATE. },
["record_id", "student_id", "total_score", "answer_time", "answer_status"]
);
if (existingRecord.data && Object.keys(existingRecord.data).length > 0) {
addInfo = existingRecord.data;
} else {
// 创建新的答题记录
addInfo = {
record_id: randomId(TABLEID.答题记录表),
student_id: studentId,
student_name: studentName,
answer_time: getMySqlMs(),
total_score: 0,
answer_status: STATE.
};
await addData(TABLENAME.答题记录表, addInfo);
}
return addInfo;
}
/**
* 更新总分
* @param recordId 答题记录ID
*/
async function updateTotalScore(recordId: string) {
// 获取该记录的所有答题明细
const existingDetails:any = await selectDataListByParam(
TABLENAME.答题记录明细表,
{ record_id: recordId },
["score"]
);
// 计算总分
let sumScore = 0;
existingDetails.data.forEach((info: any) => {
sumScore += parseInt(info.score || 0);
});
// 更新答题记录的总分
let updateInfo = {
total_score: sumScore,
answer_time: getMySqlMs()
};
await updateManyData(
TABLENAME.答题记录表,
{ record_id: recordId },
updateInfo
);
}
/**
* 获取答题结果及对应的狮子形象
* @param record_id 答题记录ID
*/
export async function getAnswerResultWithLionImage(record_id: string) {
if (!record_id) {
throw new BizError(ERRORENUM.参数错误);
}
// 1. 获取答题记录基本信息
const recordInfo = await selectOneDataByParam(
TABLENAME.答题记录表,
{ record_id },
["record_id", "student_id", "total_score", "answer_time", "answer_status"]
);
if (!recordInfo.data) {
throw new BizError(ERRORENUM.答题记录不存在);
}
// 修改答题记录为已完成:
await finishAnswer(record_id);
// 2. 获取各方向得分
const scores = await calculateDirectionScores(record_id);
// 3. 根据得分判断对应的狮子形象
const lionImage = await getLionImageByScores(scores);
return {
record_id,
total_score: recordInfo.data.total_score,
scores, // 各方向得分
lion_image: lionImage, // 对应的狮子形象数据
answer_time: recordInfo.data.answer_time,
answer_status: recordInfo.data.answer_status
};
}
/**
* 计算各方向得分
* @param record_id 答题记录ID
*/
async function calculateDirectionScores(record_id: string) {
// 获取答题明细及对应的题目信息
let manyTableInfo: any = {};
manyTableInfo[TABLENAME.题目表] = {
column: ["question_id", "dimension_id"],
where: {}
};
let answerDetails = await selectDataToTableAssociation(
TABLENAME.答题记录明细表,
manyTableInfo,
{ record_id },
["detail_id", "record_id", "question_id", "score"]
);
// 初始化各方向得分
let directionScores = {
: 0,
: 0,
: 0
};
// 收集所有维度ID,用于批量查询维度信息
let dimensionIds = new Set<string>(); // 创建空集合
answerDetails.data.forEach((detail: any) => {
let dimensionId;
if (detail.dataValues && detail.dataValues !== null) {
let question = detail.dataValues.question;
dimensionId = question.dimension_id;
}
if (dimensionId) {
dimensionIds.add(dimensionId);
}
});
// 批量查询维度信息
let dimensions = await selectDataListByParam(
TABLENAME.维度表,
{ dimension_id: Array.from(dimensionIds) },
["dimension_id", "direction"]
);
// 创建维度ID到方向的映射
let dimensionDirectionMap = {};
dimensions.data.forEach((info: any) => {
let dimension = info.dataValues;
dimensionDirectionMap[dimension.dimension_id] = dimension.direction;
});
// 计算各方向总分
answerDetails.data.forEach((detail: any) => {
let dimensionId;
if (detail.question && detail.question !== null) {
let question = detail.question;
dimensionId = question.dimension_id;
let score = parseInt(detail.score || 0);
if (dimensionId) {
let direction = dimensionDirectionMap[dimensionId];
if (direction && directionScores.hasOwnProperty(direction)) {
directionScores[direction as keyof typeof directionScores] += score;
}
}
}
});
return directionScores;
}
/**
* 根据得分获取对应的狮子形象
* @param scores 各方向得分 {公: number, 智: number, 能: number}
*/
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'
]
);
// 遍历所有狮子形象,找到匹配的判断条件
for (const lionImage of allLionImages.data) {
const condition = lionImage.judgment_condition;
if (condition && evaluateCondition(condition, publicScore, intelligenceScore, abilityScore)) {
return lionImage;
}
}
// 如果没有找到完全匹配的条件,返回默认的潜力成长狮
const defaultLion = allLionImages.data.find((lion: any) =>
lion.judgment_condition === '公<50&智<50&能<50'
);
return defaultLion || allLionImages.data[0]; // 返回默认狮子或第一个狮子
}
/**
* 评估判断条件是否满足
* @param condition 判断条件字符串,如 "公≥50 & 智≥50 & 能≥50"
* @param publicScore 公方向得分
* @param intelligenceScore 智方向得分
* @param abilityScore 能方向得分
*/
function evaluateCondition(condition: string, publicScore: number, intelligenceScore: number, abilityScore: number): boolean {
// 将条件字符串拆分为单个条件
const conditions = condition.split('&').map(c => c.trim());
for (const cond of conditions) {
if (!evaluateSingleCondition(cond, publicScore, intelligenceScore, abilityScore)) {
return false;
}
}
return true;
}
/**
* 评估单个条件
* @param condition 单个条件字符串,如 "公≥50"
*/
function evaluateSingleCondition(condition: string, publicScore: number, intelligenceScore: number, abilityScore: number): boolean {
// 匹配条件格式:方向 + 操作符 + 数值
const match = condition.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 '≥':
case '>=':
return score >= value;
case '≤':
case '<=':
return score <= value;
case '≈':
// 近似判断,允许±5的误差
return Math.abs(score - value) <= 5;
case '=':
return score === value;
default:
return false;
}
}
/**
* 获取所有狮子形象数据(用于前端展示)
*/
export async function getAllLionImages() {
const lionImages = await selectDataListByParam(
TABLENAME.狮子形象表,
{},
[
'lion_id',
'standard_name',
'alias_name',
'characteristic',
'judgment_condition',
'magic_artifact',
'magic_artifact_text',
'suggestion',
'suggestion_text',
'lion_image'
]
);
return lionImages;
}
/**
* 根据学生ID获取最新的答题结果和狮子形象
* @param student_id 学生ID
*/
export async function getLatestAnswerResultByStudent(student_id: string) {
if (!student_id) {
throw new BizError(ERRORENUM.参数错误);
}
// 获取学生最新的答题记录
const latestRecord = await selectDataWithCustomOrder(
TABLENAME.答题记录表,
{ student_id },
["record_id"],
[["answer_time", "DESC" ]] // 按时间倒序,获取最新的记录
);
if (!latestRecord.data) {
throw new BizError(ERRORENUM.答题记录不存在);
}
// 获取详细的答题结果和狮子形象
return await getAnswerResultWithLionImage(latestRecord.data.record_id);
}
// 管理员页面 =========================================================================================================================
/**
* 获取所有学生答题记录及详细得分
* @returns 包含学生姓名和各维度得分的列表
*/
export async function getAllStudentAnswerRecords() {
// 1. 先同步uac_user表中的活跃用户到answer_record表
await syncActiveUsersToAnswerRecord();
// 1. 获取所有已完成答题的记录
const completedRecords = await selectDataListByParam(
TABLENAME.答题记录表,
{ answer_status: STATE. }, // 只获取已完成的记录
["record_id", "student_id", "student_name", "total_score", "answer_time"]
);
if (!completedRecords.data || completedRecords.data.length === 0) {
return [];
}
const result = [];
// 2. 遍历每个学生的答题记录
for (const record of completedRecords.data) {
const studentRecord = await getStudentDetailedScores(record.record_id);
if (studentRecord) {
result.push(studentRecord);
}
}
return result;
}
/**
* 获取单个学生的详细得分情况
*/
async function getStudentDetailedScores(record_id: string) {
// 获取答题明细及对应的题目和维度信息
const answerDetails = await selectDataToTableAssociation(
TABLENAME.答题记录明细表,
{
[TABLENAME.题目表]: {
column: ["question_id", "dimension_id"],
where: {}
}
},
{ record_id },
["detail_id", "score", "question_id"]
);
if (!answerDetails.data || answerDetails.data.length === 0) {
return null;
}
// 收集所有维度ID,用于批量查询维度信息
const dimensionIds = new Set<string>();
answerDetails.data.forEach((detail: any) => {
const dimensionId = detail.question?.dimension_id;
if (dimensionId) {
dimensionIds.add(dimensionId);
}
});
// 批量查询维度信息
const dimensions = await selectDataListByParam(
TABLENAME.维度表,
{ dimension_id: Array.from(dimensionIds) },
["dimension_id", "dimension_name"]
);
// 创建维度ID到维度名称的映射
const dimensionNameMap = new Map();
dimensions.data.forEach((dimension: any) => {
dimensionNameMap.set(dimension.dimension_id, dimension.dimension_name);
});
// 获取答题记录基本信息
const recordInfo = await selectOneDataByParam(
TABLENAME.答题记录表,
{ record_id },
["student_name", "total_score", "answer_time"]
);
// 初始化各维度得分
const dimensionScores: { [key: string]: number } = {
'家国情怀': 0,
'国际视野': 0,
'责任担当': 0,
'学业扎实': 0,
'勇于创新': 0,
'善于学习': 0,
'健康生活': 0,
'审美情趣': 0,
'劳动意识': 0
};
// 计算各维度得分
answerDetails.data.forEach((detail: any) => {
const dimensionId = detail.question?.dimension_id;
const dimensionName = dimensionNameMap.get(dimensionId);
const score = parseInt(detail.score || 0);
if (dimensionName && dimensionScores.hasOwnProperty(dimensionName)) {
dimensionScores[dimensionName] += score;
}
});
// 计算三大方向得分
const publicScore = dimensionScores['家国情怀'] + dimensionScores['国际视野'] + dimensionScores['责任担当'];
const intelligenceScore = dimensionScores['学业扎实'] + dimensionScores['勇于创新'] + dimensionScores['善于学习'];
const abilityScore = dimensionScores['健康生活'] + dimensionScores['审美情趣'] + dimensionScores['劳动意识'];
return {
studentName: recordInfo.data?.student_name,
// 公方向
jgqh: dimensionScores['家国情怀'],
gjsy: dimensionScores['国际视野'],
zrdd: dimensionScores['责任担当'],
gong: publicScore,
// 智方向
xyzs: dimensionScores['学业扎实'],
yycx: dimensionScores['勇于创新'],
syxx: dimensionScores['善于学习'],
zhi: intelligenceScore,
// 能方向
jksh: dimensionScores['健康生活'],
smqq: dimensionScores['审美情趣'],
ldys: dimensionScores['劳动意识'],
neng: abilityScore,
// 总分
totalScore: recordInfo.data?.total_score,
answerTime: recordInfo.data?.answer_time || "暂未提交答题"
};
}
/**
* 获取所有学生答题记录(简化版 - 批量查询优化性能)- 分页版本
* @param page 页码,从1开始
* @param pageSize 每页大小
*/
// export async function getAllStudentAnswerRecordsOptimized(page: number = 1, pageSize: number = 10) {
// try {
// // 1. 先同步活跃用户
// await syncActiveUsersToAnswerRecord();
// // 2. 获取活跃用户ID
// const activeUsers = await selectDataListByParam(
// TABLENAME.统一用户表,
// { active_flag: 1 },
// ["user_id"]
// );
// const activeUserIds = activeUsers.data?.map((user: any) => user.user_id) || [];
// if (activeUserIds.length === 0) {
// return {
// list: [],
// total: 0,
// page,
// pageSize,
// totalPages: 0
// };
// }
// // 3. 获取每个用户的最新答题记录(使用替代方案)
// const { records: latestRecords, total } = await getLatestStudentRecords(activeUserIds, page, pageSize);
// if (!latestRecords || latestRecords.length === 0) {
// return {
// list: [],
// total,
// page,
// pageSize,
// totalPages: Math.ceil(total / pageSize)
// };
// }
// // 4. 分离已完成和未完成的记录
// const completedRecords = latestRecords.filter(record => record.answer_status === 1);
// const recordIds = completedRecords.map((record: any) => record.record_id);
// // 5. 只为已完成记录获取答题明细和维度信息
// let recordScoreMap = new Map();
// const dimensionKeys = [
// '家国情怀', '国际视野', '责任担当',
// '学业扎实', '勇于创新', '善于学习',
// '健康生活', '审美情趣', '劳动意识'
// ];
// if (recordIds.length > 0) {
// // 获取答题明细
// const allAnswerDetails = await selectDataListByParam(
// TABLENAME.答题记录明细表,
// { record_id: recordIds },
// ["record_id", "score", "question_id"]
// );
// if (allAnswerDetails.data && allAnswerDetails.data.length > 0) {
// // 获取题目和维度信息
// const questionIds = [...new Set(allAnswerDetails.data.map((detail: any) => detail.question_id))];
// const includeQuestionDimension: any = {};
// includeQuestionDimension[TABLENAME.维度表] = {
// column: ["dimension_id", "dimension_name"],
// where: {}
// };
// const questionsWithDimensions = await selectDataToTableAssociation(
// TABLENAME.题目表,
// includeQuestionDimension,
// { question_id: questionIds },
// ["question_id", "dimension_id"]
// );
// // 创建题目维度映射
// const questionDimensionMap = new Map();
// questionsWithDimensions.data?.forEach((item: any) => {
// const dimensionName = item.dimension?.dimension_name;
// if (dimensionName) {
// questionDimensionMap.set(item.question_id, dimensionName);
// }
// });
// // 初始化已完成记录的维度分数
// completedRecords.forEach((record: any) => {
// const initialScores: { [key: string]: number } = {};
// dimensionKeys.forEach(key => {
// initialScores[key] = 0;
// });
// recordScoreMap.set(record.record_id, initialScores);
// });
// // 计算各维度分数
// allAnswerDetails.data.forEach((detail: any) => {
// const dimensionName = questionDimensionMap.get(detail.question_id);
// const score = parseInt(detail.score || 0);
// if (dimensionName && recordScoreMap.has(detail.record_id)) {
// const scores = recordScoreMap.get(detail.record_id);
// if (scores && scores.hasOwnProperty(dimensionName)) {
// scores[dimensionName] += score;
// }
// }
// });
// }
// }
// // 6. 构建最终结果
// const list = latestRecords.map((record: any) => {
// if (record.answer_status === 0) {
// // 未完成记录
// return {
// record_id: record.record_id,
// studentName: record.student_name,
// jgqh: 0,
// gjsy: 0,
// zrdd: 0,
// gong: 0,
// xyzs: 0,
// yycx: 0,
// syxx: 0,
// zhi: 0,
// jksh: 0,
// smqq: 0,
// ldys: 0,
// neng: 0,
// totalScore: 0,
// answerTime: "暂未提交答题",
// answerStatus: record.answer_status
// };
// } else {
// // 已完成记录
// const dimensionScores = recordScoreMap.get(record.record_id) || {};
// const publicScore = (dimensionScores['家国情怀'] || 0) +
// (dimensionScores['国际视野'] || 0) +
// (dimensionScores['责任担当'] || 0);
// const intelligenceScore = (dimensionScores['学业扎实'] || 0) +
// (dimensionScores['勇于创新'] || 0) +
// (dimensionScores['善于学习'] || 0);
// const abilityScore = (dimensionScores['健康生活'] || 0) +
// (dimensionScores['审美情趣'] || 0) +
// (dimensionScores['劳动意识'] || 0);
// return {
// record_id: record.record_id,
// studentName: record.student_name,
// jgqh: dimensionScores['家国情怀'] || 0,
// gjsy: dimensionScores['国际视野'] || 0,
// zrdd: dimensionScores['责任担当'] || 0,
// gong: publicScore,
// xyzs: dimensionScores['学业扎实'] || 0,
// yycx: dimensionScores['勇于创新'] || 0,
// syxx: dimensionScores['善于学习'] || 0,
// zhi: intelligenceScore,
// jksh: dimensionScores['健康生活'] || 0,
// smqq: dimensionScores['审美情趣'] || 0,
// ldys: dimensionScores['劳动意识'] || 0,
// neng: abilityScore,
// totalScore: record.total_score || 0,
// answerTime: record.answer_time || "已提交",
// answerStatus: record.answer_status
// };
// }
// });
// const totalPages = Math.ceil(total / pageSize);
// return {
// list,
// total,
// page,
// pageSize,
// totalPages
// };
// } catch (error) {
// console.error('获取答题记录失败:', error);
// return {
// list: [],
// total: 0,
// page,
// pageSize,
// totalPages: 0
// };
// }
// }
// 辅助函数:获取每个用户的最新答题记录
/**
* 获取所有学生答题记录(简化版 - 批量查询优化性能)- 分页版本
* @param page 页码,从1开始
* @param pageSize 每页大小
*/
export async function getAllStudentAnswerRecordsOptimized(page: number = 1, pageSize: number = 10) {
try {
// 1. 先同步活跃用户
await syncActiveUsersToAnswerRecord();
// 2. 获取活跃用户ID
const activeUsers = await selectDataListByParam(
TABLENAME.统一用户表,
{ active_flag: 1 },
["user_id"]
);
const activeUserIds = activeUsers.data?.map((user: any) => user.user_id) || [];
if (activeUserIds.length === 0) {
return {
list: [],
total: 0,
page,
pageSize,
totalPages: 0
};
}
// 3. 获取每个学生的答题记录(优先取已答题记录,相同学生多条已答题记录时取最早的一条)
const { records: studentRecords, total } = await getStudentRecordsWithPriority(activeUserIds, page, pageSize);
if (!studentRecords || studentRecords.length === 0) {
return {
list: [],
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize)
};
}
// 4. 获取所有记录的答题明细和维度信息(包括未完成记录)
const allRecordIds = studentRecords.map((record: any) => record.record_id);
let recordScoreMap = new Map();
const dimensionKeys = [
'家国情怀', '国际视野', '责任担当',
'学业扎实', '勇于创新', '善于学习',
'健康生活', '审美情趣', '劳动意识'
];
if (allRecordIds.length > 0) {
// 获取所有记录的答题明细
const allAnswerDetails = await selectDataListByParam(
TABLENAME.答题记录明细表,
{ record_id: allRecordIds },
["record_id", "score", "question_id"]
);
if (allAnswerDetails.data && allAnswerDetails.data.length > 0) {
// 获取题目和维度信息
const questionIds = [...new Set(allAnswerDetails.data.map((detail: any) => detail.question_id))];
const includeQuestionDimension: any = {};
includeQuestionDimension[TABLENAME.维度表] = {
column: ["dimension_id", "dimension_name"],
where: {}
};
const questionsWithDimensions = await selectDataToTableAssociation(
TABLENAME.题目表,
includeQuestionDimension,
{ question_id: questionIds },
["question_id", "dimension_id"]
);
// 创建题目维度映射
const questionDimensionMap = new Map();
questionsWithDimensions.data?.forEach((item: any) => {
const dimensionName = item.dimension?.dimension_name;
if (dimensionName) {
questionDimensionMap.set(item.question_id, dimensionName);
}
});
// 初始化所有记录的维度分数
studentRecords.forEach((record: any) => {
const initialScores: { [key: string]: number } = {};
dimensionKeys.forEach(key => {
initialScores[key] = 0;
});
recordScoreMap.set(record.record_id, initialScores);
});
// 计算各维度分数(包括未完成记录)
allAnswerDetails.data.forEach((detail: any) => {
const dimensionName = questionDimensionMap.get(detail.question_id);
const score = parseInt(detail.score || 0);
if (dimensionName && recordScoreMap.has(detail.record_id)) {
const scores = recordScoreMap.get(detail.record_id);
if (scores && scores.hasOwnProperty(dimensionName)) {
scores[dimensionName] += score;
}
}
});
}
}
// 5. 构建最终结果 - 正确显示所有记录的实际数据
const list = studentRecords.map((record: any) => {
const dimensionScores = recordScoreMap.get(record.record_id) || {};
const publicScore = (dimensionScores['家国情怀'] || 0) +
(dimensionScores['国际视野'] || 0) +
(dimensionScores['责任担当'] || 0);
const intelligenceScore = (dimensionScores['学业扎实'] || 0) +
(dimensionScores['勇于创新'] || 0) +
(dimensionScores['善于学习'] || 0);
const abilityScore = (dimensionScores['健康生活'] || 0) +
(dimensionScores['审美情趣'] || 0) +
(dimensionScores['劳动意识'] || 0);
return {
record_id: record.record_id,
student_id: record.student_id,
studentName: record.student_name,
// 维度得分 - 使用实际数据
jgqh: dimensionScores['家国情怀'] || 0,
gjsy: dimensionScores['国际视野'] || 0,
zrdd: dimensionScores['责任担当'] || 0,
gong: publicScore,
xyzs: dimensionScores['学业扎实'] || 0,
yycx: dimensionScores['勇于创新'] || 0,
syxx: dimensionScores['善于学习'] || 0,
zhi: intelligenceScore,
jksh: dimensionScores['健康生活'] || 0,
smqq: dimensionScores['审美情趣'] || 0,
ldys: dimensionScores['劳动意识'] || 0,
neng: abilityScore,
// 其他字段 - 使用实际数据
totalScore: record.total_score || 0,
answerTime: record.answer_status === 1 ? (record.answer_time || "已提交") : (record.answer_time || "暂未提交答题"),
answerStatus: record.answer_status
};
});
const totalPages = Math.ceil(total / pageSize);
return {
list,
total,
page,
pageSize,
totalPages
};
} catch (error) {
console.error('获取答题记录失败:', error);
return {
list: [],
total: 0,
page,
pageSize,
totalPages: 0
};
}
}
// export async function getAllStudentAnswerRecordsOptimized(page: number = 1, pageSize: number = 10) {
// try {
// // 1. 先同步活跃用户
// await syncActiveUsersToAnswerRecord();
// // 2. 获取活跃用户ID
// const activeUsers = await selectDataListByParam(
// TABLENAME.统一用户表,
// { active_flag: 1 },
// ["user_id"]
// );
// const activeUserIds = activeUsers.data?.map((user: any) => user.user_id) || [];
// if (activeUserIds.length === 0) {
// return {
// list: [],
// total: 0,
// page,
// pageSize,
// totalPages: 0
// };
// }
// // 3. 获取每个学生的答题记录(优先取已答题记录,相同学生多条已答题记录时取最早的一条)
// const { records: studentRecords, total } = await getStudentRecordsWithPriority(activeUserIds, page, pageSize);
// if (!studentRecords || studentRecords.length === 0) {
// return {
// list: [],
// total,
// page,
// pageSize,
// totalPages: Math.ceil(total / pageSize)
// };
// }
// // 4. 分离已完成和未完成的记录
// const completedRecords = studentRecords.filter(record => record.answer_status === 1);
// const recordIds = completedRecords.map((record: any) => record.record_id);
// // 5. 只为已完成记录获取答题明细和维度信息
// let recordScoreMap = new Map();
// const dimensionKeys = [
// '家国情怀', '国际视野', '责任担当',
// '学业扎实', '勇于创新', '善于学习',
// '健康生活', '审美情趣', '劳动意识'
// ];
// if (recordIds.length > 0) {
// // 获取答题明细
// const allAnswerDetails = await selectDataListByParam(
// TABLENAME.答题记录明细表,
// { record_id: recordIds },
// ["record_id", "score", "question_id"]
// );
// if (allAnswerDetails.data && allAnswerDetails.data.length > 0) {
// // 获取题目和维度信息
// const questionIds = [...new Set(allAnswerDetails.data.map((detail: any) => detail.question_id))];
// const includeQuestionDimension: any = {};
// includeQuestionDimension[TABLENAME.维度表] = {
// column: ["dimension_id", "dimension_name"],
// where: {}
// };
// const questionsWithDimensions = await selectDataToTableAssociation(
// TABLENAME.题目表,
// includeQuestionDimension,
// { question_id: questionIds },
// ["question_id", "dimension_id"]
// );
// // 创建题目维度映射
// const questionDimensionMap = new Map();
// questionsWithDimensions.data?.forEach((item: any) => {
// const dimensionName = item.dimension?.dimension_name;
// if (dimensionName) {
// questionDimensionMap.set(item.question_id, dimensionName);
// }
// });
// // 初始化已完成记录的维度分数
// completedRecords.forEach((record: any) => {
// const initialScores: { [key: string]: number } = {};
// dimensionKeys.forEach(key => {
// initialScores[key] = 0;
// });
// recordScoreMap.set(record.record_id, initialScores);
// });
// // 计算各维度分数
// allAnswerDetails.data.forEach((detail: any) => {
// const dimensionName = questionDimensionMap.get(detail.question_id);
// const score = parseInt(detail.score || 0);
// if (dimensionName && recordScoreMap.has(detail.record_id)) {
// const scores = recordScoreMap.get(detail.record_id);
// if (scores && scores.hasOwnProperty(dimensionName)) {
// scores[dimensionName] += score;
// }
// }
// });
// }
// }
// // 6. 构建最终结果
// const list = studentRecords.map((record: any) => {
// if (record.answer_status === 0) {
// // 未完成记录
// return {
// record_id: record.record_id,
// studentName: record.student_name,
// jgqh: 0,
// gjsy: 0,
// zrdd: 0,
// gong: 0,
// xyzs: 0,
// yycx: 0,
// syxx: 0,
// zhi: 0,
// jksh: 0,
// smqq: 0,
// ldys: 0,
// neng: 0,
// totalScore: 0,
// answerTime: "暂未提交答题",
// answerStatus: record.answer_status
// };
// } else {
// // 已完成记录
// const dimensionScores = recordScoreMap.get(record.record_id) || {};
// const publicScore = (dimensionScores['家国情怀'] || 0) +
// (dimensionScores['国际视野'] || 0) +
// (dimensionScores['责任担当'] || 0);
// const intelligenceScore = (dimensionScores['学业扎实'] || 0) +
// (dimensionScores['勇于创新'] || 0) +
// (dimensionScores['善于学习'] || 0);
// const abilityScore = (dimensionScores['健康生活'] || 0) +
// (dimensionScores['审美情趣'] || 0) +
// (dimensionScores['劳动意识'] || 0);
// return {
// record_id: record.record_id,
// studentName: record.student_name,
// jgqh: dimensionScores['家国情怀'] || 0,
// gjsy: dimensionScores['国际视野'] || 0,
// zrdd: dimensionScores['责任担当'] || 0,
// gong: publicScore,
// xyzs: dimensionScores['学业扎实'] || 0,
// yycx: dimensionScores['勇于创新'] || 0,
// syxx: dimensionScores['善于学习'] || 0,
// zhi: intelligenceScore,
// jksh: dimensionScores['健康生活'] || 0,
// smqq: dimensionScores['审美情趣'] || 0,
// ldys: dimensionScores['劳动意识'] || 0,
// neng: abilityScore,
// totalScore: record.total_score || 0,
// answerTime: record.answer_time || "已提交",
// answerStatus: record.answer_status
// };
// }
// });
// const totalPages = Math.ceil(total / pageSize);
// return {
// list,
// total,
// page,
// pageSize,
// totalPages
// };
// } catch (error) {
// console.error('获取答题记录失败:', error);
// return {
// list: [],
// total: 0,
// page,
// pageSize,
// totalPages: 0
// };
// }
// }
// 辅助函数:获取每个学生的答题记录(优先已答题,相同学生多条已答题记录时取有数据的记录)
async function getStudentRecordsWithPriority(activeUserIds: string[], page: number, pageSize: number) {
try {
// 获取所有活跃用户的答题记录
const allRecords = await selectDataListByParam(
TABLENAME.答题记录表,
{ student_id: activeUserIds },
["record_id", "student_id", "student_name", "total_score", "answer_time", "answer_status"]
);
if (!allRecords.data || allRecords.data.length === 0) {
return { records: [], total: 0 };
}
// 按用户分组,处理每个学生的记录
const userRecordMap = new Map();
allRecords.data.forEach((record: any) => {
const studentId = record.student_id;
const existingRecord = userRecordMap.get(studentId);
if (!existingRecord) {
// 如果还没有记录,直接设置
userRecordMap.set(studentId, record);
} else {
const existingStatus = existingRecord.answer_status;
const currentStatus = record.answer_status;
const existingTime = new Date(existingRecord.answer_time);
const currentTime = new Date(record.answer_time);
// 优先逻辑:已答题记录优先于未答题记录
if (existingStatus === 0 && currentStatus === 1) {
// 现有记录是未答题,新记录是已答题,使用新记录
userRecordMap.set(studentId, record);
} else if (existingStatus === 1 && currentStatus === 1) {
// 两个都是已答题记录,取时间更早的记录
if (currentTime < existingTime) {
userRecordMap.set(studentId, record);
}
} else if (existingStatus === 1 && currentStatus === 0) {
// 现有记录是已答题,新记录是未答题,保持现有记录
// 不做任何操作
} else if (existingStatus === 0 && currentStatus === 0) {
// 两个都是未答题记录,检查是否有数据
const existingHasData = existingRecord.total_score > 0 || existingRecord.answer_time;
const currentHasData = record.total_score > 0 || record.answer_time;
if (!existingHasData && currentHasData) {
// 现有记录无数据,新记录有数据,使用新记录
userRecordMap.set(studentId, record);
} else if (existingHasData && !currentHasData) {
// 现有记录有数据,新记录无数据,保持现有记录
// 不做任何操作
} else if (existingHasData && currentHasData) {
// 两个都有数据,取时间更早的记录
if (currentTime < existingTime) {
userRecordMap.set(studentId, record);
}
} else {
// 两个都无数据,取时间更早的记录
if (currentTime < existingTime) {
userRecordMap.set(studentId, record);
}
}
}
}
});
// 转换为数组
const uniqueRecords = Array.from(userRecordMap.values());
// 总体排序逻辑:按答题时间倒序(最新的在前)
uniqueRecords.sort((a, b) => {
const timeA = new Date(a.answer_time).getTime();
const timeB = new Date(b.answer_time).getTime();
return timeB - timeA; // 倒序:时间大的在前
});
// 手动分页
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedRecords = uniqueRecords.slice(startIndex, endIndex);
return {
records: paginatedRecords,
total: uniqueRecords.length
};
} catch (error) {
console.error('获取学生记录失败:', error);
return { records: [], total: 0 };
}
}
// 辅助函数:获取每个学生的最早答题记录
async function getEarliestStudentRecords(activeUserIds: string[], page: number, pageSize: number) {
try {
// 获取所有活跃用户的答题记录
const allRecords = await selectDataListByParam(
TABLENAME.答题记录表,
{ student_id: activeUserIds },
["record_id", "student_id", "student_name", "total_score", "answer_time", "answer_status"]
);
if (!allRecords.data || allRecords.data.length === 0) {
return { records: [], total: 0 };
}
// 按用户分组,取每个学生的最早记录
const userRecordMap = new Map();
allRecords.data.forEach((record: any) => {
const existingRecord = userRecordMap.get(record.student_id);
// 如果还没有记录,或者当前记录时间更早,则更新
if (!existingRecord) {
userRecordMap.set(record.student_id, record);
} else {
const existingTime = new Date(existingRecord.answer_time);
const currentTime = new Date(record.answer_time);
// 修改:取时间更早的记录
if (currentTime < existingTime) {
userRecordMap.set(record.student_id, record);
}
}
});
// 转换为数组
const uniqueRecords = Array.from(userRecordMap.values());
// 修改排序逻辑:总体按答题时间倒序(最新的在前)
uniqueRecords.sort((a, b) => {
const timeA = new Date(a.answer_time).getTime();
const timeB = new Date(b.answer_time).getTime();
return timeB - timeA; // 倒序:时间大的在前
});
// 手动分页
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedRecords = uniqueRecords.slice(startIndex, endIndex);
return {
records: paginatedRecords,
total: uniqueRecords.length
};
} catch (error) {
console.error('获取最早学生记录失败:', error);
return { records: [], total: 0 };
}
}
async function getLatestStudentRecords(activeUserIds: string[], page: number, pageSize: number) {
try {
// 获取所有活跃用户的答题记录
const allRecords = await selectDataListByParam(
TABLENAME.答题记录表,
{ student_id: activeUserIds },
["record_id", "student_id", "student_name", "total_score", "answer_time", "answer_status"]
);
if (!allRecords.data || allRecords.data.length === 0) {
return { records: [], total: 0 };
}
// 按用户分组,取每个用户的最新记录
const userRecordMap = new Map();
allRecords.data.forEach((record: any) => {
const existingRecord = userRecordMap.get(record.student_id);
// 如果还没有记录,或者当前记录时间更新,则更新
if (!existingRecord) {
userRecordMap.set(record.student_id, record);
} else {
const existingTime = new Date(existingRecord.answer_time);
const currentTime = new Date(record.answer_time);
if (currentTime > existingTime) {
userRecordMap.set(record.student_id, record);
}
}
});
// 转换为数组
const uniqueRecords = Array.from(userRecordMap.values());
// 排序逻辑:已完成在前,然后按时间倒序
uniqueRecords.sort((a, b) => {
// 先按答题状态排序:已完成(1)在前,未完成(0)在后
if (a.answer_status !== b.answer_status) {
return b.answer_status - a.answer_status;
}
// 相同状态下按时间倒序(最新的在前)
const timeA = new Date(a.answer_time).getTime();
const timeB = new Date(b.answer_time).getTime();
return timeB - timeA;
});
// 手动分页
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedRecords = uniqueRecords.slice(startIndex, endIndex);
return {
records: paginatedRecords,
total: uniqueRecords.length
};
} catch (error) {
console.error('获取最新学生记录失败:', error);
return { records: [], total: 0 };
}
}
/**
* 同步uac_user表中的活跃用户到answer_record表
* 为每个active_flag=1且user_type第4位为'1'(学生身份)的用户创建一条未完成的答题记录(如果不存在)
*/
async function syncActiveUsersToAnswerRecord() {
try {
// 1. 获取uac_user表中所有active_flag=1的用户
const activeUsers = await selectDataListByParam(
TABLENAME.统一用户表,
{ active_flag: 1 },
["user_id", "user_name", "user_type"]
);
if (!activeUsers.data || activeUsers.data.length === 0) {
return;
}
// 2. 筛选出学生身份的用户(user_type第4位为'1')
const studentUsers = activeUsers.data.filter((user: any) => {
const userType = user.user_type || '';
// 确保user_type至少有4位,且第4位为'1'
return userType.length >= 4 && userType.charAt(3) === '1';
});
if (studentUsers.length === 0) {
return;
}
// 3. 批量检查这些学生用户是否已有答题记录
const studentIds = studentUsers.map((user: any) => user.user_id);
const existingRecords = await selectDataListByParam(
TABLENAME.答题记录表,
{ student_id: studentIds },
["student_id"]
);
// 创建已存在记录的学生ID集合
const existingStudentIds = new Set();
if (existingRecords.data && existingRecords.data.length > 0) {
existingRecords.data.forEach((record: any) => {
existingStudentIds.add(record.student_id);
});
}
// 4. 为没有答题记录的学生用户创建未完成记录
const recordsToCreate = [];
for (const user of studentUsers) {
if (!existingStudentIds.has(user.user_id)) {
recordsToCreate.push({
record_id: randomId(TABLEID.答题记录表),
student_id: user.user_id,
student_name: user.user_name || user.user_id,
total_score: 0,
answer_time: null,
answer_status: STATE.
});
}
}
// 5. 批量创建记录
if (recordsToCreate.length > 0) {
for (const record of recordsToCreate) {
await addData(TABLENAME.答题记录表, record);
}
console.log(`成功为 ${recordsToCreate.length} 个活跃学生用户创建未完成答题记录`);
}
} catch (error) {
console.error('同步活跃学生用户到答题记录表时出错:', error);
}
}
/**
* 单个答题记录数据下载
* @param record_id
* @returns
*/
export async function getStudentDetailed(record_id: string) {
// 获取答题明细及对应的题目和维度信息
const answerDetails = await selectDataToTableAssociation(
TABLENAME.答题记录明细表,
{
[TABLENAME.题目表]: {
column: ["question_id", "dimension_id"],
where: {}
}
},
{ record_id },
["detail_id", "score", "question_id"]
);
let output = [["学号", "学生姓名", "家国情怀", "国际视野", "责任担当", "公得分", "学业扎实", "勇于创新", "善于学习", "智得分", "健康生活", "审美情趣", "劳动意识", "能得分", "总分", "答题时间"]];
if (answerDetails.data || answerDetails.data.length != 0) {
// 收集所有维度ID,用于批量查询维度信息
const dimensionIds = new Set<string>();
answerDetails.data.forEach((detail: any) => {
const dimensionId = detail.question?.dimension_id;
if (dimensionId) {
dimensionIds.add(dimensionId);
}
});
// 批量查询维度信息
const dimensions = await selectDataListByParam(
TABLENAME.维度表,
{ dimension_id: Array.from(dimensionIds) },
["dimension_id", "dimension_name"]
);
// 创建维度ID到维度名称的映射
const dimensionNameMap = new Map();
dimensions.data.forEach((dimension: any) => {
dimensionNameMap.set(dimension.dimension_id, dimension.dimension_name);
});
// 获取答题记录基本信息
const recordInfo = await selectOneDataByParam(
TABLENAME.答题记录表,
{ record_id },
["student_id", "student_name", "total_score", "answer_time"]
);
// 初始化各维度得分
const dimensionScores: { [key: string]: number } = {
'家国情怀': 0,
'国际视野': 0,
'责任担当': 0,
'学业扎实': 0,
'勇于创新': 0,
'善于学习': 0,
'健康生活': 0,
'审美情趣': 0,
'劳动意识': 0
};
// 计算各维度得分
answerDetails.data.forEach((detail: any) => {
const dimensionId = detail.question?.dimension_id;
const dimensionName = dimensionNameMap.get(dimensionId);
const score = parseInt(detail.score || 0);
if (dimensionName && dimensionScores.hasOwnProperty(dimensionName)) {
dimensionScores[dimensionName] += score;
}
});
// 计算三大方向得分
const publicScore = dimensionScores['家国情怀'] + dimensionScores['国际视野'] + dimensionScores['责任担当'];
const intelligenceScore = dimensionScores['学业扎实'] + dimensionScores['勇于创新'] + dimensionScores['善于学习'];
const abilityScore = dimensionScores['健康生活'] + dimensionScores['审美情趣'] + dimensionScores['劳动意识'];
output.push([
recordInfo.data?.student_id,
recordInfo.data?.student_name,
dimensionScores['家国情怀'],
dimensionScores['国际视野'],
dimensionScores['责任担当'],
publicScore,
dimensionScores['学业扎实'],
dimensionScores['勇于创新'],
dimensionScores['善于学习'],
intelligenceScore,
dimensionScores['健康生活'],
dimensionScores['审美情趣'],
dimensionScores['劳动意识'],
abilityScore,
recordInfo.data?.total_score,
recordInfo.data?.answer_time || "暂未提交答题"
]);
}
return output;
}
/**
* 批量答题记录数据下载
* @param record_ids 答题记录ID数组
* @returns 二维数组,包含所有记录的详细数据
*/
export async function getBatchStudentDetailed(record_ids?: string[] | null | undefined) {
let normalizedRecordIds: string[] = [];
let allRecords: any[] = [];
// 1. 处理参数:如果没有传入record_ids,则使用与getAllStudentAnswerRecordsOptimized相同的逻辑
if (!record_ids) {
// 使用与getAllStudentAnswerRecordsOptimized相同的逻辑获取所有记录(无分页)
const result = await getAllStudentAnswerRecordsOptimizedWithoutPagination();
allRecords = result.list || [];
if (allRecords.length === 0) {
return [[
"学号", "学生姓名",
"家国情怀", "国际视野", "责任担当", "公得分",
"学业扎实", "勇于创新", "善于学习", "智得分",
"健康生活", "审美情趣", "劳动意识", "能得分",
"总分", "答题时间", "答题状态"
]];
}
normalizedRecordIds = allRecords.map((record: any) => record.record_id);
} else {
// 有特定record_ids时,按传入的顺序获取
if (Array.isArray(record_ids)) {
normalizedRecordIds = record_ids.filter(id => id && typeof id === 'string');
} else if (typeof record_ids === 'string') {
normalizedRecordIds = [record_ids];
} else {
normalizedRecordIds = [String(record_ids)];
}
if (normalizedRecordIds.length > 0) {
// 获取指定记录的信息
const recordsInfo = await selectDataWithCustomOrder(
TABLENAME.答题记录表,
{ record_id: normalizedRecordIds },
["record_id", "student_id", "student_name", "total_score", "answer_time", "answer_status"],
[
["answer_time", "DESC"]
]
);
allRecords = recordsInfo.data || [];
}
}
// 2. 构建输出数据
const output = [[
"学号", "学生姓名",
"家国情怀", "国际视野", "责任担当", "公得分",
"学业扎实", "勇于创新", "善于学习", "智得分",
"健康生活", "审美情趣", "劳动意识", "能得分",
"总分", "答题时间", "答题状态"
]];
if (allRecords.length === 0) {
return output;
}
// 3. 批量获取所有答题明细
const allAnswerDetails = await selectDataListByParam(
TABLENAME.答题记录明细表,
{ record_id: normalizedRecordIds },
["record_id", "score", "question_id"]
);
// 4. 初始化各维度默认得分(0分)
const defaultDimensionScores = {
'家国情怀': 0,
'国际视野': 0,
'责任担当': 0,
'学业扎实': 0,
'勇于创新': 0,
'善于学习': 0,
'健康生活': 0,
'审美情趣': 0,
'劳动意识': 0
};
// 5. 如果有答题明细,创建题目ID到维度名称的映射
const questionDimensionMap = new Map();
if (allAnswerDetails.data && allAnswerDetails.data.length > 0) {
const questionIds = [...new Set(allAnswerDetails.data.map((detail: any) => detail.question_id))];
const includeQuestionDimension: any = {};
includeQuestionDimension[TABLENAME.维度表] = {
column: ["dimension_id", "dimension_name"],
where: {}
};
const questionsWithDimensions = await selectDataToTableAssociation(
TABLENAME.题目表,
includeQuestionDimension,
{ question_id: questionIds },
["question_id", "dimension_id"]
);
questionsWithDimensions.data.forEach((item: any) => {
const dimensionName = item.dimension?.dimension_name;
if (dimensionName) {
questionDimensionMap.set(item.question_id, dimensionName);
}
});
}
// 6. 按记录ID分组计算各维度得分
const recordScoresMap = new Map();
// 初始化所有记录的得分都为默认值(0分)
normalizedRecordIds.forEach(recordId => {
recordScoresMap.set(recordId, {...defaultDimensionScores});
});
// 如果有答题明细,更新实际得分
if (allAnswerDetails.data && allAnswerDetails.data.length > 0) {
allAnswerDetails.data.forEach((detail: any) => {
const recordId = detail.record_id;
const dimensionName = questionDimensionMap.get(detail.question_id);
const score = parseInt(detail.score || 0);
if (!recordScoresMap.has(recordId)) {
recordScoresMap.set(recordId, {...defaultDimensionScores});
}
const scores = recordScoresMap.get(recordId);
if (dimensionName && scores && scores.hasOwnProperty(dimensionName)) {
scores[dimensionName] += score;
}
});
}
// 7. 按排序后的顺序输出数据
for (const record of allRecords) {
const dimensionScores = recordScoresMap.get(record.record_id) || {...defaultDimensionScores};
// 计算三大方向得分
const publicScore = (dimensionScores['家国情怀'] || 0) +
(dimensionScores['国际视野'] || 0) +
(dimensionScores['责任担当'] || 0);
const intelligenceScore = (dimensionScores['学业扎实'] || 0) +
(dimensionScores['勇于创新'] || 0) +
(dimensionScores['善于学习'] || 0);
const abilityScore = (dimensionScores['健康生活'] || 0) +
(dimensionScores['审美情趣'] || 0) +
(dimensionScores['劳动意识'] || 0);
output.push([
record.student_id || '',
record.student_name || '',
dimensionScores['家国情怀'] || 0,
dimensionScores['国际视野'] || 0,
dimensionScores['责任担当'] || 0,
publicScore,
dimensionScores['学业扎实'] || 0,
dimensionScores['勇于创新'] || 0,
dimensionScores['善于学习'] || 0,
intelligenceScore,
dimensionScores['健康生活'] || 0,
dimensionScores['审美情趣'] || 0,
dimensionScores['劳动意识'] || 0,
abilityScore,
record.total_score || 0,
record.answer_time || '暂未答题',
record.answer_status === 1 ? '已完成' : '未完成'
]);
}
return output;
}
// 辅助函数:getAllStudentAnswerRecordsOptimized的无分页版本
async function getAllStudentAnswerRecordsOptimizedWithoutPagination() {
try {
// 1. 先同步活跃用户
await syncActiveUsersToAnswerRecord();
// 2. 获取活跃用户ID
const activeUsers = await selectDataListByParam(
TABLENAME.统一用户表,
{ active_flag: 1 },
["user_id"]
);
const activeUserIds = activeUsers.data?.map((user: any) => user.user_id) || [];
if (activeUserIds.length === 0) {
return {
list: [],
total: 0
};
}
// 3. 获取每个学生的答题记录(优先取已答题记录,相同学生多条已答题记录时取最早的一条)
const { records: studentRecords, total } = await getStudentRecordsWithPriority(activeUserIds, 1, 100000); // 使用大pageSize获取所有记录
if (!studentRecords || studentRecords.length === 0) {
return {
list: [],
total: 0
};
}
// 4. 分离已完成和未完成的记录
const completedRecords = studentRecords.filter(record => record.answer_status === 1);
const recordIds = completedRecords.map((record: any) => record.record_id);
// 5. 只为已完成记录获取答题明细和维度信息
let recordScoreMap = new Map();
const dimensionKeys = [
'家国情怀', '国际视野', '责任担当',
'学业扎实', '勇于创新', '善于学习',
'健康生活', '审美情趣', '劳动意识'
];
if (recordIds.length > 0) {
// 获取答题明细
const allAnswerDetails = await selectDataListByParam(
TABLENAME.答题记录明细表,
{ record_id: recordIds },
["record_id", "score", "question_id"]
);
if (allAnswerDetails.data && allAnswerDetails.data.length > 0) {
// 获取题目和维度信息
const questionIds = [...new Set(allAnswerDetails.data.map((detail: any) => detail.question_id))];
const includeQuestionDimension: any = {};
includeQuestionDimension[TABLENAME.维度表] = {
column: ["dimension_id", "dimension_name"],
where: {}
};
const questionsWithDimensions = await selectDataToTableAssociation(
TABLENAME.题目表,
includeQuestionDimension,
{ question_id: questionIds },
["question_id", "dimension_id"]
);
// 创建题目维度映射
const questionDimensionMap = new Map();
questionsWithDimensions.data?.forEach((item: any) => {
const dimensionName = item.dimension?.dimension_name;
if (dimensionName) {
questionDimensionMap.set(item.question_id, dimensionName);
}
});
// 初始化已完成记录的维度分数
completedRecords.forEach((record: any) => {
const initialScores: { [key: string]: number } = {};
dimensionKeys.forEach(key => {
initialScores[key] = 0;
});
recordScoreMap.set(record.record_id, initialScores);
});
// 计算各维度分数
allAnswerDetails.data.forEach((detail: any) => {
const dimensionName = questionDimensionMap.get(detail.question_id);
const score = parseInt(detail.score || 0);
if (dimensionName && recordScoreMap.has(detail.record_id)) {
const scores = recordScoreMap.get(detail.record_id);
if (scores && scores.hasOwnProperty(dimensionName)) {
scores[dimensionName] += score;
}
}
});
}
}
// 6. 构建最终结果(与getAllStudentAnswerRecordsOptimized格式一致)
const list = studentRecords.map((record: any) => {
if (record.answer_status === 0) {
// 未完成记录
return {
record_id: record.record_id,
student_id: record.student_id,
student_name: record.student_name,
total_score: 0,
answer_time: record.answer_time,
answer_status: record.answer_status,
// 维度得分字段(为了保持格式一致)
jgqh: 0,
gjsy: 0,
zrdd: 0,
gong: 0,
xyzs: 0,
yycx: 0,
syxx: 0,
zhi: 0,
jksh: 0,
smqq: 0,
ldys: 0,
neng: 0
};
} else {
// 已完成记录
const dimensionScores = recordScoreMap.get(record.record_id) || {};
const publicScore = (dimensionScores['家国情怀'] || 0) +
(dimensionScores['国际视野'] || 0) +
(dimensionScores['责任担当'] || 0);
const intelligenceScore = (dimensionScores['学业扎实'] || 0) +
(dimensionScores['勇于创新'] || 0) +
(dimensionScores['善于学习'] || 0);
const abilityScore = (dimensionScores['健康生活'] || 0) +
(dimensionScores['审美情趣'] || 0) +
(dimensionScores['劳动意识'] || 0);
return {
record_id: record.record_id,
student_id: record.student_id,
student_name: record.student_name,
total_score: record.total_score || 0,
answer_time: record.answer_time || "已提交",
answer_status: record.answer_status,
// 维度得分字段
jgqh: dimensionScores['家国情怀'] || 0,
gjsy: dimensionScores['国际视野'] || 0,
zrdd: dimensionScores['责任担当'] || 0,
gong: publicScore,
xyzs: dimensionScores['学业扎实'] || 0,
yycx: dimensionScores['勇于创新'] || 0,
syxx: dimensionScores['善于学习'] || 0,
zhi: intelligenceScore,
jksh: dimensionScores['健康生活'] || 0,
smqq: dimensionScores['审美情趣'] || 0,
ldys: dimensionScores['劳动意识'] || 0,
neng: abilityScore
};
}
});
return {
list,
total
};
} catch (error) {
console.error('获取无分页答题记录失败:', error);
return {
list: [],
total: 0
};
}
}
/**
* 获取所有答题记录ID(用于批量下载)
* @param filters 可选筛选条件
* @returns 所有符合条件的答题记录ID数组
*/
export async function getAllRecordIds(filters: any = {}) {
// 合并筛选条件,默认只获取已完成的记录
const whereCondition = {
answer_status: STATE.,
...filters
};
const records = await selectDataListByParam(
TABLENAME.答题记录表,
whereCondition,
["record_id"]
);
if (!records.data || records.data.length === 0) {
return [];
}
return records.data.map((record: any) => record.record_id);
}
/**
* 分页批量下载答题记录数据
* @param page 页码
* @param pageSize 每页大小
* @returns 分页的批量数据
*/
export async function getBatchStudentDetailedByPage(page: number = 1, pageSize: number = 100) {
// 1. 获取分页的记录ID
const includeConf: any = {};
const pagedRecords = await selectDataToTableAssociationToPage(
TABLENAME.答题记录表,
includeConf,
{ answer_status: STATE. },
["record_id"],
page,
pageSize
);
if (!pagedRecords.data || pagedRecords.data.length === 0) {
return {
data: [],
total: 0,
page,
pageSize,
totalPages: 0
};
}
const recordIds = pagedRecords.data.map((record: any) => record.record_id);
// 2. 获取总记录数
const totalRecords = await selectDataListByParam(
TABLENAME.答题记录表,
{ answer_status: STATE. },
["record_id"]
);
const total = totalRecords.data?.length || 0;
const totalPages = Math.ceil(total / pageSize);
// 3. 批量获取详细数据
const batchData = await getBatchStudentDetailed(recordIds);
return {
data: batchData,
total,
page,
pageSize,
totalPages
};
}
/**
* 请求数据中心类型
*/
export enum OPERATIONALDATATYPE {
增加 = '/puxin/dataserver/mysql/table/add',
修改 = '/puxin/dataserver/mysql/table/update',
删除 = '/puxin/dataserver/mysql/table/delete',
查询单个 = '/puxin/dataserver/mysql/table/find/one',
查询多个 = '/puxin/dataserver/mysql/table/find/many',
分页查询 = '/puxin/dataserver/mysql/table/find/manytopage',
查询数据量 = '/puxin/dataserver/mysql/table/find/count',
多表联查 = '/puxin/dataserver/mysql/table/find/aggragate',
多表分页 = '/puxin/dataserver/mysql/table/find/aggragatetopage',
多表单个 = '/puxin/dataserver/mysql/table/find/aggragateone'
}
/**
* 表名
*/
export enum TABLENAME {
维度表 = "dimension",
题目表 = "question",
答题记录表 = "answer_record",
答题记录明细表 = "answer_record_detail",
狮子形象表 = "nm_lion_image",
统一用户表 = "uac_user",
管理后台用户 = "adminUser",
服务开启时间表 = "opening_time",
}
export enum TABLEID {
维度表 = "d",
题目表 = "q",
答题记录表 = "ar",
答题记录明细表 = "ard",
狮子形象表 = "nli",
统一用户表 = "uac",
管理后台用户 = "au",
服务开启时间表 = "ot",
}
export enum OPERATIONTYPEENUM {
= 1,
,
,
}
/**上传文件类型 */
export enum FILETYPE {
word = 1,
pdf,
图片,
视频,
多类型
}
export enum TYPEENUM {
string = 1,
number,
object,
array,
boolean,
}
export enum AUTHENTICATIONTYPEENNUM {
一般注册用户 = 0,
行政管理人员 = 1,
教职员工,
学生,
家长
}
export enum STATE {
= 0,
= 1
}
export enum ERRORENUM {
不存在表 = 1,
身份验证失败,
缺少必要参数_表名,
数据表不存在,
参数错误,
添加时数据对象为空,
修改时数据对象为空,
该方法仅可进行数据操作,
数据操作失败,
该方法仅可进行查询操作,
分页请设置当前页数,
数据查询失败,
该方法仅可进行联合查询操作,
数据联合查询失败,
INVALID_REQUEST,
INTERNAL_SERVER_ERROR,
文件不存在,
该身份证号码重复,
账号或密码错误,
请求参数错误,
您的登录已失效,
答题记录不存在,
系统繁忙请稍后重试,
网络连接失败,
服务不可用,
请求超时,
获取用户信息失败,
非法登录,
重复答题,
数据不存在
}
/**
* 只用做code码定义
*/
export enum ERRORCODEENUM {
身份验证失败 = 5001,
缺少必要参数_表名 = 5002,
数据表不存在,
}
let bizErrorMsgMap = {};
for (let key in ERRORENUM) {
bizErrorMsgMap[ERRORENUM[key]] = key;
}
export function getBizMsg(param) {
return bizErrorMsgMap[param];
}
\ No newline at end of file
/**
* 表统一管理
*/
export const TablesConfig = {}
export let EccTableConfig = {};
function initEccTableConfig() {
for(let tableChName in TablesConfig) {
let {tableName, schema} = TablesConfig[tableChName];
EccTableConfig[tableName] ={};
for (let filesName in schema) {
let valueType = typeof schema[filesName];
let value = schema[filesName];
EccTableConfig[tableName][filesName] = {type:'', notMustHave:false};
if (valueType == "function") {
EccTableConfig[tableName][filesName].type = value.name;
} else if (valueType == "object") {
if (!value.type) {
EccTableConfig[tableName][filesName].type = `[${value[0].name}]`;
} else {
if (typeof value.type == "function") {
EccTableConfig[tableName][filesName].type = value.type.name
} else EccTableConfig[tableName][filesName].type = `[${value.type[0].name}]`;
if (value.index) EccTableConfig[tableName][filesName].notMustHave = true;
}
}
}
}
console.log('table eccConfig init success');
}
initEccTableConfig();
\ No newline at end of file
import { table } from "console";
import { Association } from "sequelize";
const { Sequelize, DataTypes } = require('sequelize');
export const TablesConfig = [
{
tableNameCn:'维度表',
tableName:'dimension',
schema:{
dimension_id: {
type:Sequelize.STRING(50), //表示属性的数据类型
allowNull:false, //表示当前列是否允许为空, false表示该列不能为空
primaryKey:true, //表示主键
unique:true //表示该列的值必须唯一
},
dimension_name:{type:Sequelize.STRING(50)}, //维度名称(如家国情怀、国际视野)
direction:{type:Sequelize.STRING(10)}, //所属大方向(公/智/能)
},
association: [
{type:"hasMany", check:"question", foreignKey:"dimension_id"}
]
},
{
tableNameCn:'题目表',
tableName:'question',
schema:{
question_id: {
type:Sequelize.STRING(50), //表示属性的数据类型
allowNull:false, //表示当前列是否允许为空, false表示该列不能为空
primaryKey:true, //表示主键
unique:true //表示该列的值必须唯一
},
dimension_id:{type:Sequelize.STRING(50), allowNull:false}, //关联维度表的维度ID
question_content:{type:Sequelize.TEXT}, //题目具体描述
question_order:{type:Sequelize.INTEGER(2)}, //题目在维度内的序号(1-4)
},
association: [
{type:"hasMany", check:"answer_record_detail", foreignKey:"question_id"}
]
},
{
tableNameCn:'答题记录表',
tableName:'answer_record',
schema:{
record_id: {
type:Sequelize.STRING(50),
allowNull:false,
primaryKey:true,
unique:true
},
student_id:{type:Sequelize.STRING(50), allowNull:false}, // 关联学生id
student_name:{type:Sequelize.STRING(50)}, // 学生名称
total_score:{type:Sequelize.INTEGER}, // 总分
answer_time:{type:Sequelize.DATE}, // 答题时间
answer_status:{type:Sequelize.INTEGER}, // 答题状态:0未完成 1已完成
},
association: [
{type:"hasMany", check:"answer_record_detail", foreignKey:"record_id"}
]
},
{
tableNameCn:'答题记录明细表',
tableName:'answer_record_detail',
schema:{
detail_id: {
type:Sequelize.STRING(50),
allowNull:false,
primaryKey:true,
unique:true
},
record_id:{type:Sequelize.STRING(50), allowNull:false}, // 关联答题记录表id
question_id:{type:Sequelize.STRING(50), allowNull:false}, // 关联题目id
score:{type:Sequelize.INTEGER}, // 题目得分(1-7分)
},
association: []
},
{
tableNameCn:'狮子形象表',
tableName:'nm_lion_image',
schema:{
lion_id: {
type:Sequelize.STRING(50),
allowNull:false,
primaryKey:true,
unique:true
},
standard_name:{type:Sequelize.STRING(255), allowNull:false}, // 狮子名称(标准名)
alias_name:{type:Sequelize.STRING(255), allowNull:true}, // 狮子名称(别名)
characteristic:{type:Sequelize.TEXT, allowNull:false}, // 特征描述
judgment_condition:{type:Sequelize.STRING(255), allowNull:false}, // 判定条件
magic_artifact:{type:Sequelize.STRING(255), allowNull:false}, // 法器(优势)名称
magic_artifact_text:{type:Sequelize.TEXT, allowNull:false}, // 法器文字描述
suggestion:{type:Sequelize.STRING(255), allowNull:false}, // 镜囊(建议)名称
suggestion_text:{type:Sequelize.TEXT, allowNull:false}, // 镜囊文字描述
lion_image:{type:Sequelize.STRING(255), allowNull:false}, // 狮子形象(图片地址)
},
association: []
},
{
tableNameCn: 'UAC统一用户表',
tableName: 'uac_user',
schema: {
user_id: {
type: DataTypes.STRING(20), //登录用户名(主键)
allowNull: false,
primaryKey: true,
unique:true
},
group_id: {type: DataTypes.INTEGER, allowNull: false}, //所属组ID
user_name: {type: DataTypes.STRING(20), allowNull: false}, //真实姓名
nickname: {type: DataTypes.STRING(20), allowNull: true}, //昵称
gender: {type: DataTypes.STRING(10), allowNull: true}, //性别(男/女)
email: {type: DataTypes.STRING(50), allowNull: true}, //电子邮件
user_type: {type: DataTypes.STRING(5), allowNull: false}, //用户身份代码(5位,分别代表一般注册用户、行政管理人员、教职员工、学生、家长)
active_flag: {type: DataTypes.INTEGER, allowNull: false, defaultValue: 2}, //用户状态标志(0-禁用,1-正常,2-未激活)
admin_flag: {type: DataTypes.INTEGER, allowNull: false, defaultValue: 0}, //帐号标志(-1:公共帐号,0:一般成员,1:所属组组长,2:超级管理员)
create_date: {type: DataTypes.STRING(19), allowNull: false}, //创建时间(格式:YYYY-MM-DD HH:MM:SS)
extend_info: {type: DataTypes.STRING(250), allowNull: true}, //用户扩展信息(JSON格式)
person_id: {type: DataTypes.STRING(15), allowNull: true}, //自然人ID(额外信息)
id_type: {type: DataTypes.INTEGER, allowNull: true}, //证件类型(1-居民身份证,2-香港特区身份证明...)
id_card: {type: DataTypes.STRING(30), allowNull: true}, //证件号码
name: {type: DataTypes.STRING(50), allowNull: true}, //证件姓名
verified_status: {type: DataTypes.INTEGER, allowNull: true, defaultValue: 0}, //实名认证状态(0-未认证,1-已认证,2-人工认证)
phone: { type: DataTypes.STRING(11), allowNull: true}, //手机号
birthday: {type: DataTypes.STRING(10), allowNull: true}, //生日(格式:YYYY-MM-DD)
password: {type: DataTypes.STRING(20), allowNull: true}, //密码(额外信息)
forbidden_reason: {type: DataTypes.INTEGER, allowNull: true}, //禁用状态标识(1-管理员禁用...7-毕业禁用)
xhjw_user_id: {type: DataTypes.INTEGER, allowNull: true}, //教务系统用户ID
is_sx: {type: DataTypes.INTEGER, allowNull: true, defaultValue: 0}, //师训标记(0-未关联,1-已关联)
order_id: {type: DataTypes.INTEGER, allowNull: true, defaultValue: 0}, //排序号(数字越大越靠前)
student_user_id: {type: DataTypes.STRING(20), allowNull: true}, //家长关联的学生帐号
is_train: {type: DataTypes.INTEGER, allowNull: true, defaultValue: 0}, //是否参训人员(0-否,1-是)
zw_mark: {type: DataTypes.INTEGER, allowNull: true, defaultValue: 0}, //政务标识(0-无,1-有,2-继承组)
eduid: {type: DataTypes.STRING(40), allowNull: true}, //EduID
uoid: {type: DataTypes.INTEGER, allowNull: true, defaultValue: 0}, //本组内排序号
user_mark: {type: DataTypes.STRING(20), allowNull: true}, //用户标识(政务,院务,校务)
},
association: []
},
{
tableNameCn:'管理后台用户',
tableName:'adminUser',
schema:{
aId: {
type:Sequelize.STRING(255), //表示属性的数据类型
allowNull:false, //表示当前列是否允许为空, false表示该列不能为空
primaryKey:true, //表示主键
unique:true //表示该列的值必须唯一
},
loginId:{type:Sequelize.STRING(255), allowNull:false}, //用户
token:{type:Sequelize.STRING(255)}, //token
tokenMs:{type:Sequelize.DATE}, //token的时间
name:{type:Sequelize.STRING(255)},//用户名称
permission:{type:Sequelize.INTEGER, allowNull: true, defaultValue: 0}, //本系统权限-是否管理员(0-否,1-是)
},
association: []
},
{
tableNameCn:'服务开启时间',
tableName:'opening_time',
schema:{
otId:{
type:Sequelize.STRING(255), //表示属性的数据类型
allowNull:false, //表示当前列是否允许为空, false表示该列不能为空
primaryKey:true, //表示主键
unique:true //表示该列的值必须唯一
},
startTime:{type:Sequelize.DATE}, //开始时间
endTime:{type:Sequelize.DATE}, //结束时间
isOpen:{type:Sequelize.INTEGER, allowNull: true, defaultValue: 0}, //是否启用该时间段配置(0-否,1-是)
updated_by:{type:Sequelize.STRING(255)}, //最后修改人
updated_at:{type:Sequelize.DATE}, //最后修改时间
},
association: []
}
];
\ No newline at end of file
const path = require('path');
import * as fs from "fs";
import { BizError } from "../util/bizError";
import { analysisXml } from "../util/myXML";
import { ServerConfig } from "../config/systemClass";
export let systemConfig = new ServerConfig();
const ConfigName = "serverConfig.xml";
export async function initConfig() {
try {
let buff = fs.readFileSync(path.join(__dirname.substring(0, __dirname.indexOf("out")), ConfigName));
let configStr = buff.toString();
let configInfo: any = await analysisXml(configStr);
if (!configInfo || !configInfo.config) throw new BizError('xml中无配置');
let { port, sign, dbServer, mysqldb, xmlRpcServer } = configInfo.config;
// 基本配置
systemConfig.port = parseInt(port[0]);
systemConfig.sign = sign[0];
systemConfig.dbPath = dbServer[0];
// MySQL配置
if (mysqldb) {
let dbConfigInfo = mysqldb[0];
systemConfig.mysqldb = { host: '', port: 0, user: '', pwd: '', dataBase: '' };
if (dbConfigInfo.mysqlHost && dbConfigInfo.mysqlPort && dbConfigInfo.mysqlUser && dbConfigInfo.dataBase) {
systemConfig.mysqldb.host = dbConfigInfo.mysqlHost[0];
systemConfig.mysqldb.port = parseInt(dbConfigInfo.mysqlPort[0]);
systemConfig.mysqldb.user = dbConfigInfo.mysqlUser[0];
systemConfig.mysqldb.pwd = dbConfigInfo.mysqlPwd[0] || "";
systemConfig.mysqldb.dataBase = dbConfigInfo.dataBase[0];
}
}
// XML-RPC服务器配置
if (xmlRpcServer) {
let rpcConfig = xmlRpcServer[0];
systemConfig.xmlRpcServer = {
enabled: rpcConfig.enabled && rpcConfig.enabled[0] === 'true',
port: rpcConfig.port ? parseInt(rpcConfig.port[0]) : 8000,
host: rpcConfig.host ? rpcConfig.host[0] : 'localhost',
path: rpcConfig.path ? rpcConfig.path[0] : '/rpc',
auth: {
username: rpcConfig.auth && rpcConfig.auth[0].username ? rpcConfig.auth[0].username[0] : 'K12RPC',
password: rpcConfig.auth && rpcConfig.auth[0].password ? rpcConfig.auth[0].password[0] : 'K12RPC!Pwd1901'
}
};
} else {
// 默认配置
systemConfig.xmlRpcServer = {
enabled: false,
port: 8000,
host: 'localhost',
path: '/rpc',
auth: {
username: 'K12RPC',
password: 'K12RPC!Pwd1901'
}
};
}
console.log("config init success");
console.log("XML-RPC配置:", systemConfig.xmlRpcServer);
} catch(err) {
console.log('ERROR => 服务器配置解析错误 请检查根目录下 serverConfig.xml 文件是否正确');
console.log(err);
throw new BizError("服务器配置解析错误 请检查根目录下 serverConfig.xml 文件是否正确");
}
}
function analysisMongoConnectStr(path, port, dataBase, w, timeOutMs) {
return `mongodb://${path}:${port}/${dataBase}?w=${w}&wtimeoutMS=${timeOutMs}`;
}
\ No newline at end of file
/**
* 系统配置类
*
*/
export class ServerConfig {
/**系统配置 */
port:number;
sign:string;
dbPath:string;
mysqldb:{
host:string,
port:number,
user:string,
pwd:string,
dataBase:string,
}
/** XML-RPC服务器配置 */
xmlRpcServer: {
enabled: boolean;
port: number;
host: string;
path: string;
auth: {
username: string;
password: string;
};
};
}
\ No newline at end of file
import { mysqlModelMap } from "../model/sqlModelBind";
/**
* 添加数据
* @param tableModel
* @param data
* @returns
*/
export async function addData(tableName:string, data:any) {
let tableModel = mysqlModelMap[tableName];
let dataArray = [];
if (!Array.isArray(data)) {
dataArray.push(data);
} else dataArray = data;
await tableModel.bulkCreate(dataArray);
return { isSuccess:true };
}
\ No newline at end of file
export async function delData(tableModel, param) {
await tableModel.destroy({where:param});
return {isSuccess:true};
}
\ No newline at end of file
import { Op, Sequelize } from "sequelize";
import { ERRORENUM } from "../config/errorEnum";
import { mysqlModelMap } from "../model/sqlModelBind";
import { BizError } from "../util/bizError";
/**
* where条件查询参数
* @param param
* %like%:模糊查询 {列名: {"%like%": }}
* %gt%:大于 {列名: {"%gt%": }}
* %gte%:大于等于 {列名: {"%gte%": }}
* %lt%:小于 {列名: {"%lt%": }}
* %lte%:小于等于 {列名: {"%lte%": }}
* %between%:查询范围内数据 {列名: {"%between%": ["开始参数", "结束参数"]}} ---BETWEEN 开始参数 AND 结束参数 列>开始参数 and 列<结束参数
* %notBetween%:查询不在范围内数据 {列名: {"%notBetween%": ["开始参数", "结束参数"]}} ---NOT BETWEEN 开始参数 AND 结束参数
* %orderDesc%: order by DESC {"%orderDesc%": "列名"}
* %limit%: {"%limit%": 数量}
* @param column
* @returns
*/
function analysisParamToWhere(param, column) {
let where = {};
let order = [];
let group = "";
let literal = "";
let limit = 0;
for (let key in param) {
if (typeof param[key] == "object") {
where[key] = {};
for (let whereKey in param[key]) {
switch (whereKey) {
case "%like%":
where[key][Op.like] = `%${param[key]["%like%"]}%`;
break;
case "%gt%":
where[key][Op.gt] = param[key]["%gt%"];
break;
case "%gte%":
where[key][Op.gte] = param[key]["%gte%"];
break;
case "%lt%":
where[key][Op.lt] = param[key]["%lt%"];
break;
case "%lte%":
where[key][Op.lte] = param[key]["%lte%"];
break;
case "%between%":
where[key][Op.between] = param[key]["%between%"];
break;
case "%notBetween%":
where[key][Op.notBetween] = param[key]["%notBetween%"];
break;
case "%in%":
where[key][Op.in] = param[key]["%in%"];
break;
case "%notIn%":
where[key][Op.notIn] = param[key]["%notIn%"];
break;
case "%ne%":
where[key][Op.ne] = param[key]["%ne%"];
break;
case "%regexp%":
where[key][Op.regexp] = param[key]["%regexp%"];
break;
}
}
} else {
switch (key) {
case "%orderDesc%":
order = [[Sequelize.col(param[key]), "DESC"]];
break;
case "%orderAsc%":
order = [[Sequelize.col(param[key]), "ASC"]];
break;
case "%limit%":
limit = param[key];
break;
case "%group%":
group = param[key];
break;
case "%literal%":
literal = param["%literal%"];
break;
default: where[key] = param[key];
}
}
}
let selectParam: any = { where };
if (column && column.length) selectParam.attributes = column;
if (order && order.length) selectParam.order = order;
if (limit) selectParam.limit = limit;
if (group) selectParam.group = group;
if (literal) selectParam.where = Sequelize.literal(literal);
return selectParam;
}
/**
* 查询单个数据
* @param tableModel 表对象
* @param param
* @returns
*/
export async function selectOneDataByParam(tableName, param, column) {
let tableModel = mysqlModelMap[tableName];
let selectParam = analysisParamToWhere(param, column);
let data = await tableModel.findOne(selectParam);
data = data || {};
return { data };
}
/**
* 查询多个数据
* @param tableName 表对象
* @param param
* @returns
*/
export async function selectDataListByParam(tableName, param, column) {
let tableModel = mysqlModelMap[tableName];
let selectParam = analysisParamToWhere(param, column);
let data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 分页查询
* @param tableModel
* @param param
* @param pageNumber
* @param pageSize
* @returns
*/
export async function selectDataListToPageByParam(tableName, param, column, pageNumber: number, pageSize: number) {
let tableModel = mysqlModelMap[tableName];
let selectParam: any = analysisParamToWhere(param, column);
selectParam.limit = pageSize || 10;
selectParam.offset = (pageNumber - 1) * 10;
let data = await tableModel.findAll(selectParam);
return { data };
}
export async function selectDataCountByParam(tableName, param) {
let tableModel = mysqlModelMap[tableName];
let selectParam: any = analysisParamToWhere(param, []);
let data = await tableModel.count(selectParam);
return { data };
}
export async function associationSelect(tableName: string, param) {
let model = mysqlModelMap[tableName];
if (!model) throw new BizError(ERRORENUM.不存在表);
let data = await model.aggragateData(param);
return {data};
// try {
// let data = await model.aggragateData(param);
// return { data };
// } catch (error) {
// throw new BizError(ERRORENUM.数据查询失败, error.message);
// }
}
/**
* 多表联查 列表
* @param tableModel
* @param includeConf {"表名":["",""] }
* @param param
* @param column
* @returns
*/
export async function selectDataToTableAssociation(tableName, includeConf, param, column) {
let tableModel = mysqlModelMap[tableName];
let include = [];
for (let tableName in includeConf) {
if (!mysqlModelMap[tableName]) throw new BizError(ERRORENUM.不存在表, `尝试进行多表联查,但是不存在${tableName}`);
let {where, column} = includeConf[tableName];
let includeInfomation = analysisParamToWhere(where, column);
includeInfomation.model = mysqlModelMap[tableName];
include.push(includeInfomation);
}
let selectParam: any = analysisParamToWhere(param, column);
selectParam.include = include;
let data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 多表联查 分页
* @param tableModel
* @param includeConf {"表名":["",""] }
* @param param
* @param column
* @returns
*/
export async function selectDataToTableAssociationToPage(tableName, includeConf, param, column, pageNumber: number, pageSize: number) {
let tableModel = mysqlModelMap[tableName];
let include = [];
for (let tableName in includeConf) {
if (!mysqlModelMap[tableName]) throw new BizError(ERRORENUM.不存在表, `尝试进行多表联查,但是不存在${tableName}`);
let { where, column } = includeConf[tableName];
let includeInfomation = analysisParamToWhere(where, column);
includeInfomation.model = mysqlModelMap[tableName];
include.push(includeInfomation);
}
let selectParam: any = analysisParamToWhere(param, column);
selectParam.include = include;
selectParam.limit = pageSize || 10;
selectParam.offset = (pageNumber - 1) * 10;
let data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 多表查询单个
* @param tableModel
* @param includeConf
* @param param
* @param column
* @returns
*/
export async function selectOneDataToTableAssociation(tableName, includeConf, param, column) {
let tableModel = mysqlModelMap[tableName];
let include = [];
for (let tableName in includeConf) {
if (!mysqlModelMap[tableName]) throw new BizError(ERRORENUM.不存在表, `尝试进行多表联查,但是不存在${tableName}`);
let { where, column } = includeConf[tableName];
let includeInfomation = analysisParamToWhere(where, column);
includeInfomation.model = mysqlModelMap[tableName];
include.push(includeInfomation);
}
let selectParam: any = analysisParamToWhere(param, column);
selectParam.include = include;
let data = await tableModel.findOne(selectParam);
data = data || {};
return { data };
}
\ No newline at end of file
import { Op, Sequelize, Model, WhereOptions, Order, IncludeOptions } from "sequelize";
import { ERRORENUM } from "../config/errorEnum";
import { mysqlModelMap } from "../model/sqlModelBind";
import { BizError } from "../util/bizError";
// 定义类型接口
interface QueryParam {
[key: string]: any;
"%orderDesc%"?: string;
"%orderAsc%"?: string;
"%limit%"?: number;
"%group%"?: string;
"%literal%"?: string;
}
interface AnalysisResult {
where: WhereOptions;
attributes?: string[];
order?: Order;
limit?: number;
group?: string;
}
interface IncludeConfig {
[tableName: string]: {
where?: QueryParam;
column?: string[];
};
}
/**
* where条件查询参数解析
* 支持的操作符:
* %like%:模糊查询 {列名: {"%like%": "值"}}
* %gt%:大于 {列名: {"%gt%": 值}}
* %gte%:大于等于 {列名: {"%gte%": 值}}
* %lt%:小于 {列名: {"%lt%": 值}}
* %lte%:小于等于 {列名: {"%lte%": 值}}
* %between%:查询范围内数据 {列名: {"%between%": ["开始", "结束"]}}
* %notBetween%:查询不在范围内数据 {列名: {"%notBetween%": ["开始", "结束"]}}
* %in%:IN查询 {列名: {"%in%": [值1, 值2]}}
* %notIn%:NOT IN查询 {列名: {"%notIn%": [值1, 值2]}}
* %ne%:不等于 {列名: {"%ne%": 值}}
* %regexp%:正则表达式 {列名: {"%regexp%": "模式"}}
* %orderDesc%: 降序排序 {"%orderDesc%": "列名"}
* %orderAsc%: 升序排序 {"%orderAsc%": "列名"}
* %limit%: 限制数量 {"%limit%": 数量}
* %group%: 分组 {"%group%": "列名"}
* %literal%: 原生SQL {"%literal%": "SQL语句"}
*/
function analysisParamToWhere(param: QueryParam, column?: string[]): AnalysisResult {
const where: WhereOptions = {};
const order: Order = [];
let group: string | undefined;
let literal: string | undefined;
let limit: number | undefined;
for (const key in param) {
if (typeof param[key] === "object" && param[key] !== null && !Array.isArray(param[key])) {
where[key] = {};
for (const whereKey in param[key]) {
switch (whereKey) {
case "%like%":
where[key][Op.like] = `%${param[key]["%like%"]}%`;
break;
case "%gt%":
where[key][Op.gt] = param[key]["%gt%"];
break;
case "%gte%":
where[key][Op.gte] = param[key]["%gte%"];
break;
case "%lt%":
where[key][Op.lt] = param[key]["%lt%"];
break;
case "%lte%":
where[key][Op.lte] = param[key]["%lte%"];
break;
case "%between%":
where[key][Op.between] = param[key]["%between%"];
break;
case "%notBetween%":
where[key][Op.notBetween] = param[key]["%notBetween%"];
break;
case "%in%":
where[key][Op.in] = param[key]["%in%"];
break;
case "%notIn%":
where[key][Op.notIn] = param[key]["%notIn%"];
break;
case "%ne%":
where[key][Op.ne] = param[key]["%ne%"];
break;
case "%regexp%":
where[key][Op.regexp] = param[key]["%regexp%"];
break;
}
}
} else {
switch (key) {
case "%orderDesc%":
order.push([Sequelize.col(param[key]), "DESC"]);
break;
case "%orderAsc%":
order.push([Sequelize.col(param[key]), "ASC"]);
break;
case "%limit%":
limit = Number(param[key]);
break;
case "%group%":
group = param[key];
break;
case "%literal%":
literal = param[key];
break;
default:
where[key] = param[key];
}
}
}
const result: AnalysisResult = { where };
if (column && column.length) result.attributes = column;
if (order.length) result.order = order;
if (limit) result.limit = limit;
if (group) result.group = group;
if (literal) result.where = Sequelize.literal(literal) as any;
return result;
}
/**
* 获取表模型
*/
function getTableModel(tableName: string): typeof Model {
let tableModel = mysqlModelMap[tableName];
if (!tableModel) {
throw new BizError(ERRORENUM.不存在表, `表 ${tableName} 不存在`);
}
return tableModel;
}
/**
* 查询单个数据
*/
export async function selectOneDataByParam(tableName: string, param: QueryParam, column?: string[]) {
let tableModel = mysqlModelMap[tableName];
const selectParam = analysisParamToWhere(param, column);
const data = await tableModel.findOne(selectParam);
return { data: data || {} };
}
/**
* 查询多个数据
*/
export async function selectDataListByParam(tableName: string, param: QueryParam, column?: string[]) {
let tableModel = mysqlModelMap[tableName];
const selectParam = analysisParamToWhere(param, column);
const data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 分页查询
*/
export async function selectDataListToPageByParam(tableName: string, param: QueryParam, column: string[], pageNumber: number, pageSize: number ) {
let tableModel = mysqlModelMap[tableName];
const selectParam: any = analysisParamToWhere(param, column);
selectParam.limit = pageSize || 10;
selectParam.offset = (pageNumber - 1) * pageSize;
const data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 查询数据总数
*/
export async function selectDataCountByParam(tableName: string, param: QueryParam) {
let tableModel = mysqlModelMap[tableName];
const selectParam: any = analysisParamToWhere(param, []);
const count = await tableModel.count(selectParam);
return { data: count };
}
/**
* 关联查询
*/
export async function associationSelect(tableName: string, param: any) {
let model = mysqlModelMap[tableName];
if (!model) throw new BizError(ERRORENUM.不存在表);
const data = await model.aggragateData(param);
return { data };
}
/**
* 构建关联查询的include配置
*/
function buildIncludeOptions(includeConf: IncludeConfig): IncludeOptions[] {
return Object.entries(includeConf).map(([tableName, config]) => {
const tableModel = mysqlModelMap[tableName];
if (!tableModel) {
throw new BizError(ERRORENUM.不存在表, `尝试进行多表联查,但是不存在 ${tableName}`);
}
const includeOptions: IncludeOptions = {
model: tableModel,
...analysisParamToWhere(config.where || {}, config.column)
};
return includeOptions;
});
}
/**
* 多表联查 - 列表
*/
export async function selectDataToTableAssociation(tableName: string, includeConf: IncludeConfig, param: QueryParam, column?: string[]) {
let tableModel = mysqlModelMap[tableName];
const include = buildIncludeOptions(includeConf);
const selectParam: any = analysisParamToWhere(param, column);
selectParam.include = include;
const data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 多表联查 - 分页
*/
export async function selectDataToTableAssociationToPage(tableName:string, includeConf:IncludeConfig, param:QueryParam, column:string[], pageNumber:number, pageSize:number) {
let tableModel = mysqlModelMap[tableName];
const include = buildIncludeOptions(includeConf);
const selectParam: any = analysisParamToWhere(param, column);
selectParam.include = include;
selectParam.limit = pageSize || 10;
selectParam.offset = (pageNumber - 1) * pageSize;
const data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 多表联查 - 单个
*/
export async function selectOneDataToTableAssociation(tableName:string,includeConf:IncludeConfig,param:QueryParam,column?:string[]) {
let tableModel = mysqlModelMap[tableName];
const include = buildIncludeOptions(includeConf);
const selectParam: any = analysisParamToWhere(param, column);
selectParam.include = include;
const data = await tableModel.findOne(selectParam);
return { data: data || {} };
}
// ==================== 新增的通用方法 ====================
/**
* 查询并计数(返回数据和总数)
*/
export async function selectDataWithCount( tableName: string, param: QueryParam, column?: string[] ) {
let tableModel = mysqlModelMap[tableName];
const selectParam = analysisParamToWhere(param, column);
const result = await tableModel.findAndCountAll(selectParam);
return { data: result.rows, count: result.count };
}
/**
* 分页查询(增强版,返回数据、总数和分页信息)
*/
export async function selectPaginatedData( tableName: string, param: QueryParam, column: string[], pageNumber: number, pageSize: number ) {
let tableModel = mysqlModelMap[tableName];
const selectParam: any = analysisParamToWhere(param, column);
selectParam.limit = pageSize;
selectParam.offset = (pageNumber - 1) * pageSize;
const result = await tableModel.findAndCountAll(selectParam);
return {
data: result.rows,
pagination: {
page: pageNumber,
pageSize,
total: result.count,
totalPages: Math.ceil(result.count / pageSize)
}
};
}
/**
* 自定义排序查询
* @param orders 排序数组,格式: [['column1', 'DESC'], ['column2', 'ASC']]
*/
export async function selectDataWithCustomOrder( tableName: string, param: QueryParam, column: string[], orders: [string, 'ASC' | 'DESC'][] ) {
let tableModel = mysqlModelMap[tableName];
const selectParam: any = analysisParamToWhere(param, column);
// 添加自定义排序
selectParam.order = orders.map(([column, direction]) =>
[Sequelize.col(column), direction]
);
const data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 查询最大值
*/
export async function selectMaxValue( tableName: string, column: string, whereParam?: QueryParam ) {
let tableModel = mysqlModelMap[tableName];
const where = whereParam ? analysisParamToWhere(whereParam, []).where : {};
const result = await tableModel.max(column, { where });
return { max: result };
}
/**
* 查询最小值
*/
export async function selectMinValue( tableName: string, column: string, whereParam?: QueryParam ) {
let tableModel = mysqlModelMap[tableName];
const where = whereParam ? analysisParamToWhere(whereParam, []).where : {};
const result = await tableModel.min(column, { where });
return { min: result };
}
/**
* 查询平均值
*/
export async function selectAvgValue( tableName: string, column: string, whereParam?: QueryParam ) {
let tableModel = mysqlModelMap[tableName];
const where = whereParam ? analysisParamToWhere(whereParam, []).where : {};
const result = await tableModel.average(column, { where });
return { average: result };
}
/**
* 查询求和
*/
export async function selectSumValue( tableName: string, column: string, whereParam?: QueryParam ) {
let tableModel = mysqlModelMap[tableName];
const where = whereParam ? analysisParamToWhere(whereParam, []).where : {};
const result = await tableModel.sum(column, { where });
return { sum: result };
}
/**
* 分页查询(支持自定义排序)
* @param tableName 表名
* @param param 查询参数
* @param column 查询列
* @param pageNumber 页码
* @param pageSize 每页大小
* @param orders 排序数组,格式: [['column1', 'DESC'], ['column2', 'ASC']]
*/
export async function selectPaginatedDataWithOrder(
tableName: string,
param: QueryParam,
column: string[],
pageNumber: number,
pageSize: number,
orders: [string, 'ASC' | 'DESC'][] = []
) {
let tableModel = mysqlModelMap[tableName];
const selectParam: any = analysisParamToWhere(param, column);
selectParam.limit = pageSize;
selectParam.offset = (pageNumber - 1) * pageSize;
// 添加自定义排序
if (orders && orders.length > 0) {
selectParam.order = orders.map(([column, direction]) =>
[Sequelize.col(column), direction]
);
}
const result = await tableModel.findAndCountAll(selectParam);
return {
data: result.rows,
pagination: {
page: pageNumber,
pageSize,
total: result.count,
totalPages: Math.ceil(result.count / pageSize)
}
};
}
import { Op, Sequelize } from "sequelize";
import { mysqlModelMap } from "../model/sqlModelBind";
/**
* where条件查询参数
* @param param
* %like%:模糊查询 {列名: {"%like%": }}
* %gt%:大于 {列名: {"%gt%": }}
* %gte%:大于等于 {列名: {"%gte%": }}
* %lt%:小于 {列名: {"%lt%": }}
* %lte%:小于等于 {列名: {"%lte%": }}
* %between%:查询范围内数据 {列名: {"%between%": ["开始参数", "结束参数"]}} ---BETWEEN 开始参数 AND 结束参数
* %notBetween%:查询不在范围内数据 {列名: {"%notBetween%": ["开始参数", "结束参数"]}} ---NOT BETWEEN 开始参数 AND 结束参数
* %orderDesc%: order by DESC {"%orderDesc%": "列名"}
* %limit%: {"%limit%": 数量}
* @returns
*/
function analysisParamToWhere(param) {
let where = {};
let order = [];
let limit = 0;
for (let key in param) {
if (typeof param[key] == "object") {
where[key] = {};
for (let whereKey in param[key]){
switch(whereKey) {
case "%like%":
where[key][Op.like] = `%${param[key]["%like%"]}%`;
break;
case "%gt%":
where[key][Op.gt] = param[key]["%gt%"];
break;
case "%gte%":
where[key][Op.gte] = param[key]["%gte%"];
break;
case "%lt%":
where[key][Op.lt] = param[key]["%lt%"];
break;
case "%lte%":
where[key][Op.lte] = param[key]["%lte%"];
break;
case "%between%":
where[key][Op.between] = param[key]["%between%"];
break;
case "%notBetween%":
where[key][Op.notBetween] = param[key]["%notBetween%"];
break;
case "%in%":
where[key][Op.in] = param[key]["%in%"];
break;
case "%notIn%":
where[key][Op.notIn] = param[key]["%notIn%"];
break;
}
}
}else {
switch (key) {
case "%orderDesc%":
order = [[Sequelize.col(param[key]), "DESC"]];
break;
case "%orderAsc%":
order = [[Sequelize.col(param[key]), "ASC"]];
break;
case "%limit%":
limit = param[key];
break;
default: where[key] = param[key];
}
}
}
let selectParam:any = {where};
if (order && order.length) selectParam.order = order;
if (limit) selectParam.limit = limit;
return selectParam;
}
export async function updateManyData(tableName, param:object, data:object) {
let tableModel = mysqlModelMap[tableName];
let where = analysisParamToWhere(param);
await tableModel.update(data, where);
return {isSuccess:true};
}
\ No newline at end of file
import { systemConfig } from "../config/serverConfig";
//导入sequelize模块
const Sequelize = require('sequelize');
var mysqlDB;
export async function initMysqlDB() {
mysqlDB = new Sequelize(systemConfig.mysqldb.dataBase,systemConfig.mysqldb.user,systemConfig.mysqldb.pwd,{
host:systemConfig.mysqldb.host,
port:systemConfig.mysqldb.port,
dialect:'mysql', //数据库类型
pool:{ //数据库连接池
max:20, //最大连接对象的个数
min:5, //最小连接对象的个数
idle:1000 //最长等待时间,单位为毫秒
},
timezone: '+08:00', //东八时区
dialectOptions: {
dateStrings: true,
typeCast: true
},
});
}
export { mysqlDB };
\ No newline at end of file
import { initConfig, systemConfig} from "./config/serverConfig";
import * as mysqlDB from "./db/mysqlInit";
import { initMysqlModel } from "./model/sqlModelBind";
import { httpServer } from "./net/http_server";
import { initUACIntegration } from './biz/UAC';
import { BackupService } from './biz/dataBackup';
async function lanuch() {
/**初始化配置解析 */
await initConfig();
/**初始化数据库 */
// await mongoDB.initDB();
// await initModel();
/**初始化sql */
await mysqlDB.initMysqlDB();
await initMysqlModel();
/**创建http服务 */
httpServer.createServer(systemConfig.port);
console.log('This indicates that the server is started successfully.');
backup();
// 应用启动时初始化UAC集成
const xmlRpcServer = await initUACIntegration();
if (xmlRpcServer) {
console.log('🟢 XML-RPC服务器启动成功,正在监听端口:', systemConfig.xmlRpcServer.port);
} else {
console.error('🔴 XML-RPC服务器启动失败');
}
}
lanuch();
function backup() {
const backupService = new BackupService({
host: systemConfig.mysqldb.host,
user: systemConfig.mysqldb.user,
password: systemConfig.mysqldb.pwd,
database: systemConfig.mysqldb.dataBase,
localBackupPath: './nm_backups', // 本地临时备份路径
remoteBackupPath: '/mnt/nm_gzn/backups', // 远程服务器备份路径
remoteHost: '123.207.147.179', //远程服务器ip
remoteUser: 'root', //远程服务器用户名
remotePassword: 'GNIWT20110919!@@@', // 远程服务器密码
sshPort: 22, // SSH端口
retentionDays: 30,
keepLocalBackup: true
});
// 先进行测试
backupService.testBackup()
.then(() => {
console.log('启动定时备份(每天凌晨2:00执行)');
// 启动定时备份(每天凌晨2:00执行)
backupService.startScheduledBackup(2, 0);
})
.catch(error => {
console.error('备份配置测试失败,请检查配置:', error);
});
// 也可以立即执行一次备份
// backupService.backupNow()
// .then(result => console.log('立即备份完成:', result))
// .catch(error => console.error('立即备份失败:', error));
}
import { bizlive } from "tencentcloud-sdk-nodejs";
import { ERRORCODEENUM } from "../config/errorEnum";
/**
* 中间件 错误返回
* @param err
* @param req
* @param res
* @param next
*/
export function httpErrorHandler(err, req, res, next) {
console.log("in httpErrorHandler");
console.log(err);
//todo 自定义错误编码
if (err) {
if ( ERRORCODEENUM[err.message] ) {
res.success({success:false, msg:err.message, code:ERRORCODEENUM[err.message]});
next();
}
else {
res.success({success:false, msg:err.message, code:500});
next();
}
}
}
\ No newline at end of file
import { ERRORENUM } from "../config/errorEnum";
import { systemConfig } from "../config/serverConfig";
import { EccTableConfig } from "../config/mongoTableConfig";
import { BizError } from "../util/bizError";
import { mysqlModelMap } from "../model/sqlModelBind";
import { selectOneDataByParam } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
/**
* 中间件 校验连接对象token
* @param req
* @param res
* @param next
* @returns
*/
export async function checkMongoSign(req, res, next) {
if (!req.headers) req.headers = {};
let sign = req.headers.sign;
let table = req.headers.table;
if (sign != systemConfig.sign) return next( new BizError(ERRORENUM.身份验证失败, `传入的sign值为:${sign}`) );
if (!table) return next( new BizError(ERRORENUM.缺少必要参数_表名, `传入的table值为:${table}`) );
if (!EccTableConfig[table]) return next( new BizError(ERRORENUM.不存在表, `传入的table值为:${table}`) );
next();
}
/**
* 中间件 校验连接对象token
* @param req
* @param res
* @param next
* @returns
*/
export async function checkMySqlSign(req, res, next) {
if (!req.headers) req.headers = {};
let sign = req.headers.sign;
let table = req.headers.table;
if (sign != systemConfig.sign) return next( new BizError(ERRORENUM.身份验证失败, `传入的sign值为:${sign}`) );
if (!table) return next( new BizError(ERRORENUM.缺少必要参数_表名, `传入的table值为:${table}`) );
if (!mysqlModelMap[table]) return next( new BizError(ERRORENUM.不存在表, `传入的table值为:${table}`) );
req.tableModel = mysqlModelMap[table];
next();
}
export async function checkUser(req, res, next) {
if (!req.headers) req.headers = {};
const userId = req.headers.userid || "";
const reqToken = req.headers.token || "";
if (!userId) return next(new BizError(ERRORENUM.身份验证失败, `studentId:${userId} token:${reqToken}`));
let userDbData:any = await selectOneDataByParam(TABLENAME.管理后台用户, {loginId:userId}, ["loginId", "token"]);
if (!userDbData || !userDbData.data || !userDbData.data.loginId) return next(new BizError(ERRORENUM.非法登录, `userId:${userId} token:${reqToken}`));
if (userDbData.data.token != reqToken) return next(new BizError(ERRORENUM.身份验证失败, `studentId:${userId} `));
const userName = req.headers.username || "";
req.userInfo = {
studentId:userId,
studentName:userName
}
next();
}
export function watch(req, res, next) {
res.success = success.bind({res:res, req:req});
return next();
}
/**
* 中间件正确返回方法
* @param data
*/
function success(data) {
let resultPack;
if (data ) {
if ( data.success === undefined || data.success === true ) {
resultPack = {data, success:true, code:200};
}
else {
resultPack = data;
}
}else {
resultPack = {code:500, success:false, msg:'result is null'};
}
this.res.send(resultPack);
}
/**
* mysql 数据层
*/
import { TablesConfig } from "../config/mysqlTableConfig";
import { mysqlDB } from "../db/mysqlInit";
let mysqlModelMap = {};
export async function initMysqlModel() {
/**初始化表 */
for (let i =0; i < TablesConfig.length; i++) {
let { tableName, schema } = TablesConfig[i];
let schemaConf = {
freezeTableName:true, //true表示使用给定的表名,false表示模型名后加s作为表名
timestamps:false //true表示给模型加上时间戳属性(createAt updateAt),false表示不带时间戳属性
};
let model = mysqlDB.define( tableName, schema, schemaConf);
mysqlModelMap[tableName] = await model.sync({}).then();
// try {
// await model.sync({ force: false });
// mysqlModelMap[tableName] = model;
// } catch (error) {
// console.error(`同步表 ${tableName} 失败:`, error);
// }
}
/**初始化表关联 */
for (let i =0; i < TablesConfig.length; i++) {
let { tableName, association } = TablesConfig[i];
association.forEach( (item:any) => {
if (item) {
let {type, check, foreignKey} = item;
if (type == "hasOne") {
mysqlModelMap[check].hasOne(mysqlModelMap[tableName]);
} else if (type == "hasMany") {
mysqlModelMap[tableName].hasMany(mysqlModelMap[check], {foreignKey});
}
mysqlModelMap[check].belongsTo(mysqlModelMap[tableName], {foreignKey});
// else if (type === "belongsTo") {
// mysqlModelMap[tableName].belongsTo(mysqlModelMap[check], { foreignKey });
// }
console.log("---->", mysqlModelMap[tableName].getTableName());
console.log("====>", mysqlModelMap[check].getTableName());
}
});
}
}
export { mysqlModelMap };
\ No newline at end of file
import express = require('express');
import bodyParser = require('body-parser');
import routers = require('../routers/router');
import compression = require('compression');
import { watch } from '../middleware/watch';
import { httpErrorHandler } from '../middleware/httpErrorHandler';
import * as path from "path";
// import * as fallback from 'express-history-api-fallback';
import historyFallback from 'express-history-api-fallback'; // 正确导入默认导出的函数
export class httpServer {
static createServer(port: number) {
var httpServer = express();
httpServer.all('*', (req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header('Access-Control-Allow-Headers', 'Content-Type,request-origin,userid,token,username');
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
res.header('Access-Control-Allow-Credentials', 'true');
res.header("X-Powered-By", ' 3.2.1');
next();
});
httpServer.use(express.static('public'));
httpServer.use(express.static('img'));
httpServer.use(compression());
httpServer.use(watch);
httpServer.use(bodyParser.json({ limit: "500kb" }));
routers.setRouter(httpServer);
httpServer.use(httpErrorHandler);
const root = path.join(__dirname, "../../public");
httpServer.use(express.static(root))
// 使用 fallback 函数
// httpServer.use(fallback('index.html', { root }));
httpServer.use(historyFallback('index.html', { root }));
console.log('web listen on port:' + port);
httpServer.listen(port);
console.log('server listen on port:' + port);
return httpServer;
}
}
\ No newline at end of file
/**
* 公智能评价答题
*/
import asyncHandler from 'express-async-handler';
import * as questionBiz from '../biz/question';
import * as authenticationBiz from '../biz/authentication';
import { checkUser } from '../middleware/user';
export function setRouter(httpServer) {
/**单点登录 */
httpServer.post('/gzn/sso/verify', asyncHandler(authenticationBiz.checkSession));
/**答题 */
httpServer.post('/gzn/question/checkanswered', checkUser, asyncHandler(checkStudentHasAnswered));
httpServer.post('/gzn/question/getppeningtime', checkUser, asyncHandler(getFirstOpeningTimeConfig));
httpServer.post('/gzn/question/setopeningtime', checkUser, asyncHandler(setOpeningTimeConfig));
httpServer.post('/gzn/question/answer', checkUser, asyncHandler(questionsByDirection));
httpServer.post('/gzn/question/completeanswer', checkUser, asyncHandler(completeAnswer));
httpServer.post('/gzn/question/finishanswer', checkUser, asyncHandler(finishAnswer));
httpServer.post('/gzn/question/answerresult', checkUser, asyncHandler(answerResultWithLionImage));
/**管理答题记录 */
httpServer.post('/gzn/admin/allanswerrecords', checkUser, asyncHandler(allStudentAnswerRecords));
httpServer.post('/gzn/admin/studentdetailed', checkUser, asyncHandler(studentDetailed));
httpServer.post('/gzn/admin/batchstudentdetailed', checkUser, asyncHandler(batchStudentDetailed));
}
/**
* 判断学生是否重复答题
* @param req
* @param res
*/
async function checkStudentHasAnswered(req, res) {
const UserInfo = req.userInfo;
let result = await questionBiz.checkStudentHasAnswered(UserInfo.studentId);
res.success(result);
}
/**
* 服务时间配置回显
* @param req
* @param res
*/
async function getFirstOpeningTimeConfig(req, res) {
const userInfo = req.userInfo;
let result = await questionBiz.getFirstOpeningTimeConfig();
res.success(result);
}
/**
* 创建或更新服务时间配置
* @param req
* @param res
*/
async function setOpeningTimeConfig(req, res) {
const UserInfo = req.userInfo;
let {startTime, endTime, isOpen, name, otId} = req.body;
let result = await questionBiz.setOpeningTimeConfig(startTime, endTime, isOpen, name, otId);
res.success(result);
}
// async function checkIsInOpeningTime(req, res) {
// const UserInfo = req.userInfo;
// let {}
// }
/**
* 题目
* @param req
* @param res
*/
async function questionsByDirection(req, res) {
const UserInfo = req.userInfo;
let result = await questionBiz.getQuestionsByDirection();
res.success(result);
}
/**
* 批量完成答题接口
* @param req UserInfo.studentName
* @param res
*/
async function completeAnswer(req, res) {
const UserInfo = req.userInfo;
let {studentName, answers} = req.body;
let result = await questionBiz.completeAnswerBatch(UserInfo.studentId, studentName, answers);
res.success(result);
}
/**
* 完成全部答题
* @param req
* @param res
*/
async function finishAnswer(req, res) {
const UserInfo = req.userInfo;
let {record_id} = req.body;
let result = await questionBiz.finishAnswer(record_id);
res.success(result);
}
/**
* 获取测评得分以及狮子形象
* @param req
* @param res
*/
async function answerResultWithLionImage(req, res) {
const UserInfo = req.userInfo;
let {record_id} = req.body;
let result = await questionBiz.getAnswerResultWithLionImage(record_id);
res.success(result);
}
//管理员页面 ==============================================================================================
/**
* 获取所有学生答题记录及详细得分
* @param req
* @param res
*/
async function allStudentAnswerRecords(req, res) {
const UserInfo = req.userInfo;
let {page} = req.body;
let result = await questionBiz.getAllStudentAnswerRecordsOptimized(page);
// let result = await questionBiz.getAllStudentAnswerRecords();
res.success(result);
}
/**
* 单个答题记录数据下载
* @param req
* @param res
*/
async function studentDetailed(req, res) {
const UserInfo = req.userInfo;
let {record_id} = req.body;
let result = await questionBiz.getStudentDetailed(record_id);
res.success(result);
}
/**
* 批量答题记录数据下载
* @param req
* @param res
*/
async function batchStudentDetailed(req, res) {
const UserInfo = req.userInfo;
let {record_id} = req.body;
let result = await questionBiz.getBatchStudentDetailed(record_id);
res.success(result);
}
/**
* 总路由入口
*/
import * as questionRouter from './question';
export function setRouter(httpServer){
questionRouter.setRouter(httpServer);
}
// test-xmlrpc.js
const xmlrpc = require('xmlrpc');
console.log('🧪 测试所有XML-RPC方法...');
const client = xmlrpc.createClient({
host: '127.0.0.1',
port: 13277, // HTTPS默认端口
path: '/gzn/rpc',
url: 'http://127.0.0.1:13277/gzn/rpc' // 明确指定完整URL
});
// 格式化日期为 YYYY-MM-DD HH:MM:SS 格式
function formatDate(date) {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 测试数据 - 根据表结构调整字段值
const testUser = {
user_id: 'test_user_005',
group_id: 1,
user_name: 'testuser',
user_type: 'stu', // 修改为3个字符,符合 STRING(5) 限制
active_flag: 1,
admin_flag: 0,
create_date: formatDate(new Date()), // 使用格式化后的日期
name: '测试用户',
verified_status: 0,
is_sx: 0,
order_id: 0,
is_train: 0,
zw_mark: 0,
uoid: 0
};
const testUpdateUser = {
user_id: 'test_user_003',
group_id: 1,
user_name: 'testuserupdate',
user_type: 'stu', // 修改为3个字符,符合 STRING(5) 限制
active_flag: 1,
admin_flag: 0,
create_date: formatDate(new Date()), // 使用格式化后的日期
name: '测试用户',
verified_status: 0,
is_sx: 0,
order_id: 0,
is_train: 0,
zw_mark: 0,
uoid: 0
};
const testGroup = {
group_id: 1001,
group_name: '测试组',
parent_id: 0,
thread_id: 1,
group_flag: 1
};
// 修改参数传递方式 - 将参数展开而不是作为数组
const testMethods = [
{
name: 'user.syncUser',
params: testUser,
description: '用户同步'
},
{
name: 'user.addUser',
params: testUser,
description: '添加用户'
},
{
name: 'user.updateUserCommon',
params: testUpdateUser,
description: '更新用户'
},
{
name: 'user.deleteUser',
params: "'test_user_001','test_user_002'",
description: '删除用户'
},
{
name: 'system.listMethods',
params: null,
description: '列出方法'
}
];
// 顺序测试所有方法
let currentIndex = 0;
function testNextMethod() {
if (currentIndex >= testMethods.length) {
console.log('✅ 所有方法测试完成');
return;
}
const method = testMethods[currentIndex];
console.log(`\n${currentIndex + 1}. 测试 ${method.name} (${method.description})...`);
console.log('📤 发送参数:', JSON.stringify(method.params, null, 2));
// 根据参数类型决定如何调用
if (method.params === null || method.params === undefined) {
client.methodCall(method.name, [], (err, result) => {
handleResponse(err, result, method);
});
} else if (Array.isArray(method.params)) {
client.methodCall(method.name, method.params, (err, result) => {
handleResponse(err, result, method);
});
} else {
client.methodCall(method.name, [method.params], (err, result) => {
handleResponse(err, result, method);
});
}
}
function handleResponse(err, result, method) {
if (err) {
console.error(`❌ ${method.name} 失败:`, err.message);
} else {
console.log(`✅ ${method.name} 成功:`, result);
}
currentIndex++;
setTimeout(testNextMethod, 1000);
}
// 开始测试
testNextMethod();
// 超时处理
setTimeout(() => {
console.log('\n⏰ 测试超时');
process.exit(0);
}, 30000);
\ No newline at end of file
import { ERRORENUM } from "../config/errorEnum";
import { BizError } from "../util/bizError";
/**
* 校验value是否符合传入的枚举
* @param name 被掉用名称 用于输出异常日志
* @param key 目标字段 用于输出异常日志
* @param enumConf 目标枚举
* @param value 目标值
* 无返回 有异常直接报错
*/
export function eccEnumValue(name:string, key:string, enumConf, value:any) {
let eccSuccess = true;
if ( typeof value == 'number' ) {
if (!enumConf[value] ) eccSuccess = false;
} else if (Array.isArray(value)) {
value.forEach(item => {
if ( !enumConf[item] ) eccSuccess = false;
});
}
if (!eccSuccess) throw new BizError(ERRORENUM.文件不存在, `${name} 下的 ${key} 字段值为 ${value} 不满足枚举范围`);
}
/**
* 将枚举值转换成对应的枚举名(key)
* @param enumConf 目标枚举
* @param value 目标值
* @returns string 返回字符串 如果传入多个枚举值,就拼接字符串
*/
export function changeEnumValue(enumConf, value:any) {
if (!value) return '';
if ( typeof value == 'number' ) {
let str = enumConf[value];
/**特化处理 */
if(/_dou/.test(str)) str = str.replace(/_dou/gm, ",");
if(/_zyh/.test(str)) str = str.replace(/_zyh/gm, "“");
if(/_yyh/.test(str)) str = str.replace(/_yyh/gm, "”");
if(/_dun/.test(str)) str = str.replace(/_dun/gm, "、");
if(/_ju/.test(str)) str = str.replace(/_ju/gm, "。");
return str
} else if (typeof value == 'string') {
try {//兼容数据库 '[1,2,3]'
value = JSON.parse(value);
}catch(err) {
return enumConf[parseInt(value)];
}
}
let str = "";
value.forEach((item, index) => {
let subStr = enumConf[item];
/**特化处理 */
if(/_dou/.test(subStr)) subStr = subStr.replace(/_dou/gm, ",");
if(/_zyh/.test(subStr)) subStr = subStr.replace(/_zyh/gm, "“");
if(/_yyh/.test(subStr)) subStr = subStr.replace(/_yyh/gm, "”");
if(/_dun/.test(subStr)) subStr = subStr.replace(/_dun/gm, "、");
if(/_ju/.test(subStr)) subStr = subStr.replace(/_ju/gm, "。");
str += subStr;
if (index == value.length-1) str+="";
else str += ","
});
return str;
}
\ No newline at end of file
import { ERRORENUM } from "../config/errorEnum";
import { BizError } from "../util/bizError";
import { TYPEENUM } from "../config/enum";
/**
* 根据conf配置校验请求参数
* @param conf 配置
* @param param 表单
* @param skipKeys []不必填的字段
*/
export function eccReqParamater(conf:object, param, skipKeys?) {
skipKeys = skipKeys || [];
let skipMap = {};
skipKeys.forEach(keyName => {
skipMap[keyName] = 1;
});
/**校验多余字段 */
for (let key in param) {
if (!conf[key]) throw new BizError(ERRORENUM.参数错误, `多余${key}字段`);
}
/**校验必填和缺失字段 */
for (let key in conf) {
let confType = conf[key];
let value = param[key];
let valueType = typeof value;
if ( value == null || value == undefined ) {
if (!skipMap[key]) throw new BizError(ERRORENUM.参数错误, `缺失${key}字段`);
} else {
let isError = false;
let errorStr = "";
switch(confType) {
case 'Number':
if ( confType.toLowerCase() != valueType ) isError = true;
else {
if ((""+param[key]).indexOf('.') > -1) {
param[key] = parseInt(`${param[key] *100}`)/100;
}
}
break;
case 'String':
case 'Boolean':
case 'Object':
if ( confType.toLowerCase() != valueType ) isError = true;
break;
case '[Number]':
if ( !Array.isArray(param[key]) ) isError = true;
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'number' ) {
isError = true;
errorStr = `${key}应是number型数组其中下标${i}${typeof item}`;
}
}
break;
case '[Object]':
if ( !Array.isArray(param[key]) ) isError = true;
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'object' ) {
isError = true;
errorStr = `${key}应是object型数组其中下标${i}${typeof item}`;
}
}
break;
case '[String]':
if ( !Array.isArray(param[key]) ) isError = true;
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'string' ) {
isError = true;
errorStr = `${key}应是String型数组其中下标${i}${typeof item}`;
}
}
break;
// case 'Address':
// /**地址类型 基本数据类型为数组字符串但是要判断层级关系 */
// if ( !Array.isArray(param[key]) ) {
// isError = true;
// errorStr = `${key}应是数组形`;
// }
// if ( param[key].length != 4) {
// isError = true;
// errorStr = `${key}超过特定长度4 目前长度 ${param[key].length}`;
// }
// for (let i =0; i < param[key].length; i++) {
// let item = param[key][i];
// if ( typeof item != 'string' ) {
// isError = true;
// errorStr = `${key}应是string型数组其中下标${i}是${typeof item}`;
// }
// }
// /** 不符合规则的 */
// let nullIndex = -1;
// for (let i = 0; i < param[key].length; i++) {
// if (nullIndex != -1) {//出现过空 第一次出现后的位置 都不能有值
// if (param[key]) {
// //做一个特化
// throw new BizError(ERRORENUM.地址数据不完整, `${key} 下标 ${nullIndex} 为空 `);
// }
// }
// if (nullIndex == -1 && !param[key][i]) {
// /**按顺序第一次赋值 */
// nullIndex = i;
// }
// }
// break;
}
errorStr = isError && errorStr == "" ? `${key}应该是${confType}型 而不是${valueType}`: errorStr;
if (isError) throw new BizError(ERRORENUM.参数错误, errorStr);
}
}
return param;
}
//对象判空
export function objectKeyIsNull(obj, ...keyNames) {
let isNull = false;
for (let i = 0; i < keyNames.length; i++) {
let keyStr = keyNames[i];
let moreKeyList = keyStr.split(".");
let lastObj;
for (let j = 0; j < moreKeyList.length; j++) {
lastObj = obj[moreKeyList[j]];
if (!lastObj) {
isNull = true;
break;
}
}
if (isNull) break;
}
return isNull;
}
/**
* 校验类型
* @param target 目标值
* @param type TYPEENUM枚举值
* @returns 通过 = true 不通过 = false
*/
export function checkType(target, type) {
if (target == undefined || target == null) return false;
switch (type) {
case TYPEENUM.string:
if (typeof target == 'string') {
return true;
}
break;
case TYPEENUM.number:
if (typeof target == 'number') {
return true;
}
break;
case TYPEENUM.object:
if (typeof target == 'object' && !Array.isArray(target) ) {
return true;
}
break;
case TYPEENUM.array:
if (typeof target == 'object' && Array.isArray(target) ) {
return true;
}
break;
case TYPEENUM.boolean:
if (typeof target == 'boolean') {
return true;
}
break;
};
return false;
}
export function checkStrLeng(str, length?) {
length = length ? length : 40;
let result = true;
if (str.length < length) result = false;
return result;
}
const { parseString } = require('xml2js');
import * as iconv from 'iconv-lite';
/**
* 解析用户信息XML响应
*/
export async function parseUserNameInfoXml(xmlData: string): Promise<any> {
return new Promise((resolve, reject) => {
// 先统一转换编码
const utf8XmlData = convertEncoding(xmlData, 'gbk');
parseString(utf8XmlData, {
explicitArray: false,
trim: true
}, (err, result) => {
if (err) {
console.error('XML解析错误:', err);
return resolve(null);
}
try {
// 解析XML结构并提取用户信息
const response = result.methodResponse;
if (!response || !response.params || !response.params.param) {
console.error('无效的XML响应结构');
return resolve(null);
}
// 提取用户信息数组
const params = Array.isArray(response.params.param) ?
response.params.param : [response.params.param];
if (params.length > 0 && params[0].value && params[0].value.array) {
const data = params[0].value.array.data;
if (data && data.value && data.value.length > 0) {
// 提取第一个用户信息
const userValue = data.value[0];
if (userValue.struct) {
const member = userValue.struct.member;
const userInfo: any = {};
// 处理成员字段
(Array.isArray(member) ? member : [member]).forEach((m: any) => {
if (m.name && m.value) {
const key = m.name;
const value = m.value.string || m.value.int || m.value.double || '';
userInfo[key] = value; // 不再需要单独转换编码
}
});
console.log('成功提取用户信息:', userInfo);
return resolve(userInfo);
}
}
}
console.error('未找到用户信息在响应中');
resolve(null);
} catch (parseError) {
console.error('响应解析错误:', parseError);
resolve(null);
}
});
});
}
/**
* 解析用户权限信息XML响应
*/
export async function parseUserInfoXml(xmlData: string): Promise<any> {
return new Promise((resolve, reject) => {
// 先统一转换编码
const utf8XmlData = convertEncoding(xmlData, 'gbk');
parseString(utf8XmlData, {
explicitArray: false,
trim: true,
// 添加值处理器来处理不同类型的数据
valueProcessors: [
(value: string, name: string) => {
// 处理整型数据
if (name === 'int') {
return parseInt(value, 10);
}
// 处理字符串数据(不再需要编码转换)
if (name === 'string') {
return value;
}
return value;
}
]
}, (err, result) => {
if (err) {
console.error('XML解析错误:', err);
return resolve(null);
}
try {
// 解析XML结构
const response = result.methodResponse;
if (!response || !response.params || !response.params.param) {
console.error('无效的XML响应结构');
return resolve(null);
}
// 提取参数值
const param = response.params.param;
const value = param.value;
if (value && value.array && value.array.data) {
const data = value.array.data;
// 检查是否有值
if (data.value && data.value.struct) {
const struct = data.value.struct;
// 提取成员信息
if (struct.member) {
const members = Array.isArray(struct.member) ?
struct.member : [struct.member];
// 构建用户信息对象
const userInfo: any = {};
members.forEach((member: any) => {
if (member.name && member.value) {
const key = member.name;
let value:any = '';
// 根据值类型提取数据
if (member.value.string) {
value = member.value.string; // 不再需要转换编码
} else if (member.value.int) {
value = parseInt(member.value.int, 10);
} else if (member.value.double) {
value = parseFloat(member.value.double);
} else if (member.value.boolean) {
value = member.value.boolean === '1' ||
member.value.boolean === 'true';
} else {
// 尝试直接获取值
value = member.value;
}
userInfo[key] = value;
}
});
console.log('成功提取用户信息:', userInfo);
return resolve(userInfo);
}
}
}
console.error('未找到用户信息在响应中');
resolve(null);
} catch (parseError) {
console.error('响应解析错误:', parseError);
resolve(null);
}
});
});
}
/**
* 更简化的解析方法(针对您提供的XML结构)
*/
export async function parseUserInfoXmlSimple(xmlData: string): Promise<any> {
return new Promise((resolve, reject) => {
// 先统一转换编码
const utf8XmlData = convertEncoding(xmlData, 'gbk');
parseString(utf8XmlData, {
explicitArray: false,
trim: true
}, (err, result) => {
if (err) {
console.error('XML解析错误:', err);
return resolve(null);
}
try {
// 简化解析逻辑
const userInfo: any = {};
// 提取所有成员
const members = result?.methodResponse?.params?.param?.value?.array?.data?.value?.struct?.member;
if (!members) {
console.error('未找到用户信息在响应中');
return resolve(null);
}
// 处理单个或多个成员
const memberList = Array.isArray(members) ? members : [members];
memberList.forEach((member: any) => {
if (member.name && member.value) {
const key = member.name;
let value: any;
// 根据值类型处理
if (member.value.string !== undefined) {
value = member.value.string; // 不再需要转换编码
} else if (member.value.int !== undefined) {
value = parseInt(member.value.int, 10);
} else if (member.value.double !== undefined) {
value = parseFloat(member.value.double);
} else if (member.value.boolean !== undefined) {
value = member.value.boolean === '1' ||
member.value.boolean === 'true';
} else {
value = member.value;
}
userInfo[key] = value;
}
});
console.log('成功提取用户信息:', userInfo);
resolve(userInfo);
} catch (parseError) {
console.error('响应解析错误:', parseError);
resolve(null);
}
});
});
}
/**
* 统一的编码转换函数 (GBK -> UTF-8)
*/
// export function convertEncoding(text: any, from: string = 'gbk'): string {
// if (!text) return '';
// try {
// if (Buffer.isBuffer(text)) {
// return iconv.decode(text, from);
// }
// if (typeof text === 'string') {
// // 如果是字符串,先转换为Buffer再解码
// const buffer = Buffer.from(text, 'binary');
// return iconv.decode(buffer, from);
// }
// return String(text);
// } catch (error) {
// console.warn('编码转换失败:', error);
// return typeof text === 'string' ? text : String(text);
// }
// }
/**
* 增强的编码转换函数
*/
export function convertEncoding(data: any, from: string = 'gbk'): string {
if (!data) return '';
try {
if (Buffer.isBuffer(data)) {
// 直接解码Buffer
return iconv.decode(data, from);
}
if (typeof data === 'string') {
// 检查是否已经是正确编码
if (!hasGarbledCharacters(data)) {
return data;
}
// 尝试修复可能的双重编码
const fixed = fixDoubleEncoding(data);
if (!hasGarbledCharacters(fixed)) {
return fixed;
}
// 如果修复失败,尝试将字符串转换为Buffer再解码
const buffer = Buffer.from(data, 'binary');
return iconv.decode(buffer, from);
}
return String(data);
} catch (error) {
console.warn('增强编码转换失败:', error);
return typeof data === 'string' ? data : String(data);
}
}
/**
* 检测字符串是否包含乱码字符
*/
export function hasGarbledCharacters(str: string): boolean {
if (!str || typeof str !== 'string') return false;
// 检查常见的乱码模式
const garbledPatterns = [
/\\u[0-9a-fA-F]{4}/g, // Unicode转义序列
/�/g, // Unicode替换字符
/[^\x00-\x7F\u4E00-\u9FFF]/g // 非ASCII和非中文字符
];
return garbledPatterns.some(pattern => pattern.test(str));
}
/**
* 修复可能的双重编码问题
*/
export function fixDoubleEncoding(str: string): string {
if (!str || typeof str !== 'string') return str || '';
try {
// 尝试将字符串视为UTF-8编码的GBK数据
// 1. 先将字符串转换为Buffer(假设它是UTF-8)
const utf8Buffer = Buffer.from(str, 'utf8');
// 2. 然后将这个Buffer作为GBK解码
return iconv.decode(utf8Buffer, 'gbk');
} catch (error) {
console.warn('双重编码修复失败:', error);
return str;
}
}
/**
* 解析用户身份代码
* 1:是
* 0:否
* @param userTypeString
* @returns
*/
export function parseUserTypeEnhanced(userTypeString) {
// 配置项:身份类型定义
const ROLE_CONFIG = [
{ key: 'generalUser', label: '一般注册用户' },
{ key: 'administrator', label: '行政管理人员' },
{ key: 'staff', label: '教职员工' },
{ key: 'student', label: '学生' },
{ key: 'parent', label: '家长' }
];
// 验证输入
if (typeof userTypeString !== 'string') {
throw new Error('输入必须是一个字符串');
}
if (userTypeString.length !== 5) {
throw new Error('字符串长度必须为5位');
}
if (!/^[01]+$/.test(userTypeString)) {
throw new Error('字符串只能包含0和1');
}
// 构建结果对象
const result = {};
ROLE_CONFIG.forEach((role, index) => {
result[role.label] = parseInt(userTypeString[index], 10);
});
return result;
}
import moment = require("moment");
import { ERRORENUM } from "../config/errorEnum";
import { BizError } from "../util/bizError";
import { FILETYPE } from "../config/enum";
const md5 = require("md5");
export function randomId(tableName:string) {
let randomStr = `${new Date().valueOf()}_${Math.ceil(Math.random()*100000)}`;
return `${tableName}_${md5(randomStr)}`;
}
export function getUserToken(loginId:string) {
return md5(`${loginId}_${Math.ceil(Math.random()*1000)}${new Date().valueOf()}`);
}
export function getMySqlMs(time?) {
time = time || new Date().valueOf();
// time += (8*3600*1000);
return moment(time).format("YYYY-MM-DD HH:mm:ss");
}
export function getClientMs(time) {
if (!time) return new Date().valueOf();
return new Date(time).valueOf();
}
export function getPartyMemberId(param) {
return md5(`${param}-${new Date().valueOf()}-${Math.ceil(Math.random() * 10000)}`);
}
export function getDefPwd(phone:string) {
return md5(`${phone.slice(5, 11)}`);
}
export function getFileType(fileName) {
let fileType = 0;
fileName.forEach(info => {
let repList = info.split(".");
let type = repList[repList.length-1];
if (!type) throw new BizError(ERRORENUM.文件不存在, `文件名 ${info}`);
let typeNum = 0;
switch(type) {
case 'pdf': typeNum = FILETYPE.pdf; break;
case 'doc':
case 'docx': typeNum = FILETYPE.word; break;
case 'jpg':
case 'png': typeNum = FILETYPE.图片; break;
};
if (typeNum) {
if (!fileType) fileType = typeNum;
else if (fileType != typeNum) fileType = FILETYPE.多类型;
}
});
return fileType;
}
const xlsx = require('node-xlsx');
const path = require('path');
/**
* onceSheetBecomeOfblockData 将excel文件的指定sheet解析成数据块数据
* @param fileName 文件名称
* @param sheetName 表名称
* @returns [ {blockData:数据块(二维数组), blockTitle:"数据标题"}]
*/
export function onceSheetBecomeOfblockData(fileName, sheetName) {
let {sheetMap} = getExcel( path.join(__dirname.substring(0,__dirname.indexOf("out")), "res", fileName ));
// return sheetMap;
let thisBlockData = getBlockData(sheetMap[sheetName]);
return thisBlockData;
}
/**
* excelBecomeOfBlockData 将excel所有的sheet解析成数据块
* @param fileName 文件名称
* @returns {"sheetName1":[ {blockData:数据块(二维数组), blockTitle:"数据标题"}], ...}
*/
export function excelBecomeOfBlockData(fileName) {
let {sheetMap} = getExcel( path.join(__dirname.substring(0,__dirname.indexOf("out")), "res", fileName ));
let result = {};
for (let sheetName in sheetMap) {
result[sheetName] = getBlockData(sheetMap[sheetName]);
}
return result;
}
/**
* planaryArrayBecomeOfBlockData 将符合excel规则的sheet二维数组转为 数据块
* @param dataList excel解出来的数据
* @returns thisBlockData 返回数据块集合 格式:blockList = [ {blockData:数据块(二维数组), blockTitle:"数据标题"}]
*/
export function planaryArrayBecomeOfBlockData(planaryArray) {
return getBlockData(planaryArray);;
}
//===
/**
* getBlockData 数据分块
* @param dataList 解析出来的excel二维数组
* @returns 返回数据块集合 格式:blockList = [ {blockData:数据块(二维数组), blockTitle:"数据标题"}]
*/
function getBlockData(dataList) {
let blockList = [];
for (let i = 0; i < 999; i++) {
let {blockData, blockTitle, notItem, delDataList} = checkBlock(dataList);
if (notItem) break;
dataList = delDataList;
if (blockTitle) blockList.push({blockData, blockTitle});
}
return blockList;
}
function getListFristNotNullItemIndex(list) { //获取起始坐标
if (!list.length) return null;
for (let i = 0; i < list.length; i++) {
if (list[i]) return i;
}
}
function getListFirstNullItemIndex(startX, list) { //获取第一个为空的坐标
if (!list.length) return null;
let checkItem = false;
let firstItemIndex = 0;
for (let i = startX; i <= list.length; i++) {
let item = list[i];
if (!checkItem && item) checkItem = true;
if (checkItem && !item) {
firstItemIndex = i;
break;
}
}
return firstItemIndex;
}
function listRegionIsNull(list, startX, endX) { //指定区间内数据是否未空
let isNull = true;
if ( !list.length ) return isNull;
for (let i = startX; i < endX; i++) {
let item = list[i];
if (item) {
isNull = false;
break;
}
}
return isNull;
}
function thisListNotItem(list) {
for (let i = 0; i < list.length; i++) {
if (list[i]) return false;
}
return true
}
function checkBlock(dataList) {
//纵向有效起始点
let startY = 0;
let startX = 0;
let isNotBlockTitle = false; //没有块标题
let isLook = false;
let endX = 0;//x轴最长结束下标 【包括下标】
let blockTitle = ''; //标题块名称
let notItem = true;
for (let i = 0; i < dataList.length; i++) {
let childList = dataList[i] || [];
if (!thisListNotItem(childList)) {
if ( !isLook ) {
let thisRoowStartX = getListFristNotNullItemIndex(childList);
let thisRoowLastItem = childList[thisRoowStartX + 1];
let LastList = dataList[i+1] || [];
// let lastRoowStartX = getListFristNotNullItemIndex(LastList);
let lastRoowHaveItem = LastList[thisRoowStartX];
if ( thisRoowLastItem || (LastList.length && lastRoowHaveItem) ) {
if (lastRoowHaveItem && thisRoowLastItem ) {
isNotBlockTitle = true; //不存在标题块
blockTitle = `${thisRoowStartX}_${i}`;
startY = i;
startX = thisRoowStartX;
}
else {
blockTitle = dataList[i][thisRoowStartX];
dataList[i][thisRoowStartX] = null;
if ( thisRoowLastItem ) { // 同行存在元素 标题在y轴上
startY = i;
startX = thisRoowStartX + 1;
} else { // 同行存在元素 标题在x轴上
startY = i + 1;
startX = thisRoowStartX;
}
}
isLook = true;
} else { //只有标题 无内容
console.log(dataList[i][thisRoowStartX]);
dataList[i][thisRoowStartX] = null;
}
} else {
//测量最大连续长度
let firstNullX = getListFirstNullItemIndex(startX, childList);
if (firstNullX) endX = Math.max(endX, firstNullX-1);
break;
}
notItem = false;
}
}
let endY = 0;//y轴连续下标 【包括下标】
let yInfoStart = false;
let yInfoEnd = false;
for (let y = startY; y < dataList.length; y++) {
//纵向找连续性
let thisRoow = dataList[y];
let regionIsNull = listRegionIsNull(thisRoow, startX, endX);
if (!regionIsNull) {
endY = y;
if (!yInfoStart) yInfoStart = true;
}
if (yInfoStart && regionIsNull) yInfoEnd = true;
if (yInfoEnd) break;
}
let blockData = [];
for (let y = startY; y <= endY; y++) {
let onceList = [];
for (let x = startX; x <= endX; x++) {
onceList.push(dataList[y][x]);
dataList[y][x] = null;
}
blockData.push(onceList);
}
return {blockData, blockTitle, delDataList:dataList,notItem};
}
//获取单个excel文件的数据
function getExcel(filePath) {
const workSheetsFromFile = xlsx.parse(filePath);
let sheetMap = {};
let sheetList = [];
for (let i = 0; i < workSheetsFromFile.length; i++) {
let sheetInfo = workSheetsFromFile[i];
sheetMap[sheetInfo.name] = sheetInfo.data;
sheetList.push(sheetInfo);
}
return {sheetMap, sheetList}
}
\ No newline at end of file
/**
* 异常类
* 需要和log4js共同使用
*/
import { getBizMsg } from "../config/errorEnum";
import { logError } from "./log";
export class BizError extends Error {
statusCode(statusCode: any) {
throw new Error('Method not implemented.');
}
constructor(...msgs) {
let reqErrorMsg = '';
let logErrorMsg = '';
for (let i = 0; i <msgs.length; i++) {
if (!i) {
let msg = getBizMsg(msgs[i]);
reqErrorMsg = msg;
logErrorMsg = msg;
} else {
logErrorMsg += ` | ${msgs[i]} `;
}
}
logError(logErrorMsg);
super(reqErrorMsg);
}
}
/**
* 日志类
* 包括错误日志 普通日志
* 日志存放在根目录的logs内
*/
let log4js = require('log4js');
let path = require('path');
//log路径
export const systemLogPath = {
errorLogFile:"error",
errorLogDir:"error",
handleLogFile:"handle",
handleLogDir:"handle"
}
//日志根目录
// let baseLogPath = path.resolve(__dirname.substring(0, __dirname.indexOf("out")), 'logs');
let baseLogPath = path.resolve('./', 'logs');
let errFile = path.resolve(baseLogPath, systemLogPath.errorLogDir, systemLogPath.errorLogFile);
let handFile =path.resolve(baseLogPath, systemLogPath.handleLogDir, systemLogPath.handleLogFile);
let config = {
appenders:
{
"rule-console": {"type": "console"},
"errorLogger": {
"type": "dateFile", // 日志类型
"filename": errFile, // 输出文件名
"pattern": "yyyy-MM-dd.log", // 后缀
"alwaysIncludePattern": true, // 上面两个参数是否合并
"encoding": "utf-8", // 编码格式
"maxLogSize": 1000, // 最大存储内容
"numBackups": 3, // 当文件内容超过文件存储空间时,备份文件的数量
"path": `/${systemLogPath.errorLogDir}`
},
"handleLogger": {
"type": "dateFile",
"filename": handFile,
"pattern": "yyyy-MM-dd.log",
"alwaysIncludePattern": true,
"encoding": "utf-8",
"maxLogSize": 1000,
"numBackups": 3,
"path": `/${systemLogPath.handleLogDir}`
}
},
categories: {
"default": {"appenders": ["rule-console"], "level": "all"}, //这个配置一定要有
"errorLogger": {"appenders": ["errorLogger"], "level": "error"},
"handleLogger": {"appenders": ["handleLogger"], "level": "all"}
},
"baseLogPath": path.resolve(baseLogPath, systemLogPath.handleLogDir, systemLogPath.handleLogFile)
};
log4js.configure(config); //加载配置文件
//调用预先定义的日志名称
let errorLogger = log4js.getLogger("errorLogger");
let handleLogger = log4js.getLogger("handleLogger");
let consoleLogger = log4js.getLogger("rule-console");
//错误日志
export function logError(...errStrs) {
let str = "";
errStrs.forEach(item => {
str += item + " | ";
});
errorLogger.error(`errorInfo => ${str}`);
}
//普通日志
export function logHandle(msgStr:string) {
handleLogger.info(`logInfo => ${msgStr}`);
}
//输出日志
export function logConsole(logStr:string) {
consoleLogger.info(`logInfo => ${logStr}`);
}
/**
* 解析xml
*/
var xml2js = require("xml2js");
/**
*
* @param str 需要解析的xml文本
* @returns 解析好的对象
*/
export function analysisXml(str) {
return new Promise( (resolve, reject) => {
xml2js.parseString(str, (err, result) => {
if (err) return reject(err);
return resolve(result);
});
});
}
\ No newline at end of file
/**
* 零碎的通用工具
*/
import moment = require("moment");
/**
* 匹配新旧对象变化
* 将newObj 与 oldObj 比对,将newObj中发生变化的key返回
* 使用前需要校验对象中的内容
* @param newObj 新对象
* @param oldObj 旧对象
* @returns [key] 发生变化的key
*/
export function checkChange(newObj, oldObj) {
let changeKeyList = [];
for (let newKey in newObj) {
if (`${newObj[newKey]}` != `${oldObj[newKey]}`) changeKeyList.push(newKey);
}
return changeKeyList;
}
/**
* 根据conf截取data中的数据
* @param conf
* @param data
* @returns
*/
export function extractData(conf, data, isAdmin) {
let result = {};
for (let key in conf) {
let confInfo = conf[key];
if (confInfo.changeDate) {
if (isAdmin) result[key] = data[key] ? moment(data[key]).format("YYYY-MM-DD") : '-';
else result[key] = data[key] || 0;
} else if (confInfo.isAdd && isAdmin) {
let addStr = "";
data[key].forEach(str => {
addStr += str;
});
result[key] = addStr;
}
else {
result[key] = data[key];
if (typeof result[key] == 'string' && !result[key]) result[key] = '';
}
}
return result;
}
/**
* 校验数据对象是否有空
* @param data
* @param sensitive 敏感校验 true时 0 和 ""会校验失败 false时 校验成功
* @returns true/false true = 有空值 false=无空值
*/
export function checkDataHaveNull(data:object, sensitive:boolean) {
if (Array.isArray(data)) return data.length == 0;
if (Object.keys(data).length == 0) return true;
let success = false;
for (let key in data) {
if (data[key] == null || data[key] == undefined) success = true;
if (sensitive) {
if (data[key] === 0 || data[key] === "" ) success = true;
}
}
return success;
}
\ No newline at end of file
/**
* 请求工具
*/
import * as request from 'request';
import { BizError } from './bizError';
/**
* 请求接口(get)
* @param url 路由
* @param query 请求参数
* @param headers 请求头
* @returns
*/
export function get(url:string, query?, headers?) {
if (!url || (url.search(/http:/) && url.search(/https:/)) ) throw new BizError(!url ? "请求地址为空" : "请求地址错误");
return new Promise((resolve, reject)=>{
let paramater:any = { url, json:true };
if (query) paramater.qs = query;
if (headers) paramater.headers = headers;
request.get(paramater, function (err, r, body) {
if (err) return reject(err);
if (r && r.statusCode != 200) return reject(new Error('httpError:'+r.statusCode));
resolve(body);
});
})
}
export function post(url, body, headers) {
if (!url || (url.search(/http:/) && url.search(/https:/)) ) throw new BizError(!url ? "请求地址为空" : "请求地址错误");
let header = {"content-type": "application/json"};
return new Promise((resolve, reject)=>{
request({
url: url,
method: "POST",
json: true,
headers: Object.assign(header, headers),
body: body
}, function(error, response, body) {
if (!error && response.statusCode == 200) {
resolve(body);
}
else {
// reject(error)
}
});
})
}
export function postForm(url, body, headers) {
if (!url || (url.search(/http:/) && url.search(/https:/)) ) throw new BizError(!url ? "请求地址为空" : "请求地址错误");
return new Promise((resolve, reject)=>{
request({
url: url,
method: "POST",
json: true,
form:body
}, function(error, response, res) {
if (!error) {
resolve(res);
}
else {
reject(error)
}
});
})
}
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2017",
"sourceMap": true,
"rootDir":"./src",
"outDir":"./out",
"esModuleInterop": true,
// "strict": true,
},
"exclude": [
"node_modules",
]
}
// {
// "compilerOptions": {
// "target": "ES6",
// "module": "commonjs",
// "esModuleInterop": true,
// "outDir": "./dist",
// "strict": true
// },
// "include": ["src/**/*"]
// }
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