project init

This commit is contained in:
cebgcontract 2022-10-18 15:00:38 +08:00
commit ac132ad2f0
32 changed files with 4064 additions and 0 deletions

7
.env.development Normal file
View File

@ -0,0 +1,7 @@
API_PORT=3006
API_HOST=127.0.0.1
API_TOKEN_SECRET=sdf(**&*&xx2213
API_TOKEN_EXPIRESIN=1d
DB_MAIN=mongodb://localhost/wallet-development

6
.env.production Normal file
View File

@ -0,0 +1,6 @@
API_PORT=3001
API_HOST=127.0.0.1
API_TOKEN_SECRET=sdf(**&*&xx2214
API_TOKEN_EXPIRESIN=1d
DB_MAIN=mongodb://localhost/wallet-production

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
dist/*.js

14
.eslintrc.js Normal file
View File

@ -0,0 +1,14 @@
/** @format */
module.exports = {
parser: '@typescript-eslint/parser', //定义ESLint的解析器
extends: [
'plugin:prettier/recommended', // 使用prettier中的样式规范且如果使得ESLint会检测prettier的格式问题同样将格式问题以error的形式抛出
],
parserOptions: {ecmaVersion: 2019, sourceType: 'module'},
env: {
//指定代码的运行环境
browser: true,
node: true,
},
}

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.idea
node_modules
build
dist
.DS_Store

13
.prettierrc.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
"printWidth": 120,
"semi": false, // 在语句末尾添加分号
"singleQuote": true, // 使用单引号而非双引号
"trailingComma": "all", // 在任何可能的多行中输入尾逗号
"bracketSpacing": true, // 在对象字面量声明所使用的的花括号前后({})输出空格
"jsxBracketSameLine": true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)
"arrowParens": "avoid", // 为单行箭头函数的参数添加圆括号。
"requirePragma": false, // Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码
"insertPragma": false, // 顶部插入一个 @format
"tabWidth": 2,
"useTabs": false,
};

41
package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "wallet-svr",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev:api": "ts-node -r tsconfig-paths/register src/api.ts",
"prod:api": "NODE_PATH=./dist node dist/api.js",
"lint": "eslint --ext .ts src/**",
"format": "eslint --ext .ts src/** --fix"
},
"author": "",
"license": "ISC",
"dependencies": {
"@fastify/cors": "^8.1.0",
"@fastify/formbody": "^7.3.0",
"@fastify/helmet": "^10.0.1",
"@fastify/jwt": "^6.3.2",
"@typegoose/typegoose": "^9.12.1",
"axios": "^1.1.3",
"dotenv": "^16.0.3",
"fastify": "^4.8.1",
"fastify-plugin": "^4.2.1",
"mongoose": "^6.6.5",
"mongoose-findorcreate": "^3.0.0",
"tracer": "^1.1.6"
},
"devDependencies": {
"@types/dotenv": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"eslint": "^8.25.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.7.1",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.1.0",
"typescript": "^4.8.4"
}
}

1
pm2_dev.sh Executable file
View File

@ -0,0 +1 @@
pm2 start npm --name "wallet-svr" --log-date-format "YYYY-MM-DD HH:mm:ss" -- run "dev:api"

177
src/api.server.ts Normal file
View File

@ -0,0 +1,177 @@
import fastify, {
FastifyError,
FastifyInstance,
FastifyReply,
FastifyRequest,
} from "fastify";
import helmet from "@fastify/helmet";
import { IncomingMessage, Server, ServerResponse } from "http";
import { RouterMap } from "decorators/router";
import { mongoose } from "@typegoose/typegoose";
import logger from "logger/logger";
import config from "config/config";
const zReqParserPlugin = require("plugins/zReqParser");
const zTokenParserPlugin = require("plugins/zTokenParser");
const apiAuthPlugin = require("plugins/apiauth");
const fs = require("fs");
const join = require("path").join;
require("./common/Extend");
export class ApiServer {
server: FastifyInstance<Server, IncomingMessage, ServerResponse>;
public constructor() {
this.server = fastify({ logger: true, trustProxy: true });
this.registerPlugins();
}
private registerPlugins() {
this.server.register(require("@fastify/formbody"));
this.server.register(zReqParserPlugin);
this.server.register(helmet, { hidePoweredBy: false });
this.server.register(zTokenParserPlugin);
this.server.register(apiAuthPlugin, {
secret: config.api.token_secret,
expiresIn: config.api.token_expiresIn,
});
this.server.register(require("@fastify/cors"), {});
}
private registerRouter() {
logger.log("register api routers");
let self = this;
for (let [controller, config] of RouterMap.decoratedRouters) {
for (let data of config.data) {
logger.info(
"add router",
data.method || "all",
data.path,
`${data.target.constructor.name}.${controller.name}()`
);
// @ts-ignore
self.server[data.method || "all"](
data.path,
{
preValidation: async function (
request: FastifyRequest,
reply: FastifyReply
) {
request.roles = config.roles;
await this.apiAuth(request, reply);
},
},
controller
);
}
}
}
/**
* controller
*/
initControllers() {
logger.info("Bootstrap controllers...");
const controllers = join(__dirname, "./controllers");
fs.readdirSync(controllers)
.filter((file: string) => ~file.search(/^[^.].*\.(ts|js)$/))
.forEach((file: any) => {
// logger.log(file);
return require(join(controllers, file));
});
}
async connectDB() {
const options = {
useNewUrlParser: true,
poolSize: 5,
keepAlive: true,
keepAliveInitialDelay: 300000,
useUnifiedTopology: true,
};
const uri = config.db_main;
logger.info(`connect to ${uri} ...`);
try {
// await mongoose.createConnection(uri, options)
await mongoose.connect(uri, options);
logger.log("DB Connected");
} catch (err) {
logger.log(`DB Connection Error: ${err.message}`);
}
}
private setErrHandler() {
this.server.setNotFoundHandler(function (
request: any,
reply: { send: (arg0: { errcode: number; errmsg: string }) => void }
) {
reply.send({ errcode: 404, errmsg: "page not found" });
});
this.server.setErrorHandler(function (
error: FastifyError,
request: FastifyRequest,
reply: FastifyReply
) {
let statusCode = (error && error.statusCode) || 100;
if (statusCode >= 500) {
logger.error(error);
} else if (statusCode >= 400) {
logger.info(error);
} else {
logger.error(error);
}
reply.code(200).send({
errcode: statusCode,
errmsg: error ? error.message : "unknown error",
});
});
}
/**
* ,
* {
* code: 0,
* msg?: '',
* data: any
* }
* @private
*/
private setFormatSend() {
this.server.addHook(
"preSerialization",
async (request: FastifyRequest, reply: FastifyReply, payload) => {
reply.header("X-Powered-By", "PHP/5.4.16");
// @ts-ignore
if (!payload.errcode) {
payload = {
errcode: 0,
data: payload,
};
}
return payload;
}
);
}
public async start() {
let self = this;
return new Promise(async (resolve, reject) => {
await self.connectDB();
self.initControllers();
self.registerRouter();
self.setErrHandler();
self.setFormatSend();
// new Schedule().start();
this.server.listen(
{ port: config.api.port },
(err: any, address: any) => {
if (err) {
logger.log(err);
process.exit(0);
}
resolve && resolve(address);
}
);
});
}
}

