0.0.6 • Published 15 days ago
@eside/leopold v0.0.6
项目介绍
本项目基于 koa 进行二次开发.
通过组合常用的koa插件,实现基于此项目可以快速的开发其他网络服务程序。 对于网络请求:设计中已匹配静态页面为优先,其次匹配动态网页渲染,最后是api服务,通过制定配置文件,根据发展需要,不断提供更多可供设计人员使用的工具与服务。
当前集成插件
- Cors跨域请求处理(简易版)
- 静态文件(基于koa-mount,koa-static,并提供有限的配置自定义)
- Log4j日志系统(简易版)
- 请求体处理(基于koa-body,并提供有限的配置自定义)
- 页面压缩
- 模板引擎(基于ejs)
- 动态路由自动注册(使用path-match定制)
提供的工具
- 统一返回结果处理
- 定时任务设置
- Redis的集成使用
- Mysql的集成使用
- 基于数据模型的数据库CURD
配置文件范例
// app.config.js
const p = require('path');
module.exports = {
path: '.',
port: 8360,
plugins: {
Db: {
// mysql: {
// type: 'MYSQL',
// config: {
// host: '42.192.233.200',
// port: '3306',
// database: '',
// user: '',
// password: '',
// encoding: 'utf8mb4',
// dateStrings: true,
// connectionLimit: 5 // 最大连接数
// }
// },
// redis: {
// type: 'REDIS',
// config: {
// host: '127.0.0.1',
// port: '6379',
// maxConnections: 5 // 最大连接数
// }
// }
},
Result: {
// UNKNOWN_ERROR: {
// errCode: -1,
// msg: 'UNKNOWN_ERROR',
// msgZh: '未知异常'
// }
},
Logger: {
// 日志的输出
appenders: {
access: {
type: 'dateFile',
pattern: '-yyyy-MM-dd.log', //生成文件的规则
alwaysIncludePattern: true, // 文件名始终以日期区分
encoding: 'utf-8',
filename: p.join(process.cwd(), './logs/access'), //生成文件名
maxLogSize: 5 * 1000 * 1000, // 超过多少(byte)就切割
keepFileExt: true // 切割的日志保留文件扩展名,false(默认):生成类似default.log.1文件;true:生成类似default.1.log
},
application: {
type: 'dateFile',
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true,
encoding: 'utf-8',
filename: p.join(process.cwd(), './logs/application'),
maxLogSize: 5 * 1000 * 1000,
keepFileExt: true
},
debug: {
type: 'dateFile',
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true,
encoding: 'utf-8',
filename: p.join(process.cwd(), './logs/debug'),
maxLogSize: 5 * 1000 * 1000,
keepFileExt: true
},
error: {
type: 'dateFile',
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true,
encoding: 'utf-8',
filename: p.join(process.cwd(), './logs/error'),
maxLogSize: 5 * 1000 * 1000,
keepFileExt: true
},
out: {
type: 'console'
}
},
categories: {
default: { appenders: ['debug'], level: 'all' },
access: { appenders: ['access'], level: 'info' },
debug: { appenders: ['debug'], level: 'all' },
error: { appenders: ['error'], level: 'all' },
application: { appenders: ['application'], level: 'all' }
}
}
},
middlewares: {
Cors: {
enabled: true,
config: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Expose-Headers': '*',
'Access-Control-Max-Age': 60,
'Access-Control-Allow-Methods': 'PUT, POST, GET, DELETE, OPTIONS'
}
},
BodyParser: {
enabled: true,
config: {
opts: {
multipart: true,
formidable: {
maxFileSize: 200 * 1024 * 1024, // 设置上传文件大小最大限制,默认2M
keepExtensions: true // 保持后缀名
}
}
}
},
Compress: {
enabled: true,
config: {
opts: {
filter: function (content_type) {
return /text/i.test(content_type);
},
threshold: 2048
}
}
},
Assets: {
enabled: true,
config: {
path: '/',
mapping: './static',
opts: {
index: 'index.html', // 默认为true 访问的文件为index.html 可以修改为别的文件名
hidden: false, // 是否同意传输隐藏文件
defer: false // 如果为true,则在返回next()之后进行服务,从而允许后续中间件先进行响应
}
}
},
TemplateHandler: {
enabled: true,
config: {
mapping: './template'
}
},
DynamicRoutes: {
enabled: true,
config: {
routes: [
{ dir: '/server', mapping: '/api' },
{ dir: '/views', mapping: '/' }
]
}
}
}
};
程序使用
// main.js
// 默认 localhost:8360
const { Application } = require('@esidecn/leopold-base');
const config = require('./app.config');
const p = require('path');
const router = require('./router');
const app = new Application(config);
// 其他中间件函数
app.server.use(async (ctx, next) => {
await next();
});
app.server.use(router.routes(), router.allowedMethods());
app.start();
CURD 范例
// dayjs.js
const dayjs = require('dayjs');
dayjs.locale('zh-cn');
/**
* 日期格式化
* @param d
* @param format YYYY/MM/DD HH:mm:ss
*/
const formatDate = (d, format = 'YYYY/MM/DD HH:mm:ss') => {
return dayjs(d).format(format);
};
// filter.js
/**
* 使用model过滤结果集
* @param dbResult
* @param filter
* @returns {*}
*/
const filterDbResult = (dbResult, filter) => {
if (dbResult) {
return dbResult.map((item) => {
for (const key in filter) {
const filterFn = filter[key];
if (item[key] !== undefined && item[key] !== null) {
item[key] = filterFn(item[key], item) || item[key];
}
}
return item;
});
} else {
return [];
}
};
module.exports = {
filterDbResult
};
// validator.js
/**
* 收集必填的规则
* @param model
* @returns {{}}
*/
const getRequiredRules = (model) => {
let validator = {};
for (const key in model._column) {
validator[key] = [];
const columnItem = model._column[key];
if (columnItem.primaryKey || (columnItem.allowNull !== void 0 && !columnItem.allowNull)) {
if (!columnItem.defaultExpr) {
const requiredFn = (val) => {
if (val) return true;
else return false;
};
validator[key].push({
message: `字段${key}需要必填,请检查输入`,
fn: requiredFn
});
}
}
}
return validator;
};
/**
* 根据必填项验证参数
* @param params
* @param rules
* @returns {[boolean,string]}
*/
const validateRules = (params = {}, rules = {}) => {
let err = false;
let errMsg = '';
for (const key in rules) {
const validList = rules[key];
if (validList && validList.length > 0) {
for (const validItem of validList) {
if (!validItem.fn(params[key])) {
err = true;
errMsg = validItem.message;
}
}
}
}
return [err, errMsg];
};
module.exports = {
getRequiredRules,
validateRules
}
// serviceUtil.js
const { formatDate } = require('./dayjs');
const { filterDbResult } = require('./filter');
const { getRequiredRules, validateRules } = require('./validator');
// const { formatHumpLineTransfer } = require('../utils/formatter');
/**
* 创建表格
* @param ctx
* @param model
* @returns {Promise<{msg: string, errCode: number, msgZh: string}>}
*/
const createTable = async (ctx, model) => {
const { mysql } = ctx.$root.DB;
const { Result } = ctx.$root;
const res = Object.assign({}, Result.STATUS_CODE.SUCCESS);
const err = Object.assign({}, Result.STATUS_CODE.SQL_ERROR);
const sql = model.getCreateTableSql();
try {
res.data = await mysql.pureQuery(sql);
return res;
} catch (e) {
err.data = e.message;
return err;
}
};
/**
* 记录总数
* @param ctx
* @param model
* @param cond
* @returns {Promise<*>}
*/
const count = async (ctx, model, cond = {}) => {
const { mysql } = ctx.$root.DB;
const [sql] = model.select(['*']).addCond(cond).count().sql();
const res = await mysql.pureQuery(sql);
if (res && res.length > 0) {
return res[0].total;
} else {
return null;
}
};
/**
* 记录分页详情
* @param ctx
* @param model
* @param cond
* @returns {Promise<*>}
*/
const page = async (ctx, model, cond = {}) => {
const countResult = await count(ctx, model, cond);
let totalPage = 1;
let totalSize = countResult;
if (cond.pageSize && cond.pageSize > 0) {
totalPage = Math.ceil(totalSize / cond.pageSize);
}
return {
totalPage,
totalSize
};
};
/**
* 记录列表
* @param ctx
* @param model
* @param cond
* @returns {Promise<*>}
*/
const list = async (ctx, model, cond = {}) => {
const { mysql } = ctx.$root.DB;
const { Result } = ctx.$root;
const res = Object.assign({}, Result.STATUS_CODE.SUCCESS);
const err = Object.assign({}, Result.STATUS_CODE.SQL_ERROR);
try {
const pageResult = await page(ctx, model, cond);
const [sql] = model.select(['*']).addCond(cond).sql();
const dbResult = await mysql.pureQuery(sql);
const userFilter = model._filter || {};
pageResult.list = filterDbResult(dbResult, userFilter);
res.data = pageResult;
return res;
} catch (e) {
console.log(e);
err.data = e.message;
return err;
}
};
/**
* 查询单个model
* @param ctx
* @param model
* @param cond
* @returns {Promise<null|*>}
*/
const listOne = async (ctx, model, cond = {}) => {
const { mysql } = ctx.$root.DB;
const { Result } = ctx.$root;
const res = Object.assign({}, Result.STATUS_CODE.SUCCESS);
const err = Object.assign({}, Result.STATUS_CODE.SQL_ERROR);
try {
const [sql] = model.select(['*']).addCond(cond).sql();
const dbResult = await mysql.pureQuery(sql);
const userFilter = model._filter || {};
const filterResult = filterDbResult(dbResult, userFilter);
if (filterResult && filterResult.length > 0) {
res.data = filterResult[0];
} else {
res.data = null;
}
return res;
} catch (e) {
err.data = e.message;
return err;
}
};
/**
* 新建
* @param ctx
* @param model
* @param params
* @returns {Promise<void>}
*/
const save = async (ctx, model, params = {}) => {
const { mysql } = ctx.$root.DB;
const { Result } = ctx.$root;
const res = Object.assign({}, Result.STATUS_CODE.SUCCESS);
const paramErr = Object.assign({}, Result.STATUS_CODE.PARAM_ERROR);
const sqlErr = Object.assign({}, Result.STATUS_CODE.SQL_ERROR);
const unknownErr = Object.assign({}, Result.STATUS_CODE.UNKNOWN_ERROR);
const rules = getRequiredRules(model);
const [err, errMsg] = validateRules(params, rules);
if (!err) {
const [sql] = model.create(params).sql();
try {
const dbResult = await mysql.pureQuery(sql);
/*
OkPacket {
fieldCount: 0,
affectedRows: 1,
insertId: 0,
serverStatus: 2,
warningCount: 0,
message: '',
protocol41: true,
changedRows: 0
}
*/
if (dbResult && dbResult.affectedRows > 0) {
res.data = 'success';
return res;
} else {
return unknownErr;
}
} catch (e) {
sqlErr.data = e.message;
return sqlErr;
}
} else {
paramErr.data = errMsg;
return paramErr;
}
};
/**
* 更新
* @param ctx
* @param model
* @param params
* @param isUpdateAt
* @param pk
* @returns {Promise<void>}
*/
const update = async (ctx, model, params = {}, isUpdateAt = true, pk = 'code') => {
if (isUpdateAt && !params['update_at']) {
params['update_at'] = formatDate(new Date(), 'YYYY/MM/DD HH:mm:ss');
}
const { mysql } = ctx.$root.DB;
const { Result } = ctx.$root;
const res = Object.assign({}, Result.STATUS_CODE.SUCCESS);
const sqlErr = Object.assign({}, Result.STATUS_CODE.SQL_ERROR);
const unknownErr = Object.assign({}, Result.STATUS_CODE.UNKNOWN_ERROR);
const cond = { [pk]: params[pk] };
const modelObj = Object.assign({}, params);
delete modelObj[pk];
const [sql] = model.update(modelObj).addCond(cond).sql();
try {
const dbResult = await mysql.pureQuery(sql);
/*
OkPacket {
fieldCount: 0,
affectedRows: 1,
insertId: 0,
serverStatus: 2,
warningCount: 0,
message: '',
protocol41: true,
changedRows: 1
}
*/
if (dbResult) {
res.data = 'success';
return res;
} else {
return unknownErr;
}
} catch (e) {
sqlErr.data = e.message;
return sqlErr;
}
};
/**
* 软移除
* @param ctx
* @param model
* @param params
* @param isUpdateAt
* @param softKey
* @param pk
* @returns {Promise<void>}
*/
const remove = async (ctx, model, params = {}, isUpdateAt = true, softKey = 'state', pk = 'code') => {
if (isUpdateAt && !params['update_at']) {
params['update_at'] = formatDate(new Date(), 'YYYY/MM/DD HH:mm:ss');
}
const { mysql } = ctx.$root.DB;
const { Result } = ctx.$root;
const res = Object.assign({}, Result.STATUS_CODE.SUCCESS);
const sqlErr = Object.assign({}, Result.STATUS_CODE.SQL_ERROR);
const unknownErr = Object.assign({}, Result.STATUS_CODE.UNKNOWN_ERROR);
const cond = { [pk]: params[pk] };
const modelObj = { [softKey]: 3 };
delete model[pk];
const [sql] = model.update(modelObj).addCond(cond).sql();
try {
const dbResult = await mysql.pureQuery(sql);
/*
OkPacket {
fieldCount: 0,
affectedRows: 1,
insertId: 0,
serverStatus: 2,
warningCount: 0,
message: '',
protocol41: true,
changedRows: 1
}
*/
if (dbResult) {
res.data = 'success';
return res;
} else {
return unknownErr;
}
} catch (e) {
sqlErr.data = e.message;
return sqlErr;
}
};
module.exports = {
createTable,
count,
list,
listOne,
save,
update,
remove
};
// server/user/list.js 中使用
const serviceUtil = require('../../utils/serviceUtil');
const UserModel = require('../../model/UserModel');
/**
* 查询用户列表
* @param ctx
* @returns {Promise<void>}
*/
module.exports = async (ctx) => {
const { Result } = ctx.$root;
if (ctx.method === 'POST') {
ctx.body = await serviceUtil.list(ctx, UserModel, ctx.request.body);
} else if (ctx.method === 'GET') {
ctx.body = Result.fail(null, 'unsupported', Result.STATUS_CODE.UNKNOWN_ERROR);
}
};
Model定义参考
const { SQLModel, SQLModelType } = require('@esidecn/leopold-base');
const UserModel = new SQLModel({
table: 'tb_user',
column: {
code: {
type: SQLModelType.VARCHAR(100),
allowNull: false,
unique: true,
primaryKey: true,
comment: '用户code'
},
fk_group: {
type: SQLModelType.VARCHAR(100),
allowNull: false,
defaultExpr: 'leopold',
comment: '用户所在group'
},
fk_department: {
type: SQLModelType.VARCHAR(100),
comment: '用户所在部门'
},
account: {
type: SQLModelType.VARCHAR(100),
allowNull: false,
unique: true,
comment: '用户账号'
},
passwd: {
type: SQLModelType.VARCHAR(100),
allowNull: false,
unique: true,
comment: '用户密码'
},
name: {
type: SQLModelType.VARCHAR(50),
comment: '用户名称'
},
description: {
type: SQLModelType.VARCHAR(100),
comment: '用户描述'
},
sex: {
type: SQLModelType.INTEGER(),
defaultExpr: '0',
comment: '用户性别 男(1) 女(2) 未知(0)'
},
phone_area_code: {
type: SQLModelType.VARCHAR(10),
defaultExpr: '86'
},
phone: {
type: SQLModelType.VARCHAR(20)
},
email: {
type: SQLModelType.VARCHAR(20)
},
state: {
type: SQLModelType.INTEGER(),
allowNull: false,
defaultExpr: '1',
comment: '正常(1) 锁定(2) 注销(3)'
},
update_at: {
type: SQLModelType.DATETIME(),
defaultExpr: 'CURRENT_TIMESTAMP',
otherExpr: 'ON UPDATE CURRENT_TIMESTAMP'
},
create_at: {
type: SQLModelType.DATETIME(),
defaultExpr: 'CURRENT_TIMESTAMP'
}
},
ref: [
// {
// table: 'tb_group',
// where: 'tb_group.code = tb_user.fk_group',
// column: {
// name: {
// alias: 'fkGroupName'
// }
// }
// },
{
table: 'tb_department',
where: 'tb_department.code = tb_user.fk_department',
column: {
name: {
alias: 'fkDepartmentName'
}
}
}
],
filter: {
passwd: (val, row = {}) => {
return '**********';
},
sex: (val, row = {}) => {
if (val === 1) {
row.sex_name = '男';
} else if (val === 2) {
row.sex_name = '女';
} else {
row.sex_name = '未知';
}
return val;
},
phone: (val, row = {}) => {
let phone = val;
phone = phone.replace(phone.substring(3, 7), '****');
return phone;
},
state: (val, row = {}) => {
if (val === 1) {
row.state_name = '正常';
} else if (val === 2) {
row.state_name = '锁定';
} else {
row.state_name = '注销';
}
return val;
}
}
});
module.exports = UserModel;
Log使用范例
// server/index.js
module.exports = async (ctx) => {
const { Result, Log } = ctx.$root;
const logger = Log.getLogger('application')
logger.info('写入信息到log')
ctx.body = Result.success(ctx, 'success');
};
fix记录
- 0.0.3
- 移除ErrorHandler
- 2024/01/14 data数据多余的‘’拼接问题
- 2024/01/15 补充OrderBy与GroupBy初步支持
- 2024/01/21 全部参数化,可以按需使用配置文件覆盖原始的部分配置
- 0.0.5
- 2023/04/04 Log配置提升
- 2023/04/04 常用工具,直接挂载到ctx