Commit b7d4d9b1 by lixinming

no message

parents
.idea
/out
/node_modules
/test
/public
/logs
/video
*.logs
*.zip
{
// 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
{
"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",
"express": "^4.17.1",
"express-async-handler": "^1.1.4",
"express-history-api-fallback": "^2.2.1",
"formidable": "^1.2.1",
"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",
"svg-captcha": "^1.3.12",
"tencentcloud-sdk-nodejs": "^4.0.562",
"ws": "^5.2.2",
"xml2js": "^0.4.23"
},
"devDependencies": {},
"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>40002</port>
<sign>xxx90909082fsdahfjosadjfpoiwausjorip2hjklrhn1ioud0u124rx0qwejfokasjfolksaujfoas</sign>
<mongodb>
<path>127.0.0.1</path>
<port>27017</port>
<w>1</w>
<dataBase>baseDB</dataBase>
<wtimeoutMS>30000</wtimeoutMS>
</mongodb>
<mysqldb>
<!-- 本地mysql配置 -->
<mysqlHost>192.168.0.105</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>123456</mysqlPwd>
<dataBase>yfs</dataBase>.
<!-- 服务器mysql配置 -->
<!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>Yfs123456_</mysqlPwd>
<dataBase>yfs</dataBase> -->
</mysqldb>
</config>
import { mysqlModelMap } from "../model/sqlModelBind";
/**
* 添加数据
* @param tableModel
* @param data
* @returns
*/
export async function addData(tableModel, data:any) {
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 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;
case "%group%":
group = param[key];
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;
return selectParam;
}
/**
* 查询单个数据
* @param tableModel 表对象
* @param param
* @returns
*/
export async function selectOneDataByParam(tableModel, param, column) {
let selectParam = analysisParamToWhere(param, column);
let data = await tableModel.findOne(selectParam);
data = data || {};
return {data};
}
/**
* 查询多个数据
* @param tableName 表对象
* @param param
* @returns
*/
export async function selectDataListByParam(tableModel, param, column) {
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(tableModel, param, column, pageNumber:number, pageSize:number) {
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(tableModel, param) {
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};
}
/**
* 多表联查 列表
* @param tableModel
* @param includeConf {"表名":["",""] }
* @param param
* @param column
* @returns
*/
export async function selectDataToTableAssociation(tableModel, includeConf, param, column) {
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(tableModel, includeConf, param, column, pageNumber:number, pageSize:number) {
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(tableModel, includeConf, param, column) {
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};
}
import { Op, Sequelize } from "sequelize";
/**
* 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(tableModel, param:object, data:object) {
let where = analysisParamToWhere(param);
await tableModel.update(data, where);
return {isSuccess:true};
}
\ No newline at end of file
export enum OPERATIONTYPEENUM {
= 1,
,
,
}
\ No newline at end of file
export enum ERRORENUM {
不存在表 = 1,
身份验证失败,
缺少必要参数_表名,
数据表不存在,
参数错误,
添加时数据对象为空,
修改时数据对象为空
}
/**
* 只用做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 = {
"党员基础信息表":{
tableName:'partyMember',
schema:{
id: {type:Number, index:{unique:true}}, //自增的id
entryType: Number, //名册录入类型
name: String, //姓名
sex: Number, //性别
birthDate: {type:Number, index:true}, //出生年月
phone: String, //手机号码
levelOfEducation: Number, //文化程度
branchName: {type:[Number], default:[]}, //所属支部
partyJoiningTime: {type:Number, index:true}, //入党时间 --正式党员录入
administrativePosition: {type:Number, default:1}, //行政职务 默认普通职员
partyPositions: {type:[Number], default:[]}, //党内职务 多选 --正式党员录入
department: {type:[Number], default:[]}, //所属科室
askForTime: {type:Number, index:true}, //申请入党时间 --入党积极分子录入、入党申请录入
listedAsActivistsTime: {type:Number, index:true}, //列为积极分子时间 --入党积极分子录入
liaison: {type:[String], default:[]}, //联系人 --入党积极分子录入
talkTime: {type:Number, index:true}, //谈话时间 --入党申请录入
partyState: Number, //党员状态
learning: {type:[String], default:[]}, //学习课程
/**-----用户相关----- */
loginId: {type: String, index: true}, //登录账号 默认手机号
pwd: String, //密码 默认手机号后6位
token:{type:String, index:true},
tokenMs:Number,
firstLoginIsChangePwd:{type:Boolean, default:false}, //首次登录是否修改密码
}
},
"支部制度表":{
tableName:'branchSystem',
schema:{
bId: {type:Number, index:{unique:true}}, //自增的支部制度id
systemTitle: String, //制度标题
periodOfValidity: Number, //制度有效范围
branchName: {type:[Number], default:[]}, //所属支部
releaseTime: {type:Number, index:true}, //发布时间
isUse: Boolean, //是否使用
fileName: {type:String, default:[]}, //文件名称
fileType: String, //文件类型
uploadTime: {type:Number, index:true}, //上传时间
}
},
"组织生活表":{
tableName:'organizationalLife',
schema:{
oId: {type:Number, index:{unique:true}}, //自增的组织生活id
theme: String,//主题
themeType: Number, //主题类型
dataMonth: {type:Number, index:true}, //数据月份
branchName: {type:[Number], default:[]}, //所属支部
fileName: {type:String, default:[]}, //文件名称
fileType: String, //文件类型
uploadTime: {type:Number, index:true}, //上传时间
}
},
"专题活动表":{
tableName:'thematicActivities',
schema:{
tId: {type:Number, index:{unique:true}}, //自增的专题活动id
theme: String,//主题
dataMonth: {type:Number, index:true}, //数据月份
branchName: {type:[Number], default:[]}, //所属支部
fileName: {type:String, default:[]}, //文件名称
fileType: String, //文件类型
uploadTime: {type:Number, index:true}, //上传时间
}
},
"党建动态表":{
tableName:'partyBuildingDynamic',
schema:{
pbId: {type:Number, index:{unique:true}}, //自增的党建动态id
theme: String,//主题
dataMonth: {type:Number, index:true}, //数据月份
branchName: {type:[Number], default:[]}, //所属支部
fileName: {type:String, default:[]}, //文件名称
fileType: String, //文件类型
uploadTime: {type:Number, index:true}, //上传时间
}
},
"党费管理表":{
tableName:'partyExpenses',
schema:{
peId: {type:Number, index:{unique:true}}, //自增的党费id
partyMember: String, //党员姓名
payType: Number, //缴纳状态
payableMonth: {type:Number, index:true}, //应交月份
branchName: {type:[Number], default:[]}, //所属支部
payTime: {type:Number, index:true}, //缴费时间
payAmount: Number, //缴费金额(元)
}
},
"党建先锋表":{
tableName:'partyVanguard',
schema:{
pvId: {type:Number, index:{unique:true}}, //自增的党建先锋id
honorName: String, //荣誉名称
honorType: Number, //荣誉类型
grantingRange:String, //授予范围
grantingUnit:String, //授予单位/个人
grantTime: {type:Number, index:true},//授予时间
branchName: {type:[Number], default:[]}, //所属支部
isUse: Boolean, //是否使用
primaryIndicators: Number, //一级指标
secondaryIndicators: String,//二级指标
IndicatorContent: String, //指标内容
evaluatingIndicator: Number, //指标评分
currentScore: {type:Number, default:0},//当前得分
completionProgress: String //完成进度
}
},
"党员学习表":{
tableName:'memberLearning',
schema:{
mlId: {type:Number, index:{unique:true}}, //自增的党员学习id
contentTitle: String, //内容标题
courseType: Number, //课程类型
courseEntryType: Number, //课程录入类型
hits: Number, //点击量
learningType: Number, //学习类型
isUse: Boolean, //是否使用
fileName: {type:String, default:[]}, //文件名称
fileType: String, //文件类型
uploadTime: {type:Number, index:true}, //上传时间
/**学习强国字段 */
name: String, //姓名
branchName: {type:[Number], default:[]}, //所属支部
monthIntegral: Number, //本月积分
totalIntegral: Number,//总积分
}
},
}
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
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中无配置');
else {
let {port, mongodb, sign, mysqldb } = configInfo.config;
systemConfig.port = parseInt(port[0]);
systemConfig.sign = sign[0];
const MongoPath = mongodb[0].path[0];
const MongoPort = mongodb[0].port[0];
const MongoW = mongodb[0].w[0];
const MongoDataBase = mongodb[0].dataBase[0];
const MongoWtimeoutMS = mongodb[0].wtimeoutMS[0];
systemConfig.mongodbStr = analysisMongoConnectStr( MongoPath, MongoPort, MongoDataBase, MongoW, MongoWtimeoutMS);
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];
}
}
}
console.log("config init success");
} 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}`
}
/**
* 系统配置类
*
*/
export class ServerConfig {
/**系统配置 */
port:number;
mongodbStr:string;
sign:string;
mysqldb:{
host:string,
port:number,
user:string,
pwd:string,
dataBase:string,
}
}
\ 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";
async function lanuch() {
/**初始化配置解析 */
await initConfig();
/**初始化数据库 */
// await mongoDB.initDB();
// await initModel();
/**初始化sql */
await mysqlDB.initMysqlDB();
await initMysqlModel();
/**创建http服务 */
httpServer.createServer(systemConfig.port);
// await smsTask();
console.log('This indicates that the server is started successfully.');
}
lanuch();
\ No newline at end of file
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";
/**
* 中间件 校验连接对象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();
}
\ No newline at end of file
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();
}
/**初始化表关联 */
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});
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';
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');
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(compression());
httpServer.use(watch);
httpServer.use(bodyParser.json({limit:"5000kb"}));
routers.setRouter(httpServer);
httpServer.use(httpErrorHandler);
httpServer.listen(port);
console.log('server listen on port:'+port);
}
}
\ No newline at end of file
import * as asyncHandler from 'express-async-handler'
import * as addBiz from '../biz/addData';
import * as delBiz from '../biz/delData';
import * as updateBiz from '../biz/updateData';
import * as findBiz from '../biz/findData';
import { checkMySqlSign } from '../middleware/user';
import { eccReqParamater } from '../tools/eccParam';
import { BizError } from '../util/bizError';
import { ERRORENUM } from '../config/errorEnum';
export function setRouter(httpServer){
httpServer.post('/yfs/dataserver/mysql/table/add', checkMySqlSign, asyncHandler(addTableData));
httpServer.post('/yfs/dataserver/mysql/table/delete', checkMySqlSign, asyncHandler(deleteData));
httpServer.post('/yfs/dataserver/mysql/table/update', checkMySqlSign, asyncHandler(updateManyData));
httpServer.post('/yfs/dataserver/mysql/table/find/one', checkMySqlSign, asyncHandler(findOneData));
httpServer.post('/yfs/dataserver/mysql/table/find/many', checkMySqlSign, asyncHandler(findManyData));
httpServer.post('/yfs/dataserver/mysql/table/find/manytopage', checkMySqlSign, asyncHandler(findManyToPageData));
httpServer.post('/yfs/dataserver/mysql/table/find/count', checkMySqlSign, asyncHandler(findCountData));
httpServer.post('/yfs/dataserver/mysql/table/find/aggragate', checkMySqlSign, asyncHandler(findAggragateData));
httpServer.post('/yfs/dataserver/mysql/table/find/aggragatetopage', checkMySqlSign, asyncHandler(findAggragateDataToPage));
httpServer.post('/yfs/dataserver/mysql/table/find/aggragateone', checkMySqlSign, asyncHandler(selectOneDataToTableAssociation));
}
async function addTableData(req, res) {
let table = req.tableModel;
let reqConf = {data: 'Object' };
let { data } = eccReqParamater(reqConf, req.body);
if (!Object.keys(data).length) throw new BizError(ERRORENUM.添加时数据对象为空);
let result = await addBiz.addData(table, data);
res.success(result);
}
async function deleteData(req, res) {
let table = req.tableModel;
let reqConf = {param: 'Object' };
let { param } = eccReqParamater(reqConf, req.body);
let result = await delBiz.delData(table, param);
res.success(result);
}
async function updateManyData(req, res) {
let table = req.tableModel;
let reqConf = {param:'Object', data: 'Object' };
let { param, data } = eccReqParamater(reqConf, req.body);
if (!Object.keys(data).length) throw new BizError(ERRORENUM.修改时数据对象为空);
let result = await updateBiz.updateManyData(table, param, data);
res.success(result);
}
async function findOneData(req, res) {
let table = req.tableModel;
let reqConf = {param:'Object', column:"[String]"};
let { param, column } = eccReqParamater(reqConf, req.body, ["column"]);
let result = await findBiz.selectOneDataByParam(table, param, column);
res.success(result);
}
async function findManyData(req, res) {
let table = req.tableModel;
let reqConf = {param:'Object', column:"[String]"};
let { param, column } = eccReqParamater(reqConf, req.body, ["column"]);
let result = await findBiz.selectDataListByParam(table, param, column);
res.success(result);
}
async function findManyToPageData(req, res) {
let table = req.tableModel;
let reqConf = {param:'Object', pageNumber:'Number', pageSize:'Number', column:"[String]"};
let { param, pageNumber, pageSize, column } = eccReqParamater(reqConf, req.body, ["column"] );
let result = await findBiz.selectDataListToPageByParam(table, param, column, pageNumber, pageSize);
res.success(result);
}
async function findCountData(req, res) {
let table = req.tableModel;
let reqConf = {param:'Object'};
let { param } = eccReqParamater(reqConf, req.body);
let result = await findBiz.selectDataCountByParam(table, param);
res.success(result);
}
async function findAggragateData(req, res) {
let table = req.tableModel;
let reqConf = {param:'Object', includeConf:'Object', column:"[String]" };
let { param, includeConf, column } = eccReqParamater(reqConf, req.body, ["column"]);
let result = await findBiz.selectDataToTableAssociation(table, includeConf, param, column);
res.success(result);
}
async function findAggragateDataToPage(req, res) {
let table = req.tableModel;
let reqConf = {param:'Object', includeConf:'Object', column:"[String]", pageNumber:'Number', pageSize:'Number', };
let { param, includeConf, column, pageNumber, pageSize } = eccReqParamater(reqConf, req.body, ["column"]);
let result = await findBiz.selectDataToTableAssociationToPage(table, includeConf, param, column, pageNumber, pageSize);
res.success(result);
}
async function selectOneDataToTableAssociation(req, res) {
let table = req.tableModel;
let reqConf = {param:'Object', includeConf:'Object', column:"[String]" };
let { param, includeConf, column } = eccReqParamater(reqConf, req.body, ["column"]);
let result = await findBiz.selectOneDataToTableAssociation(table, includeConf, param, column);
res.success(result);
}
/**
* 总路由入口
*/
import * as mysqlRouter from './mysqlRouter';
export function setRouter(httpServer){
mysqlRouter.setRouter(httpServer);
}
import { ERRORENUM } from "../config/errorEnum";
import { BizError } from "../util/bizError";
/**
* 根据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;
}
\ No newline at end of file
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 {
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"
},
"exclude": [
"node_modules"
]
}
\ No newline at end of file
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