12
src/api.ts Normal file
View File

@ -0,0 +1,12 @@
import { ApiServer } from "api.server";
import logger from "./logger/logger";
class Server extends ApiServer {
constructor() {
super();
}
}
let server = new Server();
server.start().then((address) => {
logger.log(`Api Server listening at ${address}`);
});

56
src/clock/Clock.ts Normal file
View File

@ -0,0 +1,56 @@
export class Clock {
public running: boolean = false
public paused: boolean = false
public deltaTime: number
public currentTime: number
public elapsedTime: number
protected now: Function =
(typeof window !== 'undefined' &&
window.performance &&
window.performance.now &&
window.performance.now.bind(window.performance)) ||
Date.now
protected _interval
constructor(useInterval: boolean = true) {
this.start(useInterval)
}
start(useInterval: boolean = true) {
this.deltaTime = 0
this.currentTime = this.now()
this.elapsedTime = 0
this.running = true
if (useInterval) {
// auto set interval to 60 ticks per second
this._interval = setInterval(this.tick.bind(this), 1000 / 60)
}
}
stop() {
this.running = false
if (this._interval) {
clearInterval(this._interval)
}
}
tick(newTime = this.now()) {
if (!this.paused) {
this.deltaTime = newTime - this.currentTime
this.elapsedTime += this.deltaTime
}
this.currentTime = newTime
}
pause() {
this.paused = true
}
resume() {
this.paused = false
}
}

50
src/clock/ClockTimer.ts Normal file
View File

@ -0,0 +1,50 @@
import { Delayed, Type } from './Delayed'
import { Clock } from './Clock'
export default class ClockTimer extends Clock {
delayed: Delayed[] = []
constructor(autoStart: boolean = false) {
super(autoStart)
}
tick() {
super.tick()
if (this.paused) {
return
}
let delayedList = this.delayed
let i = delayedList.length
while (i--) {
const delayed = delayedList[i]
if (delayed.active) {
delayed.tick(this.deltaTime)
} else {
delayedList.splice(i, 1)
continue
}
}
}
setInterval(handler: Function, time: number, ...args: any[]) {
let delayed = new Delayed(handler, args, time, Type.Interval)
this.delayed.push(delayed)
return delayed
}
setTimeout(handler: Function, time: number, ...args: any[]) {
let delayed = new Delayed(handler, args, time, Type.Timeout)
this.delayed.push(delayed)
return delayed
}
clear() {
let i = this.delayed.length
while (i--) {
this.delayed[i].clear()
}
this.delayed = []
}
}

61
src/clock/Delayed.ts Normal file
View File

@ -0,0 +1,61 @@
export enum Type {
Interval,
Timeout,
}
export class Delayed {
public active: boolean = true
public paused: boolean = false
public time: number
public elapsedTime: number = 0
protected handler: Function
protected args: any
protected type: number
constructor(handler: Function, args: any, time: number, type: number) {
this.handler = handler
this.args = args
this.time = time
this.type = type
}
tick(deltaTime: number) {
if (this.paused) {
return
}
this.elapsedTime += deltaTime
if (this.elapsedTime >= this.time) {
this.execute()
}
}
execute() {
this.handler.apply(this, this.args)
if (this.type === Type.Timeout) {
this.active = false
} else {
this.elapsedTime -= this.time
}
}
reset() {
this.elapsedTime = 0
}
pause() {
this.paused = true
}
resume() {
this.paused = false
}
clear() {
this.active = false
}
}

73
src/clock/Schedule.ts Normal file
View File

@ -0,0 +1,73 @@
import { singleton } from '../decorators/singleton'
import Clock from './ClockTimer'
import { Delayed } from './Delayed'
/**
*
*/
@singleton
export class Schedule {
clock: Clock = new Clock()
gameClock: Map<string, Delayed> = new Map()
public start() {
this.clock.start()
}
beginSchedule(millisecond: number, handler: Function, name: string): void {
if (this.gameClock.has(name) && this.gameClock.get(name)?.active) {
console.log(`当前已存在进行中的clock: ${name}`)
this.gameClock.get(name).clear()
this.gameClock.delete(name)
}
let timeOverFun = function () {
handler && handler()
}
this.gameClock.set(name, this.clock.setTimeout(timeOverFun, millisecond, name))
}
/**
*
*/
stopSchedule(name: string): number {
console.log(`manual stop schedule: ${name}`)
if (!this.gameClock.has(name)) {
return -1
}
let clock = this.gameClock.get(name)
if (!clock.active) {
this.gameClock.delete(name)
return -1
}
let time = clock.elapsedTime
clock.clear()
this.gameClock.delete(name)
return time
}
/**
* active
* @param {string} name
* @return {boolean}
*/
scheduleActive(name: string): boolean {
return this.gameClock.has(name) && this.gameClock.get(name).active
}
/**
*
* @param {string} name
* @return {number}
*/
getLeftTime(name: string) {
if (!this.gameClock.has(name)) {
return 0
}
let clock = this.gameClock.get(name)
if (!clock.active) {
this.gameClock.delete(name)
return 0
}
return clock.time - clock.elapsedTime
}
}

