# 技术选型

Node版后台基础框架基于Egg.js (opens new window)(阿里出品)

# 核心组件

独有COOL发布的npm组件

# Egg插件

更多Egg插件 (opens new window)

# 目录结构

目录可参考egg目录 (opens new window)

cool-admin
 ├── package.json
 ├── app.ts (可选)
 ├── agent.ts (可选)
 ├── app
 |   ├── router.ts
 │   ├── controller
 │   |   └── home.ts
 │   ├── entity(数据模型)
 │   ├── service (可选)|   └── user.ts
 │   ├── middleware (可选)|   └── response_time.ts
 │   ├── schedule (可选)|   └── my_task.ts
 |   ├── public(静态文件)
 |   ├── view(模板文件)
 │   └── extend (可选)
 │       ├── helper.ts (可选)
 │       ├── request.ts (可选)
 │       ├── response.ts (可选)
 │       ├── context.ts (可选)
 │       ├── application.ts (可选)
 │       └── agent.ts (可选)
 ├── config
 |   ├── plugin.ts
 |   ├── config.default.ts
 │   ├── config.prod.ts
 |   ├── config.test.ts (可选)
 |   ├── config.local.ts (可选)
 |   └── config.unittest.ts (可选)
 └── test
     ├── middleware
     |   └── response_time.test.ts
     └── controller
         └── home.test.ts

# 运行

环境 Node.js>=8.9.0 Redis

新建并导入数据库MySql>=5.7,数据库脚本位于 db/init.sql,修改数据库连接信息config/config.*.typeorm

推荐使用yarn

1、yarn 2、yarn dev 3、http://localhost:7001

或者npm

1、npm install 2、npm run dev 3、http://localhost:7001

# 数据模型

数据模型必须放在app/entity/*下,否则typeorm (opens new window)无法识别,如:

 import { Entity, Column, Index, Double } from 'typeorm';
 import { BaseEntity } from 'egg-cool-entity';
 /**
  * 商品
  */
 @Entity({ name: 'goods' })
 export default class Goods extends BaseEntity {
     // 标题
     @Index({ unique: true })
     @Column()
     title: string;
     // 标签
     @Index({ unique: true })
     @Column({ nullable: true })
     label: string;
     // 备注
     @Column({ nullable: true })
     remark: string;
     // 价格
     @Column({ type: 'decimal', scale: 2, precision: 10 })
     price: Double;
 }

新建完成运行代码,就可以看到数据库新建了一张sys_role表,如不需要自动创建config文件夹下修改typeorm (opens new window)的配置文件

# 控制器

有了数据表之后,如果希望通过接口对数据表进行操作,我们就必须在controller文件夹下新建对应的控制器,如:

import { BaseController } from 'egg-cool-controller';
import router from 'egg-cool-router';
import { Brackets } from 'typeorm';
/**
 * 系统-角色
 */
@router.prefix('/admin/sys/role', [ 'add', 'delete', 'update', 'info', 'list', 'page' ])
export default class SysRoleController extends BaseController {
    init () {
        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 (opens new window)
fieldEq 数组 动态条件全匹配,如需要筛选用户状态status,就可以设置成['status'],此时接口就可以接受status的值并且对数据有过滤效果
addOrderBy 对象 排序条件可传多个,如{ sortNum:asc, createTime:desc }

# 数据缓存

有些业务场景,我们并不希望每次请求接口都需要操作数据库,如:今日推荐、上个月排行榜等,数据存储在redis,注:缓存注解只在service层有效

import { BaseService } from 'egg-cool-service';
import { Cache } from 'egg-cool-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 (opens new window)原生的路由写法过于繁琐,cool-admin的路由支持BaseController还有其他原生支持具体参照egg.js路由 (opens new window)

# 自定义sql查询

除了单表的简单操作,真实的业务往往需要对数据库做一些复杂的操作。这时候我们可以在service自定义SQL,如

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.nativeQuery(sql);// 原生查询
    return this.sqlRenderPage(sql, query);// 原生分页查询
}

注:这种SQL拼接方式最终还是以传参的形式执行SQL,也是具有参数检验功能,避免SQL注入问题

# this.setSql() 设置参数

参数 类型 说明
condition 布尔型 只有满足改条件才会拼接上相应的sql和参数
sql 字符串 需要拼接的参数
params 数组 相对应的参数

# this.nativeQuery() 原生查询

参数 类型 说明
sql 字符串 查询语句
params 对象 查询参数

# this.sqlRenderPage() 原生分页查询

参数 类型 说明
sql 字符串 查询语句
param 对象 查询参数

# 支付(egg-cool-pay)

安装组件yarn add egg-cool-pay

开启插件config/plugin.ts新增

