优化错误提示,抽离中间件

This commit is contained in:
xuecong 2021-11-09 18:10:27 +08:00
parent 9c945a03be
commit 415a463027
17 changed files with 375 additions and 248 deletions

View File

@ -19,9 +19,6 @@ import KoaBodyMiddleware from 'koa-body';
import KoaSessionMilddleware from 'koa-session';
import KoaLogMiddleware from 'koa-logger';
import MysqlSessionStore from '~/store/mysql-session';
import Knex from 'knex';
import WebSocket from 'ws';
import * as redisService from '~/service/redis';
import http from 'http';
import cwlog from 'chowa-log';
import config from '~/config';
@ -30,8 +27,11 @@ import MpModule from '~/module/mp';
import PcModule from '~/module/pc';
import NotifyModule from '~/module/notify';
import OaModule from '~/module/oa';
import utils from '~/utils';
import { SYSTEMT_NOT_INIT } from '~/constant/code';
import wss from '~/wss';
import * as redisService from '~/service/redis';
import ModelMiddleware from '~/middleware/model';
import IpMiddleware from '~/middleware/ip';
import HeaderMiddleware from '~/middleware/header';
if (cluster.isMaster) {
cwlog.success(`main process ${process.pid}`);
@ -43,16 +43,7 @@ if (cluster.isMaster) {
const app = new Koa();
const router = new KoaRouter();
const server = http.createServer(app.callback());
const wss = new WebSocket.Server({ server, path: '/cws' });
const model = Knex({
client: 'mysql',
connection: config.mysqlConfig,
pool: {
min: 0,
max: 200
}
});
console.log(config);
cwlog.setProject(`${config.name}-${process.pid}`);
cwlog.displayDate();
@ -65,87 +56,33 @@ if (cluster.isMaster) {
NotifyModule(router);
OaModule(router);
// for socket
redisService.subscribe(model, wss);
app.use(KoaBodyMiddleware({ multipart: true }));
app.use(
KoaLogMiddleware({
transporter: str => {
cwlog.log(`${str}`);
}
})
);
app.use(
KoaSessionMilddleware(
{
store: new MysqlSessionStore(model),
...config.session
},
app
app.use(KoaBodyMiddleware({ multipart: true }))
.use(
KoaLogMiddleware({
transporter: str => {
cwlog.log(`${str}`);
}
})
)
);
app.use(async (ctx, next) => {
ctx.model = model;
ctx.request.ip = (ctx.request.header['x-real-ip'] as string) || ctx.request.ip;
.use(
KoaSessionMilddleware(
{
store: new MysqlSessionStore(),
...config.session
},
app
)
)
.use(ModelMiddleware())
.use(IpMiddleware())
.use(HeaderMiddleware())
.use(router.routes());
ctx.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
// WebSocket
wss.init(server);
if (ctx.method == 'OPTIONS') {
ctx.body = '';
ctx.status = 204;
} else {
const isInitAction = /^\/pc\/init\/\w+$/.test(ctx.request.path);
if (!config.inited && !/^\/pc\/upload\/sign$/.test(ctx.request.path)) {
const total = utils.sql.countReader(await ctx.model.from('ejyy_property_company_admin').count());
if (total === 0) {
if (!isInitAction) {
return (ctx.body = {
code: SYSTEMT_NOT_INIT,
message: '系统未初始化'
});
}
} else {
config.inited = true;
}
} else {
if (isInitAction) {
ctx.redirect('https://www.chowa.cn');
}
}
try {
await next();
} catch (error) {
ctx.status = 500;
if (config.debug) {
cwlog.error('===============错误捕捉开始=================');
console.log(error);
cwlog.error('===============错误捕捉结束=================');
} else {
utils.mail.send({
subject: `错误捕获`,
content: [
`访问地址:${ctx.request.path}`,
`进程号:${process.pid}`,
`body参数 ${JSON.stringify(ctx.request.body)}`,
`params参数 ${JSON.stringify(ctx.params)}`,
`进程号:${process.pid}`,
`错误原因:${error}`
]
});
}
}
}
if (ctx.status === 404) {
ctx.redirect('https://www.chowa.cn');
}
});
app.use(router.routes());
// for socket
redisService.subscribe();
const port = process.env.port ? parseInt(process.env.port, 10) : config.server.port;

View File

@ -0,0 +1,28 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import { Middleware, DefaultState, DefaultContext } from 'koa';
function HeaderMiddleware(): Middleware<DefaultState, DefaultContext> {
return async (ctx: DefaultContext, next) => {
ctx.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
if (ctx.method == 'OPTIONS') {
ctx.body = '';
return (ctx.status = 204);
}
await next();
};
}
export default HeaderMiddleware;

View File

@ -0,0 +1,23 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import { Middleware, DefaultState, DefaultContext } from 'koa';
function IpMiddleware(): Middleware<DefaultState, DefaultContext> {
return async (ctx, next) => {
ctx.request.ip = (ctx.request.header['x-real-ip'] as string) || ctx.request.ip;
await next();
};
}
export default IpMiddleware;

View File

@ -0,0 +1,31 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import { Middleware, DefaultState, DefaultContext } from 'koa';
import Knex from 'knex';
import model from '~/model';
declare module 'koa' {
interface BaseContext {
model: Knex;
}
}
function ModelMiddleware(): Middleware<DefaultState, DefaultContext> {
return async (ctx, next) => {
ctx.model = model;
await next();
};
}
export default ModelMiddleware;

View File

@ -0,0 +1,51 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import { Middleware, DefaultState, DefaultContext } from 'koa';
import utils from '~/utils';
import cwlog from 'chowa-log';
import config from '~/config';
function WatcherMiddleware(): Middleware<DefaultState, DefaultContext> {
return async (ctx, next) => {
try {
await next();
} catch (error) {
ctx.status = 500;
if (config.debug) {
cwlog.error('===============错误捕捉开始=================');
console.log(error);
cwlog.error('===============错误捕捉结束=================');
} else {
utils.mail.send({
to: config.smtp.to,
subject: `${config.name}异常捕获`,
content: [
`访问地址:${ctx.request.path}`,
`进程号:${process.pid}`,
`body参数 ${JSON.stringify(ctx.request.body)}`,
`params参数 ${JSON.stringify(ctx.params)}`,
`进程号:${process.pid}`,
`错误原因:${error}`
]
});
}
}
if (ctx.status === 404) {
ctx.redirect('https://www.chowa.cn');
}
};
}
export default WatcherMiddleware;

25
server/src/model/index.ts Normal file
View File

@ -0,0 +1,25 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import Knex from 'knex';
import config from '~/config';
const model = Knex({
client: 'mysql',
connection: config.mysqlConfig,
pool: {
min: 0,
max: 200
}
});
export default model;

View File

@ -49,7 +49,8 @@ const MpUserSupplementAction = <Action>{
{
name: 'idcard',
required: true,
validator: val => utils.idcard.verify(val)
validator: val => utils.idcard.verify(val),
message: '身份证号码验证失败'
},
{
name: 'avatar_url',

View File

@ -70,17 +70,19 @@ function MpModule(appRouter: KoaRouter) {
}
}
if (validatorService(ctx, validator)) {
const vs = validatorService(ctx, validator);
if (!vs.success) {
return (ctx.body = {
code: PARAMS_ERROR,
message: '参数错误'
message: vs.message
});
}
await response.apply(this, [ctx, next]);
});
if (process.env.NODE_ENV !== 'production') {
if (config.debug) {
cwlog.info(`${name} mounted and request from ${path.posix.join('/mp', router.path)} by ${router.method}`);
}
}

View File

@ -16,6 +16,7 @@ import { NotifyAction } from '~/types/action';
import KoaRouter from 'koa-router';
import * as NotifyModuleRouter from './router';
import cwlog from 'chowa-log';
import config from '~/config';
function MpModule(appRouter: KoaRouter) {
for (const name in NotifyModuleRouter) {
@ -25,7 +26,7 @@ function MpModule(appRouter: KoaRouter) {
await response.apply(this, [ctx, next]);
});
if (process.env.NODE_ENV !== 'production') {
if (config.debug) {
cwlog.info(
`${name} mounted and request from ${path.posix.join('/notify', router.path)} by ${router.method}`
);

View File

@ -18,6 +18,7 @@ import * as OaModuleRouter from './router';
import cwlog from 'chowa-log';
import * as wechatService from '~/service/wechat';
import menu from './menu';
import config from '~/config';
async function OaModule(appRouter: KoaRouter) {
for (const name in OaModuleRouter) {
@ -27,7 +28,7 @@ async function OaModule(appRouter: KoaRouter) {
await response.apply(this, [ctx, next]);
});
if (process.env.NODE_ENV !== 'production') {
if (config.debug) {
cwlog.info(`${name} mounted and request from ${path.posix.join('/oa', router.path)} by ${router.method}`);
}
}

View File

@ -115,17 +115,19 @@ function PcModule(appRouter: KoaRouter) {
}
}
if (validatorService(ctx, validator)) {
const vs = validatorService(ctx, validator);
if (!vs.success) {
return (ctx.body = {
code: PARAMS_ERROR,
message: '参数错误'
message: vs.message
});
}
await response.apply(this, [ctx, next]);
});
if (process.env.NODE_ENV !== 'production') {
if (config.debug) {
cwlog.info(`${name} mounted and request from ${path.posix.join('/pc', router.path)} by ${router.method}`);
}
}

View File

@ -11,40 +11,16 @@
*/
import redis from 'redis';
import Knex from 'knex';
import WebSocket from 'ws';
import http from 'http';
import quertString from 'query-string';
import { CwWebSocket } from '~/types/ws';
import wss, { PcData } from '~/wss';
import config from '~/config';
import { Role } from '~/constant/role_access';
const pub = process.env.NODE_ENV === 'production' ? redis.createClient(config.redis) : null;
const sub = process.env.NODE_ENV === 'production' ? redis.createClient(config.redis) : null;
let wss = null;
const pub = config.debug ? null : redis.createClient(config.redis);
const sub = config.debug ? null : redis.createClient(config.redis);
export const WS_NOTICE_TO_PROPERTY_COMPANY = 'WS_NOTICE_TO_PROPERTY_COMPANY';
export const WS_NOTICE_TO_REMOTE_SERVER = 'WS_NOTICE_TO_REMOTE_SERVER';
interface PcData {
id: number;
community_id: number;
type: Role;
urge: boolean;
}
function sendToPc(data: PcData) {
if (!(wss instanceof WebSocket.Server)) {
return;
}
wss.clients.forEach((client: CwWebSocket) => {
if (client.readyState === WebSocket.OPEN && client.access.includes(data.type)) {
client.send(JSON.stringify(data));
}
});
}
// todo
interface RsData {
remote_id: number;
door_id: number;
@ -57,7 +33,7 @@ function sendToRs(data: RsData) {
function dispatch(channel: string, data: Object) {
switch (channel) {
case WS_NOTICE_TO_PROPERTY_COMPANY:
return sendToPc(data as PcData);
return wss.sendToPc(data as PcData);
case WS_NOTICE_TO_REMOTE_SERVER:
return sendToRs(data as RsData);
@ -65,50 +41,15 @@ function dispatch(channel: string, data: Object) {
}
export function pubish(channel: string, data: Object) {
if (process.env.NODE_ENV === 'production') {
if (!config.debug) {
pub.publish(channel, JSON.stringify(data));
} else {
dispatch(channel, data);
}
}
export function subscribe(model: Knex, w: WebSocket.Server) {
wss = w;
wss.on('connection', async (ws: CwWebSocket, request: http.IncomingMessage) => {
const {
query: { token }
} = quertString.parseUrl(request.url);
if (!token) {
return ws.close();
}
const pcUserInfo = await model
.table('ejyy_property_company_auth')
.leftJoin(
'ejyy_property_company_user',
'ejyy_property_company_user.id',
'ejyy_property_company_auth.property_company_user_id'
)
.leftJoin(
'ejyy_property_company_access',
'ejyy_property_company_access.id',
'ejyy_property_company_user.access_id'
)
.where('ejyy_property_company_auth.token', token)
.select('ejyy_property_company_user.id', 'ejyy_property_company_access.content as access')
.first();
if (!pcUserInfo) {
return ws.close();
}
ws.user_id = pcUserInfo.id;
ws.access = pcUserInfo.access;
});
if (process.env.NODE_ENV === 'production') {
export async function subscribe() {
if (!config.debug) {
sub.subscribe(WS_NOTICE_TO_PROPERTY_COMPANY);
sub.subscribe(WS_NOTICE_TO_REMOTE_SERVER);

View File

@ -11,80 +11,88 @@
*/
import { Context } from 'koa';
import { ValidatorDeclare } from '~/types/action';
import config from '~/config';
import cwlog from 'chowa-log';
import { ValidatorDeclare, FieldVerifyDeclare } from '~/types/action';
function validatorService(ctx: Context, validator: ValidatorDeclare): boolean {
if (!validator) {
return false;
interface ValidatorResult {
success: boolean;
message?: string;
}
interface FieldVeirfy extends FieldVerifyDeclare {
value: any;
}
function validatorService(ctx: Context, validatorDeclare: ValidatorDeclare): ValidatorResult {
if (!validatorDeclare) {
return { success: true };
}
return !['body', 'params', 'query', 'files'].every(refer => {
const origin = validator[refer];
const fileds: FieldVeirfy[] = [];
if (!Array.isArray(origin)) {
return true;
['body', 'params', 'query', 'files'].forEach((refer: 'body' | 'params' | 'query' | 'files') => {
if (!Array.isArray(validatorDeclare[refer])) {
return;
}
return origin.every(({ name, required, length, min, max, regex, validator }) => {
let value = undefined;
if (refer === 'params') {
value = ctx.params[name];
} else {
value = ctx.request[refer][name];
}
if (
required === true &&
((Array.isArray(value) && value.length === 0) ||
(!Array.isArray(value) && (value == undefined || value === '')))
) {
if (config.debug) {
cwlog.warning(`${name} 字段必须`);
}
return false;
}
if (length && value && value.length !== length) {
if (config.debug) {
cwlog.warning(`${name} 长度必须等于 ${length},当前值:${value}`);
}
return false;
}
if (min && value && value.length < min) {
if (config.debug) {
cwlog.warning(`${name} 长度必须小于 ${min},当前值:${value}`);
}
return false;
}
if (max && value && value.length > max) {
if (config.debug) {
cwlog.warning(`${name} 长度必须大于 ${max},当前值:${value}`);
}
return false;
}
if (regex && value && !regex.test(value)) {
if (config.debug) {
cwlog.warning(`${name} 必须满足正则 ${regex},当前值:${value}`);
}
return false;
}
if (validator && value && !validator(value)) {
if (config.debug) {
cwlog.warning(`${name} 自定义验证未通过,当前值:${value}`);
}
return false;
}
return true;
validatorDeclare[refer].forEach(declare => {
fileds.push({
value: refer === 'params' ? ctx.params[declare.name] : ctx.request[refer][declare.name],
...declare
});
});
});
for (let i = 0; i < fileds.length; i++) {
const { name, required, length, min, max, regex, validator, value, message } = fileds[i];
if (
required === true &&
((Array.isArray(value) && value.length === 0) ||
(!Array.isArray(value) && (value == undefined || value === '')))
) {
return {
success: false,
message: message ? message : `参数错误,${name}字段是必填字段`
};
}
if (length && value && value.length !== length) {
return {
success: false,
message: message ? message : `参数错误,${name}字段长度必须等于 ${length}`
};
}
if (min && value && value.length < min) {
return {
success: false,
message: message ? message : `参数错误,${name}字段长度必须大于 ${min}`
};
}
if (max && value && value.length > max) {
return {
success: false,
message: message ? message : `参数错误,${name}字段长度必须小于 ${max}`
};
}
if (regex && value && !regex.test(value)) {
return {
success: false,
message: message ? message : `参数错误,${name}字段必须满足正则 ${regex}`
};
}
if (validator && value && !validator(value)) {
return {
success: false,
message: message ? message : `参数错误,${name}字段验证未通过`
};
}
}
return { success: true };
}
export default validatorService;

View File

@ -10,7 +10,7 @@
* +----------------------------------------------------------------------
*/
import Knex from 'knex';
import model from '~/model';
import { Session } from 'koa-session';
const FORTY_FIVE_MINUTES = 45 * 60 * 1000;
@ -34,14 +34,8 @@ function getExpiresOn(session: Session, ttl: number): number {
}
class MysqlSessionStore {
constructor(model: Knex) {
this.model = model;
}
model = <Knex>null;
async get(sid: string): Promise<Session> {
const row = await this.model
const row = await model
.from('ejyy_session_store')
.where('id', sid)
.where('expire', '>', Date.now())
@ -60,14 +54,14 @@ class MysqlSessionStore {
let expire = getExpiresOn(session, ttl).valueOf();
let data = JSON.stringify(session);
await this.model.raw(
await model.raw(
'INSERT INTO ejyy_session_store (id, expire, data) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE expire=?, data =?',
[sid, expire, data, expire, data]
);
}
async destroy(sid: string) {
await this.model
await model
.from('ejyy_session_store')
.where('id', sid)
.delete();

View File

@ -32,6 +32,7 @@ declare namespace Action {
min?: number;
max?: number;
regex?: RegExp;
message?: string;
validator?: (value: any) => boolean;
}

View File

@ -9,8 +9,6 @@
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import Knex from 'knex';
import { MpUserInfo, PcUserInfo, OaUserInfo } from './user-info';
interface InterfaceBody {
@ -24,7 +22,6 @@ declare module 'koa' {
mpUserInfo: MpUserInfo;
pcUserInfo: PcUserInfo;
OaUserInfo: OaUserInfo;
model: Knex;
}
interface ContextDelegatedResponse {

84
server/src/wss/index.ts Normal file
View File

@ -0,0 +1,84 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import WebSocket from 'ws';
import http from 'http';
import quertString from 'query-string';
import model from '~/model';
import { Role } from '~/constant/role_access';
export interface CwWebSocket extends WebSocket {
access?: Role[];
user_id?: number;
}
export interface PcData {
id: number;
community_id: number;
type: Role;
urge: boolean;
}
class ws {
static ws: WebSocket.Server;
static init(server: http.Server) {
this.ws = new WebSocket.Server({ server, path: '/cws' });
this.ws.on('connection', async (ws: CwWebSocket, request: http.IncomingMessage) => {
const {
query: { token }
} = quertString.parseUrl(request.url);
if (!token) {
return ws.close();
}
const userInfo = await model
.table('ejyy_property_company_auth')
.leftJoin(
'ejyy_property_company_user',
'ejyy_property_company_user.id',
'ejyy_property_company_auth.property_company_user_id'
)
.leftJoin(
'ejyy_property_company_access',
'ejyy_property_company_access.id',
'ejyy_property_company_user.access_id'
)
.where('ejyy_property_company_auth.token', token)
.select('ejyy_property_company_user.id', 'ejyy_property_company_access.content')
.first();
if (!userInfo) {
return ws.close();
}
ws.user_id = userInfo.id;
ws.access = userInfo.content;
});
}
static sendToPc(data: PcData) {
if (!(this.ws instanceof WebSocket.Server)) {
return;
}
this.ws.clients.forEach((client: CwWebSocket) => {
if (client.readyState === WebSocket.OPEN && client.access.includes(data.type)) {
client.send(JSON.stringify(data));
}
});
}
}
export default ws;