mirror of
https://github.com/apgzs/cool-admin-api.git
synced 2025-04-05 03:04:57 +08:00
初始化提交
This commit is contained in:
commit
9aed371442
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
logs/
|
||||
npm-debug.log
|
||||
node_modules/
|
||||
coverage/
|
||||
.idea/
|
||||
run/
|
||||
logs/
|
||||
typings/
|
||||
.DS_Store
|
||||
.vscode
|
||||
*.swp
|
||||
*.lock
|
||||
!.autod.conf.js
|
||||
|
||||
|
||||
config/**/*.js
|
||||
app/**/*.map
|
||||
test/**/*.map
|
||||
config/**/*.map
|
12
.travis.yml
Normal file
12
.travis.yml
Normal file
@ -0,0 +1,12 @@
|
||||
sudo: false
|
||||
language: node_js
|
||||
node_js:
|
||||
- '8'
|
||||
before_install:
|
||||
- npm i npminstall -g
|
||||
install:
|
||||
- npminstall
|
||||
script:
|
||||
- npm run ci
|
||||
after_script:
|
||||
- npminstall codecov && codecov
|
12
IDEStyle.xml
Normal file
12
IDEStyle.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<code_scheme name="Project" version="173">
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACKETS" value="true" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
<option name="SPACES_WITHIN_INTERPOLATION_EXPRESSIONS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SPACE_BEFORE_METHOD_PARENTHESES" value="true" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
166
README.md
Normal file
166
README.md
Normal file
@ -0,0 +1,166 @@
|
||||
## 展示
|
||||
* 演示地址:https://show.cool-admin.com
|
||||
* 文档地址:https://docs.cool-admin.com
|
||||
* 官网:https://www.cool-admin.com
|
||||
## 技术选型
|
||||
Node版后台基础框架基于[Egg.js](https://eggjs.org/zh-cn/)(阿里出品)
|
||||
* 基础:**[egg.js](https://eggjs.org/zh-cn/)**
|
||||
* 数据:**[typeorm](https://typeorm.io)**
|
||||
* 缓存:**[egg-redis](https://www.npmjs.com/package/egg-redis)**
|
||||
* 鉴权:**[egg-jwt](https://www.npmjs.com/package/egg-jwt)**
|
||||
* 网络:**[axios](https://www.npmjs.com/package/axios)**
|
||||
|
||||
## 运行
|
||||
环境 `Node.js>=8.9.0` `Redis`
|
||||
|
||||
新建并导入数据库,数据库脚本位于 `db/init.sql`,修改数据库连接信息`config/config.*.typeorm`
|
||||
|
||||
推荐使用`yarn`
|
||||
|
||||
```js
|
||||
git clone https://github.com/apgzs/cool-admin-api.git
|
||||
cd cool-admin-api
|
||||
yarn
|
||||
yarn dev
|
||||
http://localhost:7001
|
||||
```
|
||||
|
||||
或者`npm`
|
||||
|
||||
```js
|
||||
git clone https://github.com/apgzs/cool-admin-api.git
|
||||
cd cool-admin-api
|
||||
npm install
|
||||
npm run dev
|
||||
http://localhost:7001
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 数据模型
|
||||
数据模型必须放在`app/entities/*`下,否则[typeorm](https://typeorm.io "typeorm")无法识别,如:
|
||||
```js
|
||||
import { Entity, Column, Index } from 'typeorm';
|
||||
import { BaseEntity } from '../../lib/base/entity';
|
||||
/**
|
||||
* 系统角色
|
||||
*/
|
||||
@Entity({ name: 'sys_role' })
|
||||
export default class SysRole extends BaseEntity {
|
||||
// 名称
|
||||
@Index({ unique: true })
|
||||
@Column()
|
||||
name: string;
|
||||
// 角色标签
|
||||
@Index({ unique: true })
|
||||
@Column({ nullable: true })
|
||||
label: string;
|
||||
// 备注
|
||||
@Column({ nullable: true })
|
||||
remark: string;
|
||||
}
|
||||
```
|
||||
新建完成运行代码,就可以看到数据库新建了一张`sys_role`表,如不需要自动创建`config`文件夹下修改[typeorm](https://typeorm.io "typeorm")的配置文件
|
||||
|
||||
## 控制器
|
||||
有了数据表之后,如果希望通过接口对数据表进行操作,我们就必须在`controller`文件夹下新建对应的控制器,如:
|
||||
```js
|
||||
import { BaseController } from '../../../lib/base/controller';
|
||||
import { Context } from 'egg';
|
||||
import routerDecorator from '../../../lib/router';
|
||||
import { Brackets } from 'typeorm';
|
||||
/**
|
||||
* 系统-角色
|
||||
*/
|
||||
@routerDecorator.prefix('/admin/sys/role', [ 'add', 'delete', 'update', 'info', 'list', 'page' ])
|
||||
export default class SysRoleController extends BaseController {
|
||||
constructor (ctx: Context) {
|
||||
super(ctx);
|
||||
this.setEntity(this.ctx.repo.sys.Role);
|
||||
this.setPageOption({
|
||||
keyWordLikeFields: [ 'name', 'label' ],
|
||||
where: new Brackets(qb => {
|
||||
qb.where('id !=:id', { id: 1 });
|
||||
}),
|
||||
});//分页配置(可选)
|
||||
this.setService(this.service.sys.role);//设置自定义的service(可选)
|
||||
}
|
||||
}
|
||||
```
|
||||
这样我们就完成了6个接口的编写,对应的接口如下:
|
||||
* **`/admin/sys/role/add`** 新增
|
||||
* **`/admin/sys/role/delete`** 删除
|
||||
* **`/admin/sys/role/update`** 更新
|
||||
* **`/admin/sys/role/info`** 单个信息
|
||||
* **`/admin/sys/role/list`** 列表信息
|
||||
* **`/admin/sys/role/page`** 分页查询(包含模糊查询、字段全匹配等)
|
||||
|
||||
#### PageOption配置参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ------------ | ------------ | ------------ |
|
||||
| keyWordLikeFields | 数组 | 模糊查询需要匹配的字段,如`[ 'name','phone' ]` ,这样就可以模糊查询`姓名、手机`两个字段了 |
|
||||
| where | TypeORM Brackets对象 | 固定where条件设置,详见[typeorm](https://typeorm.io/#/select-query-builder "typeorm") |
|
||||
| fieldEq | 数组 | 动态条件全匹配,如需要筛选用户状态`status`,就可以设置成`['status']`,此时接口就可以接受`status`的值并且对数据有过滤效果 |
|
||||
| addOrderBy | 对象 | 排序条件可传多个,如`{ sortNum:asc, createTime:desc }` |
|
||||
|
||||
## 数据缓存
|
||||
有些业务场景,我们并不希望每次请求接口都需要操作数据库,如:今日推荐、上个月排行榜等,数据存储在`redis`,注:缓存注解只在`service`层有效
|
||||
```js
|
||||
import { BaseService } from '../../lib/base/service';
|
||||
import { Cache } from '../../lib/cache';
|
||||
/**
|
||||
* 业务-排行榜服务类
|
||||
*/
|
||||
export default class BusRankService extends BaseService {
|
||||
/**
|
||||
* 上个月榜单
|
||||
*/
|
||||
@Cache({ ttl: 1000 }) // 表示缓存
|
||||
async rankList () {
|
||||
return [ '程序猿1号', '程序猿2号', '程序猿3号' ];
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Cache配置参数
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ------------ | ------------ | ------------ |
|
||||
| resolver | 数组 | 方法参数获得,生成key用, `resolver: (args => {return args[0];}),` 这样就可以获得方法的第一个参数作为缓存`key` |
|
||||
| ttl | 数字 | 缓存过期时间,单位:`秒` |
|
||||
| url | 字符串 | 请求url包含该前缀才缓存,如`/api/*`请求时缓存,`/admin/*`请求时不缓存 |
|
||||
|
||||
## 路由
|
||||
[egg.js](https://eggjs.org/zh-cn/)原生的路由写法过于繁琐,`cool-admin`的路由支持`BaseController`还有其他原生支持具体参照[egg.js路由](https://eggjs.org/zh-cn/basics/router.html)
|
||||
|
||||
## 自定义sql查询
|
||||
除了单表的简单操作,真实的业务往往需要对数据库做一些复杂的操作。这时候我们可以在`service`自定义SQL,如
|
||||
```js
|
||||
async page (query) {
|
||||
const { keyWord, status } = query;
|
||||
const sql = `
|
||||
SELECT
|
||||
a.*,
|
||||
GROUP_CONCAT(c.name) AS roleName
|
||||
FROM
|
||||
sys_user a
|
||||
LEFT JOIN sys_user_role b ON a.id = b.userId
|
||||
LEFT JOIN sys_role c ON b.roleId = c.id
|
||||
WHERE 1 = 1
|
||||
${ this.setSql(status, 'and a.status = ?', [ status ]) }
|
||||
${ this.setSql(keyWord, 'and (a.name LIKE ? or a.username LIKE ?)', [ `%${ keyWord }%`, `%${ keyWord }%` ]) }
|
||||
${ this.setSql(true, 'and a.id != ?', [ 1 ]) }
|
||||
GROUP BY a.id`;
|
||||
return this.sqlRenderPage(sql, query);
|
||||
}
|
||||
```
|
||||
### this.setSql()设置参数
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ------------ | ------------ | ------------ |
|
||||
| condition | 布尔型 | 只有满足改条件才会拼接上相应的sql和参数 |
|
||||
| sql | 字符串 | 需要拼接的参数 |
|
||||
| params | 数组 | 相对应的参数 |
|
||||
|
20
app.ts
Normal file
20
app.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import * as moment from 'moment';
|
||||
|
||||
export default app => {
|
||||
const ctx = app.createAnonymousContext();
|
||||
app.beforeStart(async () => {
|
||||
ctx.logger.info('beforeStart');
|
||||
// 格式化时间
|
||||
Date.prototype.toJSON = function () {
|
||||
return moment(this).format('YYYY-MM-DD HH:mm:ss');
|
||||
};
|
||||
});
|
||||
|
||||
app.ready(async () => {
|
||||
ctx.logger.info('=====service start succeed=====');
|
||||
});
|
||||
|
||||
app.beforeClose(async () => {
|
||||
ctx.logger.info('beforeClose');
|
||||
});
|
||||
};
|
37
app/extend/application.ts
Normal file
37
app/extend/application.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 框架扩展
|
||||
*/
|
||||
export default class Application {
|
||||
/**
|
||||
* redis保存值
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param expire 过期时间 单位:秒
|
||||
*/
|
||||
public static async redisSet (key, value, expire?: any) {
|
||||
// @ts-ignore
|
||||
const redis = this.redis;
|
||||
await redis.set(key, value);
|
||||
if (expire) {
|
||||
await redis.expire(key, expire);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* redis获得值
|
||||
* @param key 键
|
||||
*/
|
||||
public static async redisGet (key) {
|
||||
// @ts-ignore
|
||||
return this.redis.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* redis 删除key
|
||||
* @param key
|
||||
*/
|
||||
public static async redisDel (key) {
|
||||
// @ts-ignore
|
||||
return this.redis.del(key);
|
||||
}
|
||||
}
|
46
app/extend/helper.ts
Normal file
46
app/extend/helper.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import * as ipdb from 'ipip-ipdb';
|
||||
|
||||
/**
|
||||
* 帮助类
|
||||
*/
|
||||
export default class Helper {
|
||||
/**
|
||||
* 获得请求IP
|
||||
*/
|
||||
public static async getReqIP () {
|
||||
// @ts-ignore
|
||||
const req = this.ctx.req;
|
||||
return req.headers['x-forwarded-for'] || // 判断是否有反向代理 IP
|
||||
req.connection.remoteAddress || // 判断 connection 的远程 IP
|
||||
req.socket.remoteAddress || // 判断后端的 socket 的 IP
|
||||
req.connection.socket.remoteAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据IP获得请求地址
|
||||
* @param ip 为空时则为当前请求的IP地址
|
||||
*/
|
||||
public static async getIpAddr (ip?: string) {
|
||||
if (!ip) {
|
||||
ip = await this.getReqIP();
|
||||
}
|
||||
const bst = new ipdb.BaseStation('app/resource/ipip/ipipfree.ipdb');
|
||||
const result = bst.findInfo(ip, 'CN');
|
||||
let addr = '';
|
||||
if (result) {
|
||||
if (result.regionName === result.cityName) {
|
||||
addr = result.countryName + result.regionName;
|
||||
} else {
|
||||
addr = result.countryName + result.regionName + result.cityName;
|
||||
}
|
||||
}
|
||||
if (addr.indexOf('本机') !== -1) {
|
||||
addr = '本机地址';
|
||||
return addr;
|
||||
}
|
||||
if (addr.indexOf('局域网') !== -1) {
|
||||
addr = '局域网';
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
}
|
147
app/lib/base/controller.ts
Normal file
147
app/lib/base/controller.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import { Controller, Context } from 'egg';
|
||||
import routerDecorator from '../router';
|
||||
import { Brackets } from 'typeorm';
|
||||
|
||||
// 返回参数配置
|
||||
interface ResOp {
|
||||
// 返回数据
|
||||
data?: any;
|
||||
// 是否成功
|
||||
isFail?: boolean;
|
||||
// 返回码
|
||||
code?: number;
|
||||
// 返回消息
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// 分页参数配置
|
||||
interface PageOp {
|
||||
// 模糊查询字段
|
||||
keyWordLikeFields?: string[];
|
||||
// where
|
||||
where?: Brackets;
|
||||
// 全匹配 "=" 字段
|
||||
fieldEq?: string[];
|
||||
// 排序
|
||||
addOrderBy?: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制器基类
|
||||
*/
|
||||
export abstract class BaseController extends Controller {
|
||||
protected entity;
|
||||
protected OpService;
|
||||
protected pageOption: PageOp;
|
||||
|
||||
protected constructor (ctx: Context) {
|
||||
super(ctx);
|
||||
this.OpService = this.service.comm.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务
|
||||
* @param service
|
||||
*/
|
||||
protected setService (service) {
|
||||
this.OpService = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置分页查询
|
||||
* @param option
|
||||
*/
|
||||
protected setPageOption (option: PageOp) {
|
||||
this.pageOption = option;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置操作实体
|
||||
* @param entity
|
||||
*/
|
||||
protected setEntity (entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询数据
|
||||
*/
|
||||
@routerDecorator.get('/page')
|
||||
protected async page () {
|
||||
const result = await this.OpService.page(this.ctx.query, this.pageOption, this.entity);
|
||||
this.res({ data: result });
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
*/
|
||||
@routerDecorator.get('/list')
|
||||
protected async list () {
|
||||
const result = await this.OpService.list(this.entity);
|
||||
this.res({ data: result });
|
||||
}
|
||||
|
||||
/**
|
||||
* 信息
|
||||
*/
|
||||
@routerDecorator.get('/info')
|
||||
protected async info () {
|
||||
const result = await this.OpService.info(this.ctx.query.id, this.entity);
|
||||
this.res({ data: result });
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
@routerDecorator.post('/add')
|
||||
protected async add () {
|
||||
await this.OpService.addOrUpdate(this.ctx.request.body, this.entity);
|
||||
this.res();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改
|
||||
*/
|
||||
@routerDecorator.post('/update')
|
||||
protected async update () {
|
||||
await this.OpService.addOrUpdate(this.ctx.request.body, this.entity);
|
||||
this.res();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
@routerDecorator.post('/delete')
|
||||
protected async delete () {
|
||||
await this.OpService.delete(this.ctx.request.body.ids, this.entity);
|
||||
this.res();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回数据
|
||||
* @param op 返回配置,返回失败需要单独配置
|
||||
*/
|
||||
protected res (op?: ResOp) {
|
||||
if (!op) {
|
||||
this.ctx.body = {
|
||||
code: 1000,
|
||||
message: 'success',
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (op.isFail) {
|
||||
this.ctx.body = {
|
||||
code: op.code ? op.code : 1001,
|
||||
data: op.data,
|
||||
message: op.message ? op.message : 'fail',
|
||||
};
|
||||
} else {
|
||||
this.ctx.body = {
|
||||
code: op.code ? op.code : 1000,
|
||||
message: op.message ? op.message : 'success',
|
||||
data: op.data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
14
app/lib/base/entity.ts
Normal file
14
app/lib/base/entity.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
|
||||
// 实体类基类
|
||||
export abstract class BaseEntity {
|
||||
// ID
|
||||
@PrimaryGeneratedColumn({ type: 'bigint' })
|
||||
id: number;
|
||||
// 创建时间
|
||||
@Index()
|
||||
@CreateDateColumn()
|
||||
createTime: Date;
|
||||
// 更新时间
|
||||
@UpdateDateColumn()
|
||||
updateTime: Date;
|
||||
}
|
251
app/lib/base/service.ts
Normal file
251
app/lib/base/service.ts
Normal file
@ -0,0 +1,251 @@
|
||||
import { Service, Context } from 'egg';
|
||||
import { getManager, getConnection, Brackets } from 'typeorm';
|
||||
import * as _ from 'lodash';
|
||||
// 基础配置
|
||||
const conf = {
|
||||
size: 15,
|
||||
errTips: {
|
||||
noEntity: '未设置操作实体~',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 服务基类
|
||||
*/
|
||||
export abstract class BaseService extends Service {
|
||||
public sqlParams;
|
||||
|
||||
public constructor (ctx: Context) {
|
||||
super(ctx);
|
||||
this.sqlParams = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SQL并获得分页数据
|
||||
* @param sql 执行的sql语句
|
||||
* @param query 分页查询条件
|
||||
*/
|
||||
public async sqlRenderPage (sql, query) {
|
||||
const { size = conf.size, page = 1, order = 'createTime', sort = 'desc' } = query;
|
||||
if (order && sort) {
|
||||
if (!await this.paramSafetyCheck(order + sort)) {
|
||||
throw new Error('非法传参~');
|
||||
}
|
||||
sql += ` ORDER BY ${ order } ${ sort }`;
|
||||
}
|
||||
this.sqlParams.push((page - 1) * size);
|
||||
this.sqlParams.push(parseInt(size));
|
||||
sql += ' LIMIT ?,? ';
|
||||
let params = [];
|
||||
params = params.concat(this.sqlParams);
|
||||
const result = await this.nativeQuery(sql, params);
|
||||
const countResult = await this.nativeQuery(this.getCountSql(sql), params);
|
||||
return {
|
||||
list: result,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
size: parseInt(size),
|
||||
total: parseInt(countResult[0] ? countResult[0].count : 0),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 原生查询
|
||||
* @param sql
|
||||
* @param params
|
||||
*/
|
||||
public async nativeQuery (sql, params?) {
|
||||
if (_.isEmpty(params)) {
|
||||
params = this.sqlParams;
|
||||
}
|
||||
let newParams = [];
|
||||
newParams = newParams.concat(params);
|
||||
this.sqlParams = [];
|
||||
return await this.getOrmManager().query(sql, newParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数安全性检查
|
||||
* @param params
|
||||
*/
|
||||
public async paramSafetyCheck (params) {
|
||||
const lp = params.toLowerCase();
|
||||
return !(lp.indexOf('update') > -1 || lp.indexOf('select') > -1 || lp.indexOf('delete') > -1 || lp.indexOf('insert') > -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得查询个数的SQL
|
||||
* @param sql
|
||||
*/
|
||||
public getCountSql (sql) {
|
||||
sql = sql.toLowerCase();
|
||||
return `select count(*) as count from (${ sql.split('limit')[0] }) a`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单表分页查询
|
||||
* @param entity
|
||||
* @param query
|
||||
* @param option
|
||||
*/
|
||||
public async page (query, option, entity) {
|
||||
if (!entity) throw new Error(conf.errTips.noEntity);
|
||||
const find = await this.getPageFind(query, option, entity);
|
||||
return this.renderPage(await find.getManyAndCount(), query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有数据
|
||||
* @param entity
|
||||
*/
|
||||
public async list (entity) {
|
||||
if (!entity) throw new Error(conf.errTips.noEntity);
|
||||
return await entity.find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增/修改
|
||||
* @param entity 实体
|
||||
* @param param 数据
|
||||
*/
|
||||
public async addOrUpdate (param, entity) {
|
||||
if (!entity) throw new Error(conf.errTips.noEntity);
|
||||
if (param.id) {
|
||||
await entity.update(param.id, param);
|
||||
} else {
|
||||
await entity.save(param);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获得信息
|
||||
* @param entity 实体
|
||||
* @param id id
|
||||
*/
|
||||
public async info (id, entity) {
|
||||
if (!entity) throw new Error(conf.errTips.noEntity);
|
||||
return await entity.findOne({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param entity
|
||||
* @param ids
|
||||
*/
|
||||
public async delete (ids, entity) {
|
||||
if (!entity) throw new Error(conf.errTips.noEntity);
|
||||
if (ids instanceof Array) {
|
||||
await entity.delete(ids);
|
||||
} else {
|
||||
await entity.delete(ids.split(','));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* query
|
||||
* @param data
|
||||
* @param query
|
||||
*/
|
||||
public renderPage (data, query) {
|
||||
const { size = conf.size, page = 1 } = query;
|
||||
return {
|
||||
list: data[0],
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
size: parseInt(size),
|
||||
total: data[1],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造分页查询条件
|
||||
* @param entity 实体
|
||||
* @param query 查询条件
|
||||
* @param option 配置信息
|
||||
*/
|
||||
public getPageFind (query, option, entity) {
|
||||
let { size = conf.size, page = 1, order = 'createTime', sort = 'desc', keyWord = '' } = query;
|
||||
const find = entity
|
||||
.createQueryBuilder()
|
||||
.take(parseInt(size))
|
||||
.skip(String((page - 1) * size))
|
||||
.where(option.where);
|
||||
// 附加排序
|
||||
if (!_.isEmpty(option.addOrderBy)) {
|
||||
for (const key in option.addOrderBy) {
|
||||
find.addOrderBy(key, option.addOrderBy[key].toUpperCase());
|
||||
}
|
||||
}
|
||||
// 接口请求的排序
|
||||
if (sort && order) {
|
||||
find.addOrderBy(order, sort.toUpperCase());
|
||||
}
|
||||
// 关键字模糊搜索
|
||||
if (keyWord) {
|
||||
keyWord = `%${ keyWord }%`;
|
||||
find.andWhere(new Brackets(qb => {
|
||||
const keyWordLikeFields = option.keyWordLikeFields;
|
||||
for (let i = 0; i < option.keyWordLikeFields.length; i++) {
|
||||
qb.orWhere(`${ keyWordLikeFields[i] } like :keyWord`, { keyWord });
|
||||
}
|
||||
}));
|
||||
}
|
||||
// 字段全匹配
|
||||
if (!_.isEmpty(option.fieldEq)) {
|
||||
for (const key of option.fieldEq) {
|
||||
const c = {};
|
||||
if (query[key]) {
|
||||
c[key] = query[key];
|
||||
find.andWhere(`${ key } = :${ key }`, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
return find;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置sql
|
||||
* @param condition 条件是否成立
|
||||
* @param sql sql语句
|
||||
* @param params 参数
|
||||
*/
|
||||
protected setSql (condition, sql, params?: any[]) {
|
||||
let rSql = false;
|
||||
if (condition || (condition === 0 && condition !== '')) {
|
||||
rSql = true;
|
||||
this.sqlParams = this.sqlParams.concat(params);
|
||||
}
|
||||
return rSql ? sql : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得上下文
|
||||
*/
|
||||
public getContext () {
|
||||
return this.ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得ORM操作对象
|
||||
*/
|
||||
public getRepo () {
|
||||
return this.ctx.repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得ORM管理
|
||||
*/
|
||||
public getOrmManager () {
|
||||
return getManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得ORM连接类
|
||||
*/
|
||||
public getOrmConnection () {
|
||||
return getConnection();
|
||||
}
|
||||
|
||||
}
|
8
app/lib/cache/index.d.ts
vendored
Normal file
8
app/lib/cache/index.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
export interface Config {
|
||||
resolver?: (...args: any[]) => string | number;
|
||||
ttl?: number; // 缓存过期时间
|
||||
url?: string; // url包含改前缀才缓存 如api请求时缓存 admin请求时不缓存
|
||||
}
|
||||
export declare function Cache(config?: Config): (target: object, propertyName: string, propertyDesciptor: PropertyDescriptor) => PropertyDescriptor;
|
||||
export declare function ClearCache();
|
67
app/lib/cache/index.js
vendored
Normal file
67
app/lib/cache/index.js
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
"use strict";
|
||||
|
||||
const DEFAULT_TTL = 600;
|
||||
const REDIS_PRE = 'cache';
|
||||
const _ = require('lodash');
|
||||
const {Repository} = require('typeorm');
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
function Cache(config) {
|
||||
if (config === void 0) {
|
||||
config = {};
|
||||
}
|
||||
return function (target, name, propertyDesciptor) {
|
||||
let prop = propertyDesciptor.value ? "value" : "get";
|
||||
let originalFunction = propertyDesciptor[prop];
|
||||
propertyDesciptor[prop] = async function () {
|
||||
let args = [];
|
||||
for (let _i = 0; _i < arguments.length; _i++) {
|
||||
if (!(arguments[_i] instanceof Repository)){
|
||||
args[_i] = arguments[_i];
|
||||
}
|
||||
}
|
||||
const url = this.ctx.url;
|
||||
if (config.url && _.startsWith(url, config.url) === false) {
|
||||
return await originalFunction.apply(this, args)
|
||||
}
|
||||
let key = REDIS_PRE + ':' + target.pathName + `.${name}` + (config.resolver ?
|
||||
config.resolver.apply(this, args) :
|
||||
JSON.stringify(args).split(':').join('='));
|
||||
const cacheValue = await this.app.redisGet(key);
|
||||
if (!_.isEmpty(cacheValue)) {
|
||||
return JSON.parse(cacheValue).data
|
||||
} else {
|
||||
let result = await originalFunction.apply(this, args);
|
||||
let data = {
|
||||
data: result
|
||||
};
|
||||
this.ctx.app.redisSet(key, JSON.stringify(data), config.ttl ? config.ttl : DEFAULT_TTL);
|
||||
return result
|
||||
}
|
||||
};
|
||||
return propertyDesciptor
|
||||
};
|
||||
}
|
||||
|
||||
exports.Cache = Cache;
|
||||
|
||||
function ClearCache() {
|
||||
return function (target, name, propertyDesciptor) {
|
||||
let prop = propertyDesciptor.value ? "value" : "get";
|
||||
propertyDesciptor[prop] = async function () {
|
||||
const key = REDIS_PRE + ':' + target.pathName + '*';
|
||||
const keys = await this.ctx.app.redis.keys(key);
|
||||
if (!_.isEmpty(keys)) {
|
||||
keys.forEach(key => {
|
||||
this.ctx.app.redisDel(key)
|
||||
});
|
||||
}
|
||||
};
|
||||
return propertyDesciptor
|
||||
};
|
||||
}
|
||||
|
||||
exports.ClearCache = ClearCache;
|
66
app/lib/router/index.d.ts
vendored
Normal file
66
app/lib/router/index.d.ts
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
import { Application } from 'egg';
|
||||
import { Middleware } from 'koa';
|
||||
|
||||
/** http装饰器方法类型 */
|
||||
declare type HttpFunction = (url: string, ...beforeMiddlewares: Middleware[]) => any;
|
||||
|
||||
declare class RouterDecorator {
|
||||
get: HttpFunction;
|
||||
post: HttpFunction;
|
||||
patch: HttpFunction;
|
||||
del: HttpFunction;
|
||||
options: HttpFunction;
|
||||
put: HttpFunction;
|
||||
/**
|
||||
* 记录各个class的prefix以及相关中间件
|
||||
* 最后统一设置
|
||||
* @private
|
||||
* @static
|
||||
* @type {ClassPrefix}
|
||||
* @memberof RouterDecorator
|
||||
*/
|
||||
private static __classPrefix__;
|
||||
/**
|
||||
* 记录各个routerUrl的路由配置
|
||||
* 最后统一设置
|
||||
* @private
|
||||
* @static
|
||||
* @type {Router}
|
||||
* @memberof RouterDecorator
|
||||
*/
|
||||
private static __router__;
|
||||
|
||||
constructor ();
|
||||
|
||||
/** 推入路由配置 */
|
||||
private __setRouter__;
|
||||
|
||||
/**
|
||||
* 装饰Controller class的工厂函数
|
||||
* 为一整个controller添加prefix
|
||||
* 可以追加中间件
|
||||
* @param {string} prefixUrl
|
||||
* @param {...Middleware[]} beforeMiddlewares
|
||||
* @param {[]} baseFn 配置通用的接口 可选 page、add、update、delete、info、list
|
||||
* @returns 装饰器函数
|
||||
* @memberof RouterDecorator
|
||||
*/
|
||||
prefix (prefixUrl: string, baseFn?: any[], ...beforeMiddlewares: Middleware[]): (targetControllerClass: any) => any;
|
||||
|
||||
/**
|
||||
* 注册路由
|
||||
* 路由信息是通过装饰器收集的
|
||||
* @export
|
||||
* @param {Application} app eggApp实例
|
||||
* @param {string} [options={ prefix: '' }] 举例: { prefix: '/api' }
|
||||
*/
|
||||
static initRouter (app: Application, options?: {
|
||||
prefix: string;
|
||||
}): void;
|
||||
}
|
||||
|
||||
/** 暴露注册路由方法 */
|
||||
export declare const initRouter: typeof RouterDecorator.initRouter;
|
||||
declare const _default: RouterDecorator;
|
||||
/** 暴露实例的prefix和http的各个方法 */
|
||||
export default _default;
|
135
app/lib/router/index.js
Normal file
135
app/lib/router/index.js
Normal file
@ -0,0 +1,135 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", {value: true});
|
||||
const _ = require('lodash');
|
||||
const tslib_1 = require("tslib");
|
||||
/** http方法名 */
|
||||
const HTTP_METHODS = ['get', 'post', 'patch', 'del', 'options', 'put'];
|
||||
|
||||
let baseControllerArr = [];
|
||||
|
||||
class RouterDecorator {
|
||||
constructor() {
|
||||
HTTP_METHODS.forEach(httpMethod => {
|
||||
this[httpMethod] = (url, ...beforeMiddlewares) => (target, name) => {
|
||||
const routerOption = {
|
||||
httpMethod,
|
||||
beforeMiddlewares,
|
||||
handlerName: name,
|
||||
constructorFn: target.constructor,
|
||||
className: target.constructor.name,
|
||||
url: url
|
||||
};
|
||||
if (target.constructor.name === 'BaseController') {
|
||||
baseControllerArr.push(routerOption)
|
||||
} else {
|
||||
this.__setRouter__(url, routerOption);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 推入路由配置 */
|
||||
__setRouter__(url, routerOption) {
|
||||
RouterDecorator.__router__[url] = RouterDecorator.__router__[url] || [];
|
||||
RouterDecorator.__router__[url].push(routerOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* 装饰Controller class的工厂函数
|
||||
* 为一整个controller添加prefix
|
||||
* 可以追加中间件
|
||||
* @param {string} prefixUrl
|
||||
* @param {...Middleware[]} beforeMiddlewares
|
||||
* @param {any[]} baseFn
|
||||
* @returns 装饰器函数
|
||||
* @memberof RouterDecorator
|
||||
*/
|
||||
prefix(prefixUrl, baseFn = [], ...beforeMiddlewares) {
|
||||
return function (targetControllerClass) {
|
||||
RouterDecorator.__classPrefix__[targetControllerClass.name] = {
|
||||
prefix: prefixUrl,
|
||||
beforeMiddlewares: beforeMiddlewares,
|
||||
baseFn: baseFn,
|
||||
target: targetControllerClass
|
||||
};
|
||||
return targetControllerClass;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册路由
|
||||
* 路由信息是通过装饰器收集的
|
||||
* @export
|
||||
* @param {Application} app eggApp实例
|
||||
* @param {string} [options={ prefix: '' }] 举例: { prefix: '/api' }
|
||||
*/
|
||||
static initRouter(app, options = {prefix: ''}) {
|
||||
let addUrl = [];
|
||||
Object.keys(RouterDecorator.__router__).forEach(url => {
|
||||
RouterDecorator.__router__[url].forEach((opt) => {
|
||||
const controllerPrefixData = RouterDecorator.__classPrefix__[opt.className] || {
|
||||
prefix: '',
|
||||
beforeMiddlewares: [],
|
||||
baseFn: [],
|
||||
target: {}
|
||||
};
|
||||
let fullUrl = `${options.prefix}${controllerPrefixData.prefix}${url}`;
|
||||
console.log(`>>>>>>>>custom register URL * ${opt.httpMethod.toUpperCase()} ${fullUrl} * ${opt.className}.${opt.handlerName}`);
|
||||
if (!addUrl.includes(fullUrl)) {
|
||||
app.router[opt.httpMethod](fullUrl, ...controllerPrefixData.beforeMiddlewares, ...opt.beforeMiddlewares, (ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
||||
const ist = new opt.constructorFn(ctx);
|
||||
yield ist[opt.handlerName](ctx);
|
||||
}));
|
||||
addUrl.push(fullUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
// 通用方法
|
||||
const cArr = [].concat(_.uniq(baseControllerArr));
|
||||
Object.keys(RouterDecorator.__classPrefix__).forEach(cl => {
|
||||
const controllerPrefixData = RouterDecorator.__classPrefix__[cl] || {
|
||||
prefix: '',
|
||||
beforeMiddlewares: [],
|
||||
baseFn: [],
|
||||
target: {}
|
||||
};
|
||||
const setCArr = cArr.filter(c => {
|
||||
if (RouterDecorator.__classPrefix__[cl].baseFn.includes(c.url.replace('/', ''))) {
|
||||
return c;
|
||||
}
|
||||
});
|
||||
setCArr.forEach(cf => {
|
||||
let fullUrl = `${options.prefix}${controllerPrefixData.prefix}${cf.url}`;
|
||||
console.log(`>>>>>>>>comm register URL * ${cf.httpMethod.toUpperCase()} ${fullUrl} * ${cl}.${cf.handlerName}`);
|
||||
app.router[cf.httpMethod](fullUrl, ...controllerPrefixData.beforeMiddlewares, ...cf.beforeMiddlewares, (ctx) => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
||||
const ist = new controllerPrefixData.target(ctx);
|
||||
yield ist[cf.handlerName](ctx);
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录各个class的prefix以及相关中间件
|
||||
* 最后统一设置
|
||||
* @private
|
||||
* @static
|
||||
* @type {ClassPrefix}
|
||||
* @memberof RouterDecorator
|
||||
*/
|
||||
RouterDecorator.__classPrefix__ = {};
|
||||
/**
|
||||
* 记录各个routerUrl的路由配置
|
||||
* 最后统一设置
|
||||
* @private
|
||||
* @static
|
||||
* @type {Router}
|
||||
* @memberof RouterDecorator
|
||||
*/
|
||||
RouterDecorator.__router__ = {};
|
||||
/** 暴露注册路由方法 */
|
||||
exports.initRouter = RouterDecorator.initRouter;
|
||||
/** 暴露实例的prefix和http的各个方法 */
|
||||
exports.default = new RouterDecorator();
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBb0NBLGNBQWM7QUFDZCxNQUFNLFlBQVksR0FBRyxDQUFDLEtBQUssRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxTQUFTLEVBQUUsS0FBSyxDQUFDLENBQUM7QUFLdkUsTUFBTSxlQUFlO0lBNkJqQjtRQUNJLFlBQVksQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLEVBQUU7WUFDOUIsSUFBSSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsR0FBVyxFQUFFLEdBQUcsaUJBQStCLEVBQUUsRUFBRSxDQUFDLENBQUMsTUFBVyxFQUFFLElBQVksRUFBRSxFQUFFO2dCQUNsRyxJQUFJLENBQUMsYUFBYSxDQUFDLEdBQUcsRUFBRTtvQkFDcEIsVUFBVTtvQkFDVixpQkFBaUI7b0JBQ2pCLFdBQVcsRUFBRSxJQUFJO29CQUNqQixhQUFhLEVBQUUsTUFBTSxDQUFDLFdBQVc7b0JBQ2pDLFNBQVMsRUFBRSxNQUFNLENBQUMsV0FBVyxDQUFDLElBQUk7aUJBQ3JDLENBQUMsQ0FBQztZQUNQLENBQUMsQ0FBQTtRQUNMLENBQUMsQ0FBQyxDQUFBO0lBQ04sQ0FBQztJQUVELGFBQWE7SUFDTCxhQUFhLENBQUUsR0FBVyxFQUFFLFlBQTBCO1FBQzFELGVBQWUsQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLEdBQUcsZUFBZSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDeEUsZUFBZSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDdkQsQ0FBQztJQUVEOzs7Ozs7OztPQVFHO0lBQ0ksTUFBTSxDQUFFLFNBQWlCLEVBQUUsR0FBRyxpQkFBK0I7UUFDaEUsT0FBTyxVQUFVLHFCQUFxQjtZQUNsQyxlQUFlLENBQUMsZUFBZSxDQUFDLHFCQUFxQixDQUFDLElBQUksQ0FBQyxHQUFHO2dCQUMxRCxNQUFNLEVBQUUsU0FBUztnQkFDakIsaUJBQWlCLEVBQUUsaUJBQWlCO2FBQ3ZDLENBQUM7WUFDRixPQUFPLHFCQUFxQixDQUFDO1FBQ2pDLENBQUMsQ0FBQTtJQUNMLENBQUM7SUFFRDs7Ozs7O09BTUc7SUFDSSxNQUFNLENBQUMsVUFBVSxDQUFFLEdBQWdCLEVBQUUsT0FBTyxHQUFHLEVBQUUsTUFBTSxFQUFFLEVBQUUsRUFBRTtRQUNoRSxNQUFNLENBQUMsSUFBSSxDQUFDLGVBQWUsQ0FBQyxVQUFVLENBQUMsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLEVBQUU7WUFDbEQsZUFBZSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxHQUFpQixFQUFFLEVBQUU7Z0JBQzFELE1BQU0sb0JBQW9CLEdBQUcsZUFBZSxDQUFDLGVBQWUsQ0FBQyxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxNQUFNLEVBQUUsRUFBRSxFQUFFLGlCQUFpQixFQUFFLEVBQUUsRUFBRSxDQUFDO2dCQUNySCxNQUFNLE9BQU8sR0FBRyxHQUFHLE9BQU8sQ0FBQyxNQUFNLEdBQUcsb0JBQW9CLENBQUMsTUFBTSxHQUFHLEdBQUcsRUFBRSxDQUFDO2dCQUN4RSxPQUFPLENBQUMsR0FBRyxDQUFDLHVDQUF1QyxHQUFHLENBQUMsVUFBVSxDQUFDLFdBQVcsRUFBRSxJQUFJLE9BQU8sTUFBTSxHQUFHLENBQUMsU0FBUyxJQUFJLEdBQUcsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO2dCQUNwSSxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUMsQ0FBQyxPQUFPLEVBQUUsR0FBRyxvQkFBb0IsQ0FBQyxpQkFBaUIsRUFBRSxHQUFHLEdBQUcsQ0FBQyxpQkFBaUIsRUFBRSxDQUFPLEdBQUcsRUFBRSxFQUFFO29CQUNuSCxNQUFNLEdBQUcsR0FBRyxJQUFJLEdBQUcsQ0FBQyxhQUFhLENBQUMsR0FBRyxDQUFDLENBQUM7b0JBQ3ZDLE1BQU0sR0FBRyxDQUFDLEdBQUcsQ0FBQyxXQUFXLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQztnQkFDcEMsQ0FBQyxDQUFBLENBQUMsQ0FBQztZQUNQLENBQUMsQ0FBQyxDQUFBO1FBQ04sQ0FBQyxDQUFDLENBQUM7SUFDUCxDQUFDOztBQTlFRDs7Ozs7OztHQU9HO0FBQ1ksK0JBQWUsR0FBZ0IsRUFBRSxDQUFBO0FBRWhEOzs7Ozs7O0dBT0c7QUFDWSwwQkFBVSxHQUFXLEVBQUUsQ0FBQTtBQStEMUMsZUFBZTtBQUNGLFFBQUEsVUFBVSxHQUFHLGVBQWUsQ0FBQyxVQUFVLENBQUM7QUFFckQsNEJBQTRCO0FBQzVCLGtCQUFlLElBQUksZUFBZSxFQUFFLENBQUMifQ==
|
21
app/middleware/exception.ts
Normal file
21
app/middleware/exception.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Context } from 'egg';
|
||||
import * as moment from 'moment';
|
||||
|
||||
/**
|
||||
* 统一异常处理
|
||||
* @constructor
|
||||
*/
|
||||
export default function Exception (): any {
|
||||
return async (ctx: Context, next: () => Promise<any>) => {
|
||||
try {
|
||||
await next();
|
||||
} catch (err) {
|
||||
const { message, errors } = err;
|
||||
ctx.logger.error(`>>>${ moment().format('YYYY-MM-DD HH:mm:ss') }:`, message, errors);
|
||||
ctx.body = {
|
||||
code: 1001,
|
||||
message,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
BIN
app/resource/ipip/ipipfree.ipdb
Normal file
BIN
app/resource/ipip/ipipfree.ipdb
Normal file
Binary file not shown.
6
app/router.ts
Normal file
6
app/router.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Application } from 'egg';
|
||||
import { initRouter } from '../app/lib/router';
|
||||
|
||||
export default (app: Application) => {
|
||||
initRouter(app);
|
||||
};
|
7
app/service/comm/data.ts
Normal file
7
app/service/comm/data.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { BaseService } from '../../lib/base/service';
|
||||
|
||||
/**
|
||||
* 基础数据操作服务类
|
||||
*/
|
||||
export default class Data extends BaseService {
|
||||
}
|
48
app/service/comm/file.ts
Normal file
48
app/service/comm/file.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { BaseService } from '../../lib/base/service';
|
||||
import * as moment from 'moment';
|
||||
import * as uuid from 'uuid/v1';
|
||||
import axios from 'axios';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
/**
|
||||
* 文件
|
||||
*/
|
||||
export default class File extends BaseService {
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
public async upload () {
|
||||
const ctx = this.ctx;
|
||||
if (_.isEmpty(ctx.request.files)) {
|
||||
throw new Error('上传文件为空');
|
||||
}
|
||||
const file = ctx.request.files[0];
|
||||
try {
|
||||
const extend = file.filename.split('.');
|
||||
const name = moment().format('YYYYMMDD') + '/' + uuid() + '.' + extend[extend.length - 1];
|
||||
const result = await ctx.oss.put(name, file.filepath);
|
||||
if (result.url && result.url.indexOf('http://') !== -1) {
|
||||
result.url = result.url.replace('http', 'https');
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
throw new Error('上传文件失败:' + err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载图片链接并上传到oss
|
||||
*/
|
||||
public async uploadWithPic (url) {
|
||||
try {
|
||||
const ctx = this.ctx;
|
||||
const data = await axios.get(url, { responseType: 'arraybuffer' }).then(res => {
|
||||
return res.data;
|
||||
});
|
||||
const name = moment().format('YYYYMMDD') + '/' + uuid() + '.png';
|
||||
return await ctx.oss.put(name, data);
|
||||
} catch (err) {
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
}
|
46
app/service/comm/verify.ts
Normal file
46
app/service/comm/verify.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { BaseService } from '../../lib/base/service';
|
||||
|
||||
import * as uuid from 'uuid/v1';
|
||||
import * as svgCaptcha from 'svg-captcha';
|
||||
import * as svgToDataURL from 'svg-to-dataurl';
|
||||
|
||||
/**
|
||||
* 验证 Service
|
||||
*/
|
||||
export default class Verify extends BaseService {
|
||||
|
||||
/**
|
||||
* 获得svg验证码 验证方式都换为小写比对 有效时间为30分钟
|
||||
* @param params type验证码类型,如需在<img src=""/>,src下直接赋值,可以传type值为 dataUrl
|
||||
*/
|
||||
public async captcha (params) {
|
||||
const { type, width = 150, height = 50 } = params;
|
||||
svgCaptcha.options.width = width;
|
||||
svgCaptcha.options.height = height;
|
||||
const svg = svgCaptcha.create({ color: true, background: '#fff' });
|
||||
const result = {
|
||||
captchaId: uuid(),
|
||||
data: svg.data.replace(/\"/g, "'"),
|
||||
};
|
||||
if (type === 'dataUrl') {
|
||||
result.data = svgToDataURL(result.data);
|
||||
}
|
||||
await this.app.redisSet(`verify:img:${ result.captchaId }`, svg.text.toLowerCase(), 1800);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检验图片验证码
|
||||
* @param captchaId 验证码ID
|
||||
* @param value 验证码
|
||||
*/
|
||||
public async check (captchaId, value) {
|
||||
const rv = await this.app.redisGet(`verify:img:${ captchaId }`);
|
||||
if (!rv || !value || value.toLowerCase() !== rv) {
|
||||
return false;
|
||||
} else {
|
||||
this.app.redisDel(`verify:img:${ captchaId }`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
14
appveyor.yml
Normal file
14
appveyor.yml
Normal file
@ -0,0 +1,14 @@
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: '8'
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- npm i npminstall && node_modules\.bin\npminstall
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm run test
|
||||
|
||||
build: off
|
72
config/config.default.ts
Normal file
72
config/config.default.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
|
||||
|
||||
export default (appInfo: EggAppInfo) => {
|
||||
const config = {} as PowerPartial<EggAppConfig>;
|
||||
|
||||
config.keys = appInfo.name + '_1566960005072_482';
|
||||
|
||||
// 中间件
|
||||
config.middleware = [ 'exception' ];
|
||||
|
||||
config.jwt = {
|
||||
secret: 'KFHJALFLAJFJLF',
|
||||
};
|
||||
|
||||
config.security = {
|
||||
csrf: {
|
||||
enable: false,
|
||||
},
|
||||
};
|
||||
|
||||
const whitelist = [
|
||||
// images
|
||||
'.jpg', '.jpeg', // image/jpeg
|
||||
'.png', // image/png, image/x-png
|
||||
'.gif', // image/gif
|
||||
'.bmp', // image/bmp
|
||||
'.wbmp', // image/vnd.wap.wbmp
|
||||
'.webp',
|
||||
'.tif',
|
||||
'.psd',
|
||||
// text
|
||||
'.svg',
|
||||
'.js', '.jsx',
|
||||
'.json',
|
||||
'.css', '.less',
|
||||
'.html', '.htm',
|
||||
'.xml',
|
||||
// tar
|
||||
'.zip',
|
||||
'.gz', '.tgz', '.gzip',
|
||||
// video
|
||||
'.mp3',
|
||||
'.mp4',
|
||||
'.avi',
|
||||
'.xlsx',
|
||||
'.xls',
|
||||
];
|
||||
|
||||
config.multipart = {
|
||||
fileSize: '100mb',
|
||||
mode: 'file',
|
||||
whitelist,
|
||||
};
|
||||
|
||||
config.redis = {
|
||||
client: {
|
||||
port: 6379,
|
||||
host: '127.0.0.1',
|
||||
password: '',
|
||||
db: 0,
|
||||
},
|
||||
};
|
||||
// 新增特殊的业务配置
|
||||
const bizConfig = {
|
||||
sourceUrl: `https://github.com/eggjs/examples/tree/master/${ appInfo.name }`
|
||||
};
|
||||
|
||||
return {
|
||||
...config,
|
||||
...bizConfig,
|
||||
};
|
||||
};
|
24
config/config.local.ts
Normal file
24
config/config.local.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { EggAppConfig, PowerPartial } from 'egg';
|
||||
|
||||
export default () => {
|
||||
const config: PowerPartial<EggAppConfig> = {};
|
||||
config.cluster = {
|
||||
listen: {
|
||||
port: 7001,
|
||||
hostname: '0.0.0.0',
|
||||
},
|
||||
};
|
||||
config.typeorm = {
|
||||
client: {
|
||||
type: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
username: 'root',
|
||||
password: '123123',
|
||||
database: 'test',
|
||||
synchronize: true,
|
||||
logging: true,
|
||||
},
|
||||
};
|
||||
return config;
|
||||
};
|
24
config/config.prod.ts
Normal file
24
config/config.prod.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { EggAppConfig, PowerPartial } from 'egg';
|
||||
|
||||
export default () => {
|
||||
const config: PowerPartial<EggAppConfig> = {};
|
||||
config.cluster = {
|
||||
listen: {
|
||||
port: 7001,
|
||||
hostname: '0.0.0.0',
|
||||
},
|
||||
};
|
||||
config.typeorm = {
|
||||
client: {
|
||||
type: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
username: 'root',
|
||||
password: '123123',
|
||||
database: 'test',
|
||||
synchronize: true,
|
||||
logging: true,
|
||||
},
|
||||
};
|
||||
return config;
|
||||
};
|
22
config/plugin.ts
Normal file
22
config/plugin.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { EggPlugin } from 'egg';
|
||||
|
||||
const plugin: EggPlugin = {
|
||||
typeorm: {
|
||||
enable: true,
|
||||
package: 'egg-ts-typeorm',
|
||||
},
|
||||
jwt: {
|
||||
enable: true,
|
||||
package: 'egg-jwt',
|
||||
},
|
||||
oss: {
|
||||
enable: true,
|
||||
package: 'egg-oss',
|
||||
},
|
||||
redis: {
|
||||
enable: true,
|
||||
package: 'egg-redis',
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
2
ormconfig.yml
Normal file
2
ormconfig.yml
Normal file
@ -0,0 +1,2 @@
|
||||
default: # 默认连接
|
||||
entitiesdir: "app/entities"
|
70
package.json
Normal file
70
package.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "cool-admin-api_node",
|
||||
"version": "1.0.0",
|
||||
"description": "cool-admin api接口 node版",
|
||||
"private": true,
|
||||
"egg": {
|
||||
"typescript": true,
|
||||
"declarations": true
|
||||
},
|
||||
"scripts": {
|
||||
"start": "egg-scripts start --daemon --title=egg-server-cool-admin-api_node",
|
||||
"stop": "egg-scripts stop --title=egg-server-cool-admin-api_node",
|
||||
"dev": "egg-bin dev",
|
||||
"debug": "egg-bin debug",
|
||||
"test-local": "egg-bin test",
|
||||
"test": "npm run lint -- --fix && npm run test-local",
|
||||
"cov": "egg-bin cov",
|
||||
"tsc": "ets && tsc -p tsconfig.json",
|
||||
"ci": "npm run lint && npm run cov && npm run tsc",
|
||||
"autod": "autod",
|
||||
"lint": "tslint --project . -c tslint.json",
|
||||
"clean": "ets clean",
|
||||
"build": "npm run tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.0",
|
||||
"egg": "^2.6.1",
|
||||
"egg-jwt": "^3.1.6",
|
||||
"egg-oss": "^2.0.0",
|
||||
"egg-redis": "^2.4.0",
|
||||
"egg-scripts": "^2.6.0",
|
||||
"egg-ts-typeorm": "^1.1.12",
|
||||
"ipip-ipdb": "^0.3.0",
|
||||
"lodash": "^4.17.15",
|
||||
"md5": "^2.2.1",
|
||||
"moment": "^2.24.0",
|
||||
"mysql": "^2.17.1",
|
||||
"svg-captcha": "^1.4.0",
|
||||
"svg-to-dataurl": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^2.2.40",
|
||||
"@types/node": "^7.0.12",
|
||||
"@types/supertest": "^2.0.0",
|
||||
"autod": "^3.0.1",
|
||||
"autod-egg": "^1.1.0",
|
||||
"egg-bin": "^4.11.0",
|
||||
"egg-ci": "^1.8.0",
|
||||
"egg-mock": "^3.16.0",
|
||||
"tslib": "^1.9.0",
|
||||
"tslint": "^5.0.0",
|
||||
"tslint-config-egg": "^1.0.0",
|
||||
"typescript": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
},
|
||||
"ci": {
|
||||
"version": "8"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"eslintIgnore": [
|
||||
"coverage"
|
||||
],
|
||||
"author": "cool",
|
||||
"license": "MIT"
|
||||
}
|
9
test/app/controller/home.test.ts
Normal file
9
test/app/controller/home.test.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as assert from 'assert';
|
||||
import { app } from 'egg-mock/bootstrap';
|
||||
|
||||
describe('test/app/controller/home.test.ts', () => {
|
||||
it('should GET /', async () => {
|
||||
const result = await app.httpRequest().get('/').expect(200);
|
||||
assert(result.text === 'hi, egg');
|
||||
});
|
||||
});
|
16
test/app/service/Test.test.ts
Normal file
16
test/app/service/Test.test.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import * as assert from 'assert';
|
||||
import { Context } from 'egg';
|
||||
import { app } from 'egg-mock/bootstrap';
|
||||
|
||||
describe('test/app/service/Test.test.js', () => {
|
||||
let ctx: Context;
|
||||
|
||||
before(async () => {
|
||||
ctx = app.mockContext();
|
||||
});
|
||||
|
||||
it('sayHi', async () => {
|
||||
const result = await ctx.service.test.sayHi('egg');
|
||||
assert(result === 'hi, egg');
|
||||
});
|
||||
});
|
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"charset": "utf8",
|
||||
"allowJs": false,
|
||||
"pretty": true,
|
||||
"noEmitOnError": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"strictPropertyInitialization": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"inlineSourceMap": true,
|
||||
"importHelpers": true
|
||||
},
|
||||
"exclude": [
|
||||
"app/public",
|
||||
"app/views",
|
||||
"node_modules*"
|
||||
]
|
||||
}
|
3
tslint.json
Normal file
3
tslint.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["tslint-config-egg"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user