    coolPay: {
        enable: true,
        package: 'egg-cool-pay',
    },

配置文件新增

config.coolPay = {
        wx: {
            appid: '',
            mchid: '',
            partnerKey: '',
            notify_url: '',
            // spbill_create_ip: '127.0.0.1'
        },
        ali: {
            appId: '',
            notifyUrl: '',
            rsaPrivate: path.resolve('./pem/aliPrivate.pem'),
            rsaPublic: path.resolve('./pem/aliPublic.pem'),
            sandbox: false,
            signType: 'RSA2',
        },
    };

支付宝的公钥下载后需要格式化才能使用(添加首尾、以每行64字符进行断行),如:

-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQCbc/e1JYkfcJ54+9d+l/cIuv5X3j71qREf/06WGIhewt4liu38
MzSTLJWc4HOotfeHraPpwDJ5YipYwGIfaiDDqZRfZuy+rEyQT8LInnU6OTzVkUvK
+2NMIlyOitC5sb3p61ceRyAsRM1sF7x3DszVY253aokjr82u0yXgOttu8QIDAQAB
AoGAbv11tth99vypqSfmzHQj5Q/d2U7NFQkJORdIPNJ3S3FGuvdew9qrhwkmKUP4
UFTbzvujNJoNb60vHv79EADuMqeZxnP4fHRRPoWFtYSCXf2rLQKUJaPlZLx6oiSh
5spL3wOT/hfh9qNlzz/+HRT4P8chXn0OkO4hQRCWoPmSw4ECQQDWkgCJkYAq0gIb
m1VTiwvfHiJLT1VV01gksOOXpW8i8DUeZDGWMslUsqoefu8HenytKzOvvTOYQEer
2238c9fFAkEAuXfa30m8DOSXGGz7OXOv3KFVZyf2wzh1AltywW0kw0+PKbiSjR/n
LQybKcsuX+EXodEWIcFmlysaovs3oxhBPQJBAJtPcP4iiD/2ZLow1DE1azFjsXUL
hnwqDxn3w7VHdMs4TWqjIVVTi3E4JXUPcdra6RW7OJ1S+N6SYI5ftRvPifUCQQCY
hYc6DwIVvDrBfIYRFiEumIKKJaRZoOkguiGiDeaos5mxHrduVSkgs/g6I3wMnyh3
C2Je+hQrBuiN1XhIqJ6lAkEAmYu2ap+vqHgsjDksnDy5zwMCzywbUBvGIa53zfyi
KYoEmSaDc3AMqaxbxGHwMvPupGTvwiYPyQ+E0Qf5PXa5Kw==
-----END RSA PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDIgHnOn7LLILlKETd6BFRJ0Gqg
S2Y3mn1wMQmyh9zEyWlz5p1zrahRahbXAfCfSqshSNfqOmAQzSHRVjCqjsAw1jyq
rXaPdKBmr90DIpIxmIyKXv4GGAkPyJ/6FTFY99uhpiq0qadD/uSzQsefWo0aTvP/
65zi3eof7TcZ32oWpwIDAQAB
-----END PUBLIC KEY-----

egg-cool-pay的拓展功能

支付示例controller

import { BaseController } from 'egg-cool-controller';
import router from 'egg-cool-router';

const xmlMiddleware = async (ctx, next) => {
    ctx.app.use(ctx.app.bodyParser.urlencoded({
        extended: true,
    }));
    await next();
};

/**
 * 微信、支付宝支付示例
 */
@router.prefix('/pay/example')
export default class Pay extends BaseController {
    /**
     * 微信支付下单 扫码支付
     */
    @router.post('/wx')
    async wx () {
        const orderNum = await this.app.createOrderNum(); // 基于redis支持高并发的唯一订单号
        const data = await this.app.wxPay.unifiedOrder({
            out_trade_no: orderNum,
            body: '测试微信支付',
            total_fee: 1,
            trade_type: 'NATIVE',
            product_id: 'test001',
        });
        this.res({ data });
    }

