传送门
本章内容
- 数据查询接口
Ant Design Pro
接入Linux
下Nginx
部署Jenkins
持续集成/部署
数据查询
定义查询接口需要使用的数据类型,modules/stock
目录下新建dto
目录,新建stock.dto.ts
文件,定义以下类型:
import { Exclude } from 'class-transformer';
// 股票列表类型
export class StockDto {
readonly code: string;
readonly name: string;
readonly market: string;
readonly price: string;
readonly peTtm: string;
readonly peTtmAvg: string;
readonly peTtmRate: string;
readonly peTtmMid: string;
@Exclude()
readonly sourceData: object | null;
us: any;
}
// 列表查询参数类型
export class StockQueryDto {
readonly pageSize: number;
readonly pageIndex: number;
readonly keywords: string;
readonly orderBy: number;
}
export class UserStockDto {
readonly code: string;
}
// 分页数据类型
export class PageListModel<T> {
totalNum: number; // 总数
pageSize: number; // 页数量
pageIndex: number; // 当前页码
list: T[];
}
modules/stock
目录下创建股票数据service
:
$ nest g s modules/stock
查询方法演示了TypeOrm
中QueryBuilder
和执行原生SQL
语句的写法(原生SQL
建议使用参数化查询防范sql
注入,这里只做演示),代码如下:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository, getConnection, getManager } from 'typeorm';
import { plainToClass } from 'class-transformer';
import { StockDto, StockQueryDto, PageListModel } from './dto/stock.dto';
import { Stock } from '../../entities/Stock';
import { UserStock } from '../../entities/UserStock';
@Injectable()
export class StockService {
constructor(
@InjectRepository(Stock)
private readonly stStockRepository: Repository<Stock>,
) {}
/**
* 股票列表 QueryBuilder方式查询
* leftjoin 文档缺失,使用QueryBuilder分两次查询
* @param param
* @returns
*/
async getStockList(
uid: number,
param: StockQueryDto,
): Promise<PageListModel<StockDto>> {
const pageIndex = param.pageIndex ? param.pageIndex : 1;
const pageSize = param.pageSize ? param.pageSize : 10;
const orderBy = param.orderBy ? param.orderBy : 0;
let orderByMap: any = {
'st.code': 'DESC',
};
switch (orderBy) {
case 1:
orderByMap = {
'st.peTtmRate': 'ASC',
};
break;
case 2:
orderByMap = {
'st.peTtmRate': 'DESC',
};
break;
default:
break;
}
let keyWordsWhere = 'st.is_delete = 0';
if (param.keywords) {
keyWordsWhere += ' and (st.code like :code or st.name like :name)';
}
const { totalNum } = await getRepository(Stock)
.createQueryBuilder('st')
.select('COUNT(1)', 'totalNum')
.where(keyWordsWhere, {
code: '%' + param.keywords + '%',
name: '%' + param.keywords + '%',
})
.getRawOne();
const stockList = await getRepository(Stock)
.createQueryBuilder('st')
.select([
'st.id',
'st.code',
'st.name',
'st.pe',
'st.peTtm',
'st.peTtmAvg',
'st.peTtmMid',
'st.peTtmRate',
'st.updateDt',
])
.where(keyWordsWhere, {
code: '%' + param.keywords + '%',
name: '%' + param.keywords + '%',
})
.orderBy(orderByMap)
.skip((pageIndex - 1) * pageSize)
.take(pageSize)
.getMany();
// 转换类型,否则需要Stock上添加 us属性
const stocks = plainToClass(StockDto, stockList);
for (let i = 0; i < stocks.length; i++) {
// 查询关系表是否存在
const us = await getConnection()
.createQueryBuilder()
.select(['us.uid'])
.from(UserStock, 'us')
.where('us.uid = :uid and us.code = :code ', {
uid,
code: stocks[i].code,
})
.getOne();
if (us) {
stocks[i].us = us;
} else {
stocks[i].us = null;
}
}
return {
list: stocks,
totalNum,
pageIndex,
pageSize,
};
}
/**
* 添加自选
* @param uid
* @param code
* @returns
*/
async addUserStock(uid: number, code: string): Promise<boolean> {
let result = false;
// 判断关系是否已存在
const userStock = await getConnection()
.createQueryBuilder()
.select(['ut.id'])
.from(UserStock, 'ut')
.where('ut.uid = :uid and ut.code = :code ', {
uid,
code,
})
.getOne();
if (!userStock) {
const ut = await getConnection()
.createQueryBuilder()
.insert()
.into(UserStock)
.values([{ uid, code }])
.execute();
if (ut) {
result = true;
}
}
return result;
}
/**
* 自选列表 原生sql语句的方式执行
* @param uid
* @param param
* @returns
*/
async getUserStocts(
uid: number,
param: StockQueryDto,
): Promise<PageListModel<UserStock>> {
const pageIndex = param.pageIndex ? param.pageIndex : 1;
const pageSize = param.pageSize ? param.pageSize : 10;
const orderBy = param.orderBy ? param.orderBy : 0;
let orderByStr = `order by s.code desc `;
switch (orderBy) {
case 1:
orderByStr = `order by s.pe_ttm_rate asc `;
break;
case 2:
orderByStr = `order by s.pe_ttm_rate desc `;
break;
default:
break;
}
let keyWordsWhere = `ut.uid=${uid} and s.is_delete=0`;
if (param.keywords) {
keyWordsWhere += ` and (s.code like '%${param.keywords}%' or s.name like '%${param.keywords}%') `;
}
const limit = ` limit ${pageSize} offset ${(pageIndex - 1) * pageSize}`;
const manager = getManager();
const total = await manager.query(
`select count(1) as totalNum
from user_stock ut inner join stock s on ut.code=s.code where ${keyWordsWhere}`,
);
const stockList = await manager.query(
`select ut.uid,ut.code,s.name,
s.pe_ttm as peTtm,
s.pe_ttm_avg as peTtmAvg,
s.pe_ttm_mid as peTtmMid,
s.pe_ttm_rate as peTtmRate,
s.update_dt as updateDt
from user_stock ut inner join stock s on ut.code=s.code where ${keyWordsWhere} ${orderByStr} ${limit}`,
);
return {
list: stockList,
totalNum: total.length > 0 ? Number(total[0].totalNum) : 0,
pageIndex,
pageSize,
};
}
/**
* 删除自选
* @param uid
* @param code
* @returns
*/
async deleteUserStock(uid: number, code: string): Promise<boolean> {
let result = false;
const res = await getConnection()
.createQueryBuilder()
.delete()
.from(UserStock)
.where('uid = :uid', { uid })
.andWhere('code = :code', { code })
.execute();
if (res.affected > 0) {
result = true;
}
return result;
}
}
创建stock.controller.ts
:
$ nest g co modules/stock
修改stock.controller.ts
模板代码,注意UseGuards
的使用,作用域为全部路由:
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { StockService } from './stock.service';
import { StockQueryDto } from './dto/stock.dto';
import { AuthGuard } from '@nestjs/passport';
import { RolesGuard } from '../auth/guards/role.guard';
import { User } from '../../libs/decorators/user.decorator';
import { ProfileInfo } from '../../libs/decorators/profileInfo';
import { UserStockDto } from './dto/stock.dto';
@Controller('stock')
@UseGuards(AuthGuard('jwt'))
export class StockController {
constructor(private readonly userService: StockService) {}
@UseGuards(RolesGuard)
@Post('list')
async getStockList(@Body() parmas: StockQueryDto, @User() user: ProfileInfo) {
return this.userService.getStockList(user.uid, parmas);
}
@UseGuards(RolesGuard)
@Post('add-choice')
async addChoice(@Body() parmas: UserStockDto, @User() user: ProfileInfo) {
return this.userService.addUserStock(user.uid, parmas.code);
}
@UseGuards(RolesGuard)
@Post('user-list')
async getUserStocts(
@Body() parmas: StockQueryDto,
@User() user: ProfileInfo,
) {
return this.userService.getUserStocts(user.uid, parmas);
}
@UseGuards(RolesGuard)
@Post('delete-choice')
async deleteChoice(@Body() parmas: UserStockDto, @User() user: ProfileInfo) {
return this.userService.deleteUserStock(user.uid, parmas.code);
}
}
运行程序,访问http://localhost:3000/stock/list
POST
(注意带Authorization
)参数如下:
{
"pageSize":10,
"pageIndex":1,
"keywords":"中国",
"orderBy":1
}
返回结果:
Ant Design Pro
api
接口准备就绪,安装Ant Design Pro
相关,参考文档 Ant Design Pro
前端页面初始化完成后,config/config.ts
需要注意以下几点:
- 使用代理,服务部署到服务器上配置代理需要和前端对应
- 前端项目部署在服务器非根目录,需要注意
base
和publicPath
配置 mock
打开与关闭等
Ant Design Pro
默认使用@umijs/plugin-request
作为请求库,而request
库本身已停止更新,并且在获取Response
头属性等方面不灵活,故而基于axios
重写了请求类,将http code
和业务code
做了分类处理,其他细节请直接查考源码。
Linux
下Nginx
部署
服务部署可以基于docker打包成镜像的方式,也可自己搭建node+nginx
的方式部署,此次使用第二种方式。
服务端环境:
$ cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)
$ node -v
v14.16.0
$ npm -v
6.14.11
$ pm2 -v
4.5.4
node
服务部署需注意以下:
-
本地
build
出的build/dist
直接放置到服务器上是不能运行的,相关依赖并不会打包到输出包,推荐在服务器上安装依赖后打包; -
由于
node
事件循环的单线程机制,可以使用pm2
实现进程多开; -
Nginx
代理设置需要和Ant Design Pro
保持一致,这样无论本地开发或者上线部署都不需要改动代码;
前端部署,如果是在非根目录下,请参考 非根目录
Jenkins
持续集成/部署
Jenkins
安装请参考Jenkins安装,Jenkins
主要使用单项构建或者流水线构建的方式,流水线构建方式简单粗暴的可以理解为通过Jenkinsfile
定义多个分步骤进行的单项,每个单项之间可以设置人工确认过程而决定是否继续下一步,更多细节请参考Jenkins文档 本次使用单项构建方式即可满足需求。其主要执行过程可以分为以下几个步骤:
- 手动或者代码仓库通过
webhooks
触发构建 Jenkins
通过预先配置的仓库访问令牌拉取指定的仓库代码- 使用预先配置的执行环境执行单项构建中构建命令
- 构建后操作
一般网络环境下代码仓库不建议选择github
,拉取速度会让人怀疑人生,本次以码云托管代码为例参考文档码云文档 ,按照上述关键步骤提炼出相关的关键配置:
- 安装
gitee
插件:系统配置-->插件管理-->搜索gitee
-->点击安装
- 全局配置
gitee
参数:系统配置-->系统配置-->找到gitee
配置-->输入相关配置
- 点击证书令牌“添加”-->选择“Gitee API令牌”-->输入码云上的私人令牌,添加ID方便后续使用;
- 安装
node
插件:系统配置-->插件管理-->搜索node
-->点击安装 - 配置
node
环境:系统配置-->全局工具配置-->NodeJs-->点击安装-->输入别名-->选择要安装的版本-->保存
- 创建单项构建:新建任务-->选择自由风格项目-->确定
- 通用设置
- 源码管理:选择
git
- 添加源码访问凭据
- 触发配置,视情况勾选
- 配置构建目标分支和
webhooks
秘钥(需要在码云中输入)
- 绑定配置,选择前面全局配置的
node
版本
- 构建脚本:
Jenkins
有自己的工作空间,拉取的代码以及执行构建的输出都保存在这里,下面示例为构建环境和发布环境在同一虚拟机中,直接cp
文件到目标路径然后重启pm2
即可。如果不是同一机器,则可使用ssh命令发送文件到目标机器。
- 保存,手动或者提交代码触发测试,如果报错则通过控制台日志查看报错原因排错。