107
src/common/AsyncQueue.ts Normal file
View File

@ -0,0 +1,107 @@
type Callback<T> = () => Promise<T>
export type AsyncQueue<T = void> = {
push: (task: Callback<T>) => Promise<T>
flush: () => Promise<void>
size: number
}
/**
* Ensures that each callback pushed onto the queue is executed in series.
* Such a quetie 😻
* @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple
* tasks are pushed onto the queue while there is an active task, only the
* last one will be executed, once the active task has completed.
* e.g. in the below example, only 0 and 3 will be executed.
* ```
* const queue = createAsyncQueue({ dedupeConcurrent: true })
* queue.push(async () => console.log(0)) // returns 0
* queue.push(async () => console.log(1)) // returns 3
* queue.push(async () => console.log(2)) // returns 3
* queue.push(async () => console.log(3)) // returns 3
* ```
* */
export function createAsyncQueue<T = void>(opts = { dedupeConcurrent: false }): AsyncQueue<T> {
const { dedupeConcurrent } = opts
let queue: Callback<T>[] = []
let running: Promise<void> | undefined
let nextPromise = new DeferredPromise<T>()
const push = (task: Callback<T>) => {
let taskPromise = new DeferredPromise<T>()
if (dedupeConcurrent) {
queue = []
if (nextPromise.started) nextPromise = new DeferredPromise<T>()
taskPromise = nextPromise
}
queue.push(() => {
taskPromise.started = true
task().then(taskPromise.resolve).catch(taskPromise.reject)
return taskPromise.promise
})
if (!running) running = start()
return taskPromise.promise
}
const start = async () => {
while (queue.length) {
const task = queue.shift()!
await task().catch(() => {})
}
running = undefined
}
return {
push,
flush: () => running || Promise.resolve(),
get size() {
return queue.length
},
}
}
export const createAsyncQueues = <T = void>(opts = { dedupeConcurrent: false }) => {
const queues: { [queueId: string]: AsyncQueue<T> } = {}
const push = (queueId: string, task: Callback<T>) => {
if (!queues[queueId]) queues[queueId] = createAsyncQueue<T>(opts)
return queues[queueId].push(task)
}
const flush = (queueId: string) => {
if (!queues[queueId]) queues[queueId] = createAsyncQueue<T>(opts)
return queues[queueId].flush()
}
return { push, flush }
}
class DeferredPromise<T = void, E = any> {
started = false
resolve: (x: T | PromiseLike<T>) => void = () => {}
reject: (x: E) => void = () => {}
promise: Promise<T>
constructor() {
this.promise = new Promise<T>((res, rej) => {
this.resolve = res
this.reject = rej
})
}
}
// function main() {
// const queue = createAsyncQueue()
// queue.push(async () => {
// console.log(0)
// }) // returns 0
// queue.push(async () => {
// console.log(1)
// return new Promise((resolve, reject) => {
// setTimeout(() => {
// console.log('12')
// resolve()
// }, 1000)
// })
// }) // returns 3
// queue.push(async () => console.log(2)) // returns 3
// queue.push(async () => console.log(3)) // returns 3
// console.log('hi')
// }
// main()

6
src/common/Debug.ts Normal file
View File

@ -0,0 +1,6 @@
import debug from 'debug'
debug.log = console.info.bind(console)
export const error = debug('chain:error')
error.log = console.error.bind(console)

966
src/common/Extend.ts Normal file
View File

@ -0,0 +1,966 @@
/**
* 0
* @param value 0
* @param length
* @return 0
*/
function zeroize(value: number | string, length: number = 2): string {
let str = '' + value
let zeros = ''
for (let i = 0, len = length - str.length; i < len; i++) {
zeros += '0'
}
return zeros + str
}
/****************************************扩展Object****************************************/
interface Object {
/**
*
* key value
*
* @memberOf Object
*/
clone?(): Object
/**
* to
* @param to
*/
copyto?(to: Object): void
/**
*
* @param property
*/
getPropertyDescriptor?(property: string): PropertyDescriptor
zssign?(target: any): any
}
Object.defineProperties(Object.prototype, {
clone: {
value: function () {
let o = {}
for (let n in this) {
// @ts-ignore
o[n] = this[n]
}
return o
},
writable: true,
},
getPropertyDescriptor: {
value: function (property: string): any {
let data = Object.getOwnPropertyDescriptor(this, property)
if (data) {
return data
}
let prototype = Object.getPrototypeOf(this)
if (prototype) {
return prototype.getPropertyDescriptor(property)
}
return
},
writable: true,
},
copyto: {
value: function (to: Object) {
for (let p in this) {
if (!(p in to)) {
// 本身没有这个属性
// @ts-ignore
to[p] = this[p]
} else {
let data: PropertyDescriptor = to.getPropertyDescriptor(p)
if (data) {
if (data.set || data.writable) {
// 可进行赋值
// @ts-ignore
to[p] = this[p]
}
}
}
}
},
writable: true,
},
zssign: {
value: function (target: Object) {
if (target === undefined || target === null) {
throw new TypeError('Cannot convert undefined or null to object')
}
let output = Object(target)
for (let nextKey in this) {
if (!(nextKey in output)) {
// 本身没有这个属性
output[nextKey] = this[nextKey]
}
// else {
// let data: PropertyDescriptor = output.getPropertyDescriptor(nextKey);
// if (data) {
// if (data.set || data.writable) {// 可进行赋值
// output[nextKey] = this[nextKey];
// }
// }
// }
}
return output
},
writable: true,
},
})
/****************************************扩展Math****************************************/
interface Math {
/**
*
* Math.PI / 180
* @type {number}
* @memberOf Math
*/
DEG_TO_RAD: number
/**
*
* 180 / Math.PI
*/
RAD_TO_DEG: number
/**
*
*/
PI2: number
/**
* 90°
*
* @type {number}
* @memberOf Math
*/
PI_1_2: number
/**
*
* @param value
* @param min
* @param max
*/
clamp?(value: number, min: number, max: number): number
/**
* [min,max)
*/
random2?(min: number, max: number): number
}
Math.DEG_TO_RAD = Math.PI / 180
Math.RAD_TO_DEG = 180 / Math.PI
Math.PI2 = 2 * Math.PI
Math.PI_1_2 = Math.PI * 0.5
Math.clamp = (value, min, max) => {
if (value < min) {
value = min
}
if (value > max) {
value = max
}
return value
}
Math.random2 = (min, max) => {
return min + Math.random() * (max - min)
}
/****************************************扩展Number********************************************/
interface Number {
/**
* 0
* @param length
* @return 0
*/
zeroize?(length: number): string
/**
* `min` `max`minmax
* [min,max]
*
* @param {number} min
* @param {number} max
* @returns {boolean}
*/
between?(min: number, max: number): boolean
}
Object.defineProperties(Number.prototype, {
zeroize: {
value: function (this: number, length: number) {
return zeroize(this, length)
},
writable: true,
},
between: {
value: function (this: number, min: number, max: number) {
return min <= this && max >= this
},
writable: true,
},
})
/****************************************扩展String****************************************/
interface String {
/**
* {0}{1}{2}{a} {b}obj对应key替换key的数据替换
*/
substitute?(args: any[]): string
/**
* 0
* @param length
* @return 0
*/
zeroize?(length: number): string
/**
*
* <font color="#ff0000">hash的字符串并不一定唯一</font>
*/
hash?(): number
/**
*
*/
trueLength?(): number
/**
*
* */
cnLength?(): number
/**
*
* */
versionCompare?(target: string): number
}
Object.defineProperties(String.prototype, {
zeroize: {
value: function (length: number) {
return zeroize(this, length)
},
writable: true,
},
substitute: {
value: function (this: string) {
let len = arguments.length
if (len > 0) {
let obj: IArguments
if (len == 1) {
obj = arguments[0]
if (typeof obj !== 'object') {
obj = arguments
}
} else {
obj = arguments
}
if (obj instanceof Object && !(obj instanceof RegExp)) {
return this.replace(/\{(?:%([^{}]+)%)?([^{}]+)\}/g, function (match: string, handler: string, key: string) {
//检查key中是否为%开头,如果是,则尝试按方法处理
// @ts-ignore
let value = obj[key]
if (handler) {
//如果有处理器,拆分处理器
let func = String.subHandler[handler]
if (func) {
value = func(value)
}
}
return value !== undefined ? '' + value : match
})
}
}
return this.toString() //防止生成String对象ios反射String对象会当成一个NSDictionary处理
},
writable: true,
},
hash: {
value: function () {
let len = this.length
let hash = 5381
for (let i = 0; i < len; i++) {
hash += (hash << 5) + this.charCodeAt(i)
}
return hash & 0x7fffffff
},
writable: true,
},
trueLength: {
value: function () {
let arr: string[] = this.match(/[^x00-xff]/gi)
return this.length + (arr ? arr.length : 0)
},
writable: true,
},
cnLength: {
value: function () {
// /[\u2E80-\u9FBF]
let arr: string[] = this.match(/[^x00-xff]/gi)
return arr ? arr.length : 0
},
writable: true,
},
versionCompare: {
value: function (target: string): number {
const GTR = 1 //大于
const LSS = -1 //小于
const EQU = 0 //等于
if (!target) {
return GTR
}
let v1arr = String(this)
.split('.')
.map(function (a) {
return parseInt(a)
})
let v2arr = String(target)
.split('.')
.map(function (a) {
return parseInt(a)
})
let arrLen = Math.max(v1arr.length, v2arr.length)
let result
//排除错误调用
if (this == undefined || target == undefined) {
throw new Error()
}
//检查空字符串,任何非空字符串都大于空字符串
if (this.length == 0 && target.length == 0) {
return EQU
} else if (this.length == 0) {
return LSS
} else if (target.length == 0) {
return GTR
}
//循环比较版本号
for (let i = 0; i < arrLen; i++) {
result = versionComp(v1arr[i], v2arr[i])
if (result == EQU) {
continue
} else {
break
}
}
return result
function versionComp(n1: number, n2: number) {
if (typeof n1 != 'number') {
n1 = 0
}
if (typeof n2 != 'number') {
n2 = 0
}
if (n1 > n2) {
return GTR
} else if (n1 < n2) {
return LSS
} else {
return EQU
}
}
},
writable: true,
},
})
interface StringConstructor {
/**
* 0
* @param value 0
* @param length
* @return 0
*/
zeroize?: (value: number, length: number) => string
/**
* substitute的回调函数
*
* @type {Readonly<{ [index: string]: { (input: any): string } }>}
* @memberOf StringConstructor
*/
subHandler?: Readonly<{ [index: string]: { (input: any): string } }>
}
String.zeroize = zeroize
/****************************************扩展Date****************************************/
interface Date {
/**
*
*
* @param {string} mask
* @param {boolean} [local] UTC时间显示
* @returns {string}
*/
format(mask: string, local?: boolean): string
/**
* n天
* @param {number} days
* @return {Date}
*/
addDays(days: number): Date
}
Object.defineProperties(Date.prototype, {
format: {
value: function (mask: string, local?: boolean) {
let d: Date = this
// @ts-ignore
return mask.replace(/"[^"]*"|'[^']*'|(?:d{1,2}|m{1,2}|yy(?:yy)?|([hHMs])\1?)/g, function ($0: string) {
switch ($0) {
case 'd':
return gd()
case 'dd':
return zeroize(gd())
case 'M':
return gM() + 1
case 'MM':
return zeroize(gM() + 1)
case 'yy':
return (gy() + '').substr(2)
case 'yyyy':
return gy()
case 'h':
return gH() % 12 || 12
case 'hh':
return zeroize(gH() % 12 || 12)
case 'H':
return gH()
case 'HH':
return zeroize(gH())
case 'm':
return gm()
case 'mm':
return zeroize(gm())
case 's':
return gs()
case 'ss':
return zeroize(gs())
default:
return $0.substr(1, $0.length - 2)
}
})
function gd() {
return local ? d.getDate() : d.getUTCDate()
}
function gM() {
return local ? d.getMonth() : d.getUTCMonth()
}
function gy() {
return local ? d.getFullYear() : d.getUTCFullYear()
}
function gH() {
return local ? d.getHours() : d.getUTCHours()
}
function gm() {
return local ? d.getMinutes() : d.getUTCMinutes()
}
function gs() {
return local ? d.getSeconds() : d.getUTCSeconds()
}
},
writable: true,
},
addDays: {
value: function (days: number) {
this.setDate(this.getDate() + days)
return this
},
writable: true,
},
})
/****************************************扩展Array****************************************/
const enum ArraySort {
/**
*
*/
ASC = 0,
/**
*
*/
DESC = 1,
}
interface ArrayConstructor {
// binaryInsert<T>(partArr: T[], item: T, filter: { (tester: T, ...args): boolean }, ...args);
SORT_DEFAULT: { number: 0; string: ''; boolean: false }
}
/**
* Array排序时undefined
*/
Array.SORT_DEFAULT = {
number: 0,
string: '',
boolean: false,
}
Object.freeze(Array.SORT_DEFAULT)
interface Array<T> {
/**
*
*
* @param {T} t
* @returns {number}
*
* @member Array
*/
pushOnce?(t: T): number
/**
*
*
* @param {T} t
* @returns {boolean} true
* false
*/
zremove?(t: T): boolean
/**
*
* ,
* @param {(keyof T)[]} kArr
* @param {(boolean[] | ArraySort[])} [dArr]
* @returns {this}
*
* @member Array
*/
multiSort?(kArr: (keyof T)[], dArr?: boolean[] | ArraySort[]): this
/**
*
*
* @param {string} [key]
* @param {boolean} [descend]
*
* @member Array
*/
doSort?(key?: keyof T, descend?: boolean | ArraySort): this
doSort?(descend?: boolean | ArraySort, key?: keyof T): this
/**
* to
* to的数组长度会和当前数组一致
*
* @template T
* @param {Array<T>} to
*/
cloneTo?<T>(to: Array<T>): void
/**
* to中
*
* @template T
* @param {Array<T>} to
*
* @member ArrayConstructor
*/
appendTo?<T>(to: Array<T>): void
/**
* index位置的元素, slice效率高
* @param index
*/
spliceOne?(index: number): boolean
/**
*
*/
randomSort?(): void
/**
* object
* @param obj obj | | child字段的值 | child字段的数组
* @param child
*/
contains?<T>(obj: T | T[] | {} | {}[], child?: string): boolean
/**
*
* @param arr
*/
randomInsert?<T>(arr: Array<T>): void
/**
* n个元素
* @param count
*/
randomGet?<T>(count?: number): T[]
/**
* 1
*/
randomOne?<T>(): T
/**
* n个元素
* @param count
*/
randomRemove?<T>(count?: number): T[]
/**
* n位
* @param n n > 0 , n<0 左移
*/
moveElement?<T>(n: number): T[]
/**
*
* @param arr
*/
union?<T>(arr: T[]): T[]
/**
*
* @param arr
*/
intersect?<T>(arr: T[]): T[]
/**
* arr的差集
* @param arr
*/
difference?<T>(arr: T[]): T[]
/**
* Map
* @param {string} key map的key字段名
* @return {Map<any, T>}
*/
toMap?<T>(key: string): Map<any, T>
}
Object.defineProperties(Array.prototype, {
cloneTo: {
value: function <T>(this: T[], b: any[]) {
b.length = this.length
let len = this.length
b.length = len
for (let i = 0; i < len; i++) {
b[i] = this[i]
}
},
writable: true,
},
appendTo: {
value: function <T>(this: T[], b: any[]) {
let len = this.length
for (let i = 0; i < len; i++) {
b.push(this[i])
}
},
writable: true,
},
pushOnce: {
value: function <T>(this: T[], t: T) {
let idx = this.indexOf(t)
if (!~idx) {
idx = this.length
this.push(t)
}
return idx
},
writable: true,
},
zremove: {
value: function <T>(this: T[], t: T) {
let idx = this.indexOf(t)
if (~idx) {
this.splice(idx, 1)
return true
}
return false
},
writable: true,
},
doSort: {
value: function () {
let key: string, descend: boolean
let len = arguments.length
for (let i = 0; i < len; i++) {
let arg = arguments[i]
let t = typeof arg
if (t === 'string') {
key = arg
} else {
descend = !!arg
}
}
if (key) {
return this.sort((a: any, b: any) => (descend ? b[key] - a[key] : a[key] - b[key]))
} else {
return this.sort((a: any, b: any) => (descend ? b - a : a - b))
}
},
writable: true,
},
multiSort: {
value: function (kArr: string[], dArr?: boolean[] | boolean) {
let isArr = Array.isArray(dArr)
return this.sort((a: any, b: any): number => {
const def = Array.SORT_DEFAULT
for (let idx = 0, len = kArr.length; idx < len; idx++) {
let key = kArr[idx]
// @ts-ignore
let mode = isArr ? !!dArr[idx] : !!dArr
let av = a[key]
let bv = b[key]
let typea = typeof av
let typeb = typeof bv
if (typea == 'object' || typeb == 'object') {
return 0
} else if (typea != typeb) {
if (typea == 'undefined') {
// @ts-ignore
bv = def[typeb]
} else if (typeb == 'undefined') {
// @ts-ignore
av = def[typea]
} else {
return 0
}
}
if (av < bv) {
return mode ? 1 : -1
} else if (av > bv) {
return mode ? -1 : 1
} else {
continue
}
}
return 0
})
},
writable: true,
},
spliceOne: {
value: function (index: number): boolean {
if (index === -1 || index >= this.length) {
return false
}
const len = this.length - 1
for (let i = index; i < len; i++) {
this[i] = this[i + 1]
}
this.length = len
return true
},
writable: true,
},
randomSort: {
value: function <T>() {
for (let j, x, i = this.length; i; j = (Math.random() * i) | 0, x = this[--i], this[i] = this[j], this[j] = x) {}
},
writable: true,
},
contains: {
value: function <T>(obj: T | T[] | {} | {}[], child?: string): boolean {
let result = false
if (child) {
const isArr = Array.isArray(obj)
if (isArr) {
// @ts-ignore
if (obj[0].hasOwnProperty(child)) {
let set0 = new Set()
// @ts-ignore
for (let s of obj) {
// @ts-ignore
set0.add(s[child])
}
// @ts-ignore
let set1 = new Set(this.filter(x => set0.has(x)))
return set0.size === set1.size
} else {
// @ts-ignore
let set0 = new Set(obj)
let set1 = new Set(this.filter((x: {}) => set0.has(x)))
return set1.size === set0.size
}
} else {
if (obj.hasOwnProperty(child)) {
for (let sub of this) {
if (sub.hasOwnProperty(child)) {
// @ts-ignore
if (sub[child] === obj[child]) {
result = true
break
}
}
}
} else {
for (let sub of this) {
if (sub.hasOwnProperty(child)) {
// @ts-ignore
if (sub[child] === obj) {
result = true
break
}
}
}
}
}
} else {
// 不指定 比较字段 的话, 只处理2种情况
// 1: obj 为数组
// 2: obj 不是数组
if (Array.isArray(obj)) {
let set0 = new Set(obj)
// @ts-ignore
let set1 = new Set(this.filter(x => set0.has(x)))
return set1.size === set0.size
} else {
let idx = this.indexOf(obj)
return !!~idx
}
}
return result
},
writable: true,
},
randomInsert: {
value: function <T>(arr: Array<T>) {
const length = this.length
arr.forEach(value => {
this.splice(Math.random() * length, 0, value)
})
},
writable: true,
},
randomGet: {
value: function <T>(count: number = 1): T[] {
let shuffled: T[] = this.slice(0),
i = this.length,
min = i - count,
temp,
index
if (min < 0) {
return shuffled
}
while (i-- > min) {
index = Math.floor((i + 1) * Math.random())
temp = shuffled[index]
shuffled[index] = shuffled[i]
shuffled[i] = temp
}
return shuffled.slice(min)
},
writable: true,
},
randomOne: {
value: function <T>(): T {
let results = this.randomGet(1)
if (results.length > 0) {
return results[0]
} else {
return null
}
},
writable: true,
},
randomRemove: {
value: function <T>(count: number = 1): T[] {
let result = []
while (count-- > 0 && this.length > 0) {
let index = (Math.random() * this.length) | 0
result.push(...this.splice(index, 1))
}
return result
},
writable: true,
},
moveElement: {
value: function <T>(n: number): T[] {
if (Math.abs(n) > this.length) n = n % this.length
return this.slice(-n).concat(this.slice(0, -n))
},
writable: true,
},
union: {
value: function <T>(this: T[], b: any[]): T[] {
let a = this.concat(b)
return [...new Set(a)]
},
writable: true,
},
intersect: {
value: function <T>(this: T[], b: any[]): T[] {
let set0 = new Set(b)
let set1 = new Set(this.filter(x => set0.has(x)))
return [...set1]
},
writable: true,
},
difference: {
value: function <T>(this: T[], b: any[]): T[] {
let set0 = new Set(b)
let set1 = new Set(this.filter(x => !set0.has(x)))
return [...set1]
},
writable: true,
},
toMap: {
value: function <T>(this: T[], key: string) {
let result: Map<any, T> = new Map()
for (const o of this) {
// @ts-ignore
result.set(o[key], o)
}
return result
},
writable: true,
},
})
interface Map<K, V> {
/**
* V为number的Map, , V, set
* V为其他类型时, set
* @param key
* @param value
*/
inc?(key: K, value: V): number
}
Object.defineProperties(Map.prototype, {
inc: {
value: function <K, V>(key: K, value: V) {
if (typeof value == 'number') {
this.set(key, (this.get(key) || 0) + value)
} else {
this.set(key, value)
}
return this.get(key)
},
},
})

13
src/common/ZError.ts Normal file
View File

@ -0,0 +1,13 @@
import { FastifyError } from 'fastify'
export class ZError implements FastifyError {
code: string
statusCode?: number
message: string
name: string
constructor(statusCode: number, message: string) {
this.statusCode = statusCode
this.message = message
}
}

View File

@ -0,0 +1,6 @@
import fastify = require('fastify')
class BaseController {
aotoRoute(req: fastify.FastifyRequest, res) {}
}
export default BaseController

29
src/config/config.ts Normal file
View File

@ -0,0 +1,29 @@
import * as dotenv from "dotenv";
const NODE_ENV = process.env.NODE_ENV || "development";
dotenv.config();
let path;
switch (process.env.NODE_ENV) {
case "test":
path = `${__dirname}/../../.env.development`;
break;
case "production":
path = `${__dirname}/../../.env.production`;
break;
default:
path = `${__dirname}/../../.env.development`;
}
dotenv.config({ path: path, debug: NODE_ENV === "development" });
let baseConfig = {
api: {
port: parseInt(process.env.API_PORT),
host: process.env.API_HOST,
token_secret: process.env.API_TOKEN_SECRET,
token_expiresIn: process.env.API_TOKEN_EXPIRESIN,
},
db_main: process.env.DB_MAIN,
db_second: process.env.DB_SECOND,
};
export default baseConfig;

View File

@ -0,0 +1,11 @@
import BaseController from "common/base.controller";
import { role, router } from "decorators/router";
class MainController extends BaseController {
@role("anon")
@router("post /client/login")
async clientLogin(req, res) {
const { code, token } = req.params;
return {};
}
}

18
src/decorators/dbconn.ts Normal file
View File

@ -0,0 +1,18 @@
import { mongoose } from '@typegoose/typegoose'
import config from 'config/config'
/**
* model指定数据库连接
* @param {string} name config中必须要有对应的配置 main db_main
* */
export function dbconn(name?: string) {
return target => {
name = name || 'main'
const url = config['db_' + name]
target['db'] = mongoose.createConnection(url, {
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false,
useUnifiedTopology: true,
})
}
}

29
src/decorators/nojson.ts Normal file
View File

@ -0,0 +1,29 @@
import 'reflect-metadata'
import { singleton } from './singleton'
@singleton
export class NoJsonClass {
private noJsonPropSet: Set<string> = new Set()
public addKey(className: string, propertyKey: string) {
this.noJsonPropSet.add(className + '_' + propertyKey)
}
public checkExist(className: string, propertyKey: string) {
return this.noJsonPropSet.has(className + '_' + propertyKey)
}
}
/**
* toJson方法输出的字段上加上 @noJson
* @return {{(target: Function): void, (target: Object, propertyKey: (string | symbol)): void}}
*/
export function noJson() {
// return Reflect.metadata(noJsonMetadataKey, !0)
return function (target: Object, propertyKey: string) {
new NoJsonClass().addKey(target.constructor.name, propertyKey)
}
}
export function checkJson(target: any, propertyKey: string) {
return !new NoJsonClass().checkExist(target.constructor.modelName, propertyKey)
}

142
src/decorators/router.ts Normal file
View File

@ -0,0 +1,142 @@
import BaseController from '../common/base.controller'
export class RouterData {
target?: any
method?: string
path?: string
fun?: Function
}
export class RouterMap {
static decoratedRouters: Map<
Function,
{
roles?: string[]
permissions?: string[][]
data?: RouterData[]
depts?: string[]
}
> = new Map()
}
export function router(route?: string) {
return (target: BaseController, name: string, value: PropertyDescriptor) => {
if (!route) {
const controller = target.constructor.name
const controllerName = controller.toLowerCase().replace('.controller', '')
route = 'all ' + ['', controllerName, name].join('/')
}
const split = route.split(' ')
if (split.length > 2) {
throw new Error('路由中只允许一个空格')
}
const [method, path] = split
// @ts-ignore
const key = target[name]
let routerData = new RouterData()
routerData.target = target
routerData.method = method
routerData.path = path
// @ts-ignore
routerData.fun = target[name]
if (RouterMap.decoratedRouters.has(key)) {
let objCurrent = RouterMap.decoratedRouters.get(key)
if (!objCurrent.data) {
objCurrent.data = [routerData]
} else {
objCurrent.data.push(routerData)
}
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], objCurrent)
} else {
let routerObj = {
data: [routerData],
}
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], routerObj)
}
}
}
export function role(roles?: string | string[]) {
return (target: BaseController, name: string, value: PropertyDescriptor) => {
let roleList: string[] = []
if (roles) {
if (Array.isArray(roles)) {
roleList = roles
} else {
roleList = [roles]
}
}
// @ts-ignore
const key = target[name]
let roleObj = { roles: roleList }
if (RouterMap.decoratedRouters.has(key)) {
let objCurrent = RouterMap.decoratedRouters.get(key)
Object.assign(objCurrent, roleObj)
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], objCurrent)
} else {
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], roleObj)
}
}
}
export function permission(permissions?: string | string[]) {
return (target: BaseController, name: string, value: PropertyDescriptor) => {
let permissionList: string[][] = [[]]
if (permissions) {
if (Array.isArray(permissions)) {
let arr = []
for (let sub of permissions) {
arr.push(sub.split(':'))
}
permissionList = arr
} else {
permissionList = [permissions.split(':')]
}
}
// @ts-ignore
const key = target[name]
let permissionObj = { permissions: permissionList }
if (RouterMap.decoratedRouters.has(key)) {
let objCurrent = RouterMap.decoratedRouters.get(key)
Object.assign(objCurrent, permissionObj)
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], objCurrent)
} else {
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], permissionObj)
}
}
}
/**
* dept修饰器的, id是否存在
*/
export function dept(depts?: string | string[]) {
return (target: BaseController, name: string, value: PropertyDescriptor) => {
let deptList: string[] = []
if (depts) {
if (Array.isArray(depts)) {
deptList = depts
} else {
deptList = [depts]
}
}
// @ts-ignore
const key = target[name]
let deptObj = { depts: deptList }
if (RouterMap.decoratedRouters.has(key)) {
let objCurrent = RouterMap.decoratedRouters.get(key)
Object.assign(objCurrent, deptObj)
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], objCurrent)
} else {
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], deptObj)
}
}
}

View File

@ -0,0 +1,29 @@
/**
* class
* 使:
* @singleton
* class Test {}
* new Test() === new Test() // returns `true`
* 使 decorator
* const TestSingleton = singleton(Test)
* new TestSingleton() === new TestSingleton() //returns 'true'
*/
export const SINGLETON_KEY = Symbol()
export type Singleton<T extends new (...args: any[]) => any> = T & {
[SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never
}
export const singleton = <T extends new (...args: any[]) => any>(classTarget: T) =>
new Proxy(classTarget, {
construct(target: Singleton<T>, argumentsList, newTarget) {
// Skip proxy for children
if (target.prototype !== newTarget.prototype) {
return Reflect.construct(target, argumentsList, newTarget)
}
if (!target[SINGLETON_KEY]) {
target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)
}
return target[SINGLETON_KEY]
},
})

2
src/logger/logger.ts Normal file
View File

@ -0,0 +1,2 @@
const logger = require('tracer').colorConsole({ dateformat: 'yyyy-mm-dd HH:MM:ss.L' })
export default logger

55
src/plugins/apiauth.ts Normal file
View File

@ -0,0 +1,55 @@
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
import fastifyPlugin from "fastify-plugin";
declare module "fastify" {
interface FastifyRequest {
roles?: string[];
user?: any;
token?: string;
}
interface FastifyInstance {
apiAuth: (request: FastifyRequest, reply: FastifyReply) => {};
}
}
export interface ApiAuthOptions {
secret: string;
expiresIn: string;
}
const apiAuthPlugin: FastifyPluginAsync<ApiAuthOptions> = async function (
fastify,
opts
) {
fastify.register(require("@fastify/jwt"), {
secret: opts.secret,
sign: { expiresIn: opts.expiresIn },
});
// 只有路由配置的role为anon才不需要过滤
fastify.decorate(
"apiAuth",
async function (request: FastifyRequest, reply: FastifyReply) {
if (!request.roles || request.roles.indexOf("anon") == -1) {
try {
if (!request.token) {
return reply.send({ code: 11, msg: "need login" });
}
//@ts-ignore
const data = this.jwt.verify(request.token);
if (!data || !data.id) {
return reply.send({ code: 10, msg: "need login" });
}
// let account = await Account.findById(data.id)
// if (!account) {
// return reply.send({ code: 10, msg: 'need login' })
// }
// request.user = account
} catch (err) {
return reply.send({ code: 401, msg: "need auth" });
}
}
}
);
};
export default fastifyPlugin(apiAuthPlugin, "4.x");

37
src/plugins/zReqParser.ts Normal file
View File

@ -0,0 +1,37 @@
import {
FastifyInstance,
FastifyPluginAsync,
FastifyReply,
FastifyRequest,
} from "fastify";
import fastifyPlugin from "fastify-plugin";
/**
* post get req.params
*/
declare module "fastify" {
interface FastifyInstance {
zReqParser: (request: FastifyRequest, reply: FastifyReply) => {};
}
}
const zReqParserPlugin: FastifyPluginAsync = async function (
fastify: FastifyInstance,
options?: any
) {
fastify.addHook(
"preValidation",
async (request: FastifyRequest, reply: FastifyReply) => {
let params = request.params || {};
if (request.query) {
Object.assign(params, request.query);
}
if (request.body) {
Object.assign(params, request.body);
}
request.params = params;
}
);
return;
};
export default fastifyPlugin(zReqParserPlugin, "4.x");

View File

@ -0,0 +1,73 @@
import {
FastifyInstance,
FastifyPluginAsync,
FastifyReply,
FastifyRequest,
} from "fastify";
import fastifyPlugin from "fastify-plugin";
const getTokenFromHeader = function (request) {
let token: string | undefined;
if (request.headers && request.headers.authorization) {
const parts = request.headers.authorization.split(" ");
if (parts.length === 2) {
const scheme = parts[0];
if (/^Bearer$/i.test(scheme)) {
token = parts[1];
}
}
}
return token;
};
const getTokenFromCookie = function (request) {
let token: string | undefined;
if (request.cookies) {
if (request.cookies["token"]) {
token = request.cookies["token"];
}
}
return token;
};
const getTokenFromParams = function (request) {
let token: string | undefined;
token = request.params && request.params.token;
return token;
};
const getTokenFromQuery = function (request) {
let token: string | undefined;
token = request.query && request.query.token;
return token;
};
const getTokenFromBody = function (request) {
let token: string | undefined;
token = request.body && request.body.token;
return token;
};
const zTokenParserPlugin: FastifyPluginAsync = async function (
fastify: FastifyInstance,
options?: any
) {
fastify.addHook(
"preValidation",
async (request: FastifyRequest, reply: FastifyReply) => {
request["token"] =
getTokenFromHeader(request) ||
getTokenFromCookie(request) ||
getTokenFromParams(request) ||
getTokenFromQuery(request) ||
getTokenFromBody(request);
}
);
return;
};
/**
* request的header, cookie, params, query和body中获取token, request.token中
* header中的字段key为authorization, Bearer xxxx
* key都为 token
*/
export default fastifyPlugin(zTokenParserPlugin, "4.x");

26
src/plugins/zrbac.ts Normal file
View File

@ -0,0 +1,26 @@
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
import fastifyPlugin from 'fastify-plugin'
import RBAC from 'fast-rbac'
declare module 'fastify' {
interface FastifyInstance {
/**
* RBAC interface
*/
rbac: RBAC
}
}
const zRBACPlugin: FastifyPluginAsync = async function fastifyMetrics(
fastify: FastifyInstance,
options?: RBAC.Options,
) {
const rbac = new RBAC(options)
fastify.decorate('rbac', rbac)
return
}
export = fastifyPlugin(zRBACPlugin, {
fastify: '>=3.0.0',
name: 'zrbac',
})

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"ts-node": {
"files": true
},
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"esModuleInterop": true,
"target": "es2018",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./src",
"rootDir": "./src"
},
"lib": ["es2018"],
"include": [
"src/**/*.ts",
"typings/extend.d.ts"
]
}

1976
yarn.lock Normal file

File diff suppressed because it is too large Load Diff