    /**
     * 微信支付通知回调
     */
    @router.post('/wx-notify', xmlMiddleware)
    async wxNotify () {
        let data = '';
        this.ctx.req.setEncoding('utf8');
        this.ctx.req.on('data', chunk => {
            data += chunk;
        });
        const results = await new Promise((resolve, reject) => {
            this.ctx.req.on('end', () => {
                this.app.xml2js(data, { explicitArray: false }, async (err, json) => {
                    if (err) {
                        return reject('success');
                    }
                    const checkSign = await this.app.wxSignVerify(json.xml);
                    if (checkSign && json.xml.result_code === 'SUCCESS') {
                        // 处理业务逻辑
                        console.log('微信支付成功', json.xml);
                        return resolve(true);
                    }
                    return resolve(false);
                });
            });
        });
        if (results) {
            this.ctx.body = '<xml><return_msg>OK</return_msg><return_code>SUCCESS</return_code></xml>';
        }
    }

    /**
     * 支付宝支付
     */
    @router.get('/ali')
    async ali () {
        const orderNum = await this.app.createOrderNum(); // 参数是用户ID
        const params = await this.app.aliPay.wapPay({
            subject: '测试支付宝支付',
            body: '支付宝支付',
            outTradeId: orderNum,
            timeout: '10m',
            amount: '0.01',
            goodsType: '0',
            qrPayMode: 2,
            return_url: 'https://www.baidu.com',
        });
        this.ctx.redirect('https://openapi.alipay.com/gateway.do?' + params);
    }

    /**
     * 支付宝支付回调
     */
    @router.post('/ali-notify')
    async aliNotify () {
        const { trade_status, out_trade_no } = this.getBody();
        const check = await this.app.aliPay.signVerify(this.getBody());
        if (check && trade_status === 'TRADE_SUCCESS') {
            // 处理逻辑
           console.log('支付宝支付成功', out_trade_no);
        }
        this.ctx.body = 'success';
    }
}

# 文件上传

# oss模式

cool-admin的文件上传默认采用阿里的OSS (opens new window)

修改配置文件config/config.default.ts

config.oss = {
    client: {
        accessKeyId: '',
        accessKeySecret: '',
        bucket: '',
        endpoint: '',
        timeout: '3600s',
    },
};

修改配置文件config/plugin.ts

oss: {
        enable: true,
        package: 'egg-oss',
    },

更多OSS相关的API文档 (opens new window)

# 本地模式

修改config.default.ts中的

    // 新增特殊的业务配置
    const bizConfig = {
        upload: 'local', // 可选 local:存储在本地; oss:存储在阿里云的oss
        baseUrl: 'http://127.0.0.1:7001' // 当mode为 local 时必填,
    };

WARNING

注:local模式根域名是需要自己配置的

# ElasticSearch(egg-cool-es)

ElasticSearch 一般可以用来做附近的人日志商品搜索统计分析等需求,亿级数据都能快速查询

egg-cool-es 是cool-admin专门的es组件

# 1、安装egg-cool-es
yarn add egg-cool-es
# 2、修改配置启用插件

修改config/plugin.ts文件,新增

coolEs: {
    enable: true,
    package: 'egg-cool-es',
},

新增配置

config.coolEs = {
    baseDir: 'esmodel',
    host: '127.0.0.1:9200',
    apiVersion: '7.1',
};
# 3、创建索引

默认在esmodel/*文件夹下新建如test

export default app => {
    const properties = {
        name: { type: 'text', analyzer: 'ik_max_word', search_analyzer: 'ik_max_word' }, // 名称
    };
    return  app.createIndex({ properties, shards: 1, model: app.fileName(__filename), replicas: 1 });
};

# 4、增删改查

最新版本需要elasticsearch 7.x以上

除了官方的API (opens new window)

egg-cool-es还提供了一些方便的操作如:

// 创建索引
await this.ctx.esmodel.test.crateIndex({ id: 1, name: '测试' });
// 删除索引
await this.ctx.esmodel.test.deleteById(1)
// 删除索引
await this.ctx.esmodel.test.deleteByIds([1,2,3])
// 删除索引
await this.ctx.esmodel.test.deleteByQuery(body)
// 更新索引
await this.ctx.esmodel.test.updateById({ id: 1, name: '测试2' })
// 根据id查询信息
const result = await this.ctx.esmodel.test.findById(2)
// 根据多个id查询信息
const result = await this.ctx.esmodel.test.findByIds([1,2])
// 查询条数
const result = await this.ctx.esmodel.test.findCount(body)
// 查找信息
const result = await this.ctx.esmodel.test.find(body)
// 分页查询
const result = await this.ctx.esmodel.test.findPage(1, body)
// 批量新增 type: index、create、delete、update
const list = [ { name: '测试名称' }, { name: '测试名称2' } ]
const result = await this.ctx.esmodel.test.batchIndex(list)
// 批量删除 type: index、create、delete、update
const list = [ { name: '测试名称' }, { name: '测试名称2' } ]
const result = await this.ctx.esmodel.test.batchIndex(list, 'delete' )
// 操作原生的API
await this.ctx.esmodel.test.native // 方式1
await this.ctx.esmodel.test.nativeWithModel // 方式2 该方式不需要写 index和type
await this.app.es // 方式3

# 任务调度与队列(egg-cool-task)

egg-cool-task (opens new window)是基于bull (opens new window)的一个依赖redis支持分布式的任务调度与队列组件

cool-admin系统自带该组件,支持普通优先延时重复等多种队列任务,相关API可移步bull (opens new window)查看

# 执行任务

位于task/*文件夹下,你也可以新建自己独有的TASK,如:test.ts

export default app => {
    const ctx = app.createAnonymousContext();
    const queue = app.bull.get(app.fileName(__filename));
    app.ready(() => {
        queue.process((job, done) => {
            // 执行你的业务代码
            done();
        });
    });
    return queue;
};

# 发布任务

详细API,新增任务 (opens new window)

await this.ctx.task.test.add(); // 发布一个任务到sys任务队列

# 集群模式

直接redis修改配置文件即可,如:

config.redis = {
    client: {
        cluster: true,
        nodes: [{
            host: '192.168.0.103',
            port: 7000,
            password: '',
            db: 0,
        },
                {
            host: '192.168.0.103',
            port: 7001,
            password: '',
            db: 0,
        },
                {
            host: '192.168.0.103',
            port: 7002,
            password: '',
            db: 0,
        },
                {
            host: '192.168.0.103',
            port: 7003,
            password: '',
            db: 0,
        },
                {
            host: '192.168.0.103',
            port: 7004,
            password: '',
            db: 0,
        },
                {
            host: '192.168.0.103',
            port: 7005,
            password: '',
            db: 0,
        },
        ],
    },
};

# 参数配置

# 说明

在后台编辑配置的参数都会缓存于redis当中,获取参数时不需要再读取数据库,同时提高获取参数的速度

# 获取普通参数

代码如下:

const vaule = await this.service.sys.param.dataByKey('输入对应的key')
consolo.log(vaule)

# 获得富文本的对应的网页内容

展示:http://127.0.0.1:7001/app/comm/html?key=test

const vaule = await this.service.sys.param.htmlByKey('输入对应的key')
consolo.log(vaule)

# swagger(不建议使用)

可以参考 egg-shell-decorators (opens new window),建议使用一些其他的非代码侵入式的文档如:小幺鸡 (opens new window)

# 后台权限相关

  • /admin开头的路由地址都会受到权限控制(需要登录才能访问);
  • 忽略权限:不要以/admin开头,或者在app/middleware/authority.ts下配置noTokenUrl参数;
  • /admin开头的都规范为后台接口,所有后台接口要访问必须在菜单管理配置完才能访问;

# 常见问题

# windows下vscode开发报 'Expected linebreaks to be 'LF' but found 'CRLF''

解决办法参考 链接 (opens new window)

# ENOSPC Error

运行命令 echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

# mysql8.0 连接认证失败

mysql8.0默认的认证方式不是账户密码,可以根据修改方式 (opens new window)修改即可

# 自动创建表

建议只有开发的时候才使用自动创建表,其配置在config/config.local.ts

config.typeorm = {
    client: {
        type: 'mysql',
        host: '',
        port: 3306,
        username: '',
        password: '',
        database: '',
        synchronize: true, // 自动建表
        logging: true, // 打印sql日志
        charset: ''
    },
};