This commit is contained in:
lixin 2025-01-09 16:10:19 +08:00
commit a1293723eb
219 changed files with 13801 additions and 0 deletions

19
.dockerignore Normal file
View File

@ -0,0 +1,19 @@
logs/
npm-debug.log
yarn-error.log
node_modules/
package-lock.json
yarn.lock
coverage/
dist/
.idea/
run/
.DS_Store
*.sw*
*.un~
.tsbuildinfo
.tsbuildinfo.*
.audit
typings/
public/uploads/
cache/

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
# 🎨 editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

30
.eslintrc.json Normal file
View File

@ -0,0 +1,30 @@
{
"extends": "./node_modules/mwts/",
"ignorePatterns": [
"node_modules",
"dist",
"test",
"jest.config.js",
"typings",
"public/**/**",
"view/**/**",
"packages"
],
"env": {
"jest": true
},
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/ban-ts-comment": "off",
"node/no-extraneous-import": "off",
"no-empty": "off",
"node/no-extraneous-require": "off",
"node/no-unpublished-import": "off",
"eqeqeq": "off",
"node/no-unsupported-features/node-builtins": "off",
"@typescript-eslint/ban-types": "off",
"no-control-regex": "off",
"prefer-const": "off"
}
}

4
.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
*.js text eol=lf
*.json text eol=lf
*.ts text eol=lf
*.code-snippets text eol=lf

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
logs/
cache/
npm-debug.log
yarn-error.log
node_modules/
package-lock.json
yarn.lock
coverage/
dist/
.idea/
run/
.DS_Store
launch.json
*.sw*
*.un~
.tsbuildinfo
.tsbuildinfo.*
data/*
pnpm-lock.yaml
public/uploads/*

9
.hintrc Normal file
View File

@ -0,0 +1,9 @@
{
"extends": [
"development"
],
"hints": {
"typescript-config/consistent-casing": "off",
"typescript-config/strict": "off"
}
}

3
.prettierrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
...require('mwts/.prettierrc.json')
}

28
.vscode/config.code-snippets vendored Normal file
View File

@ -0,0 +1,28 @@
{
"config": {
"prefix": "config",
"body": [
"import { ModuleConfig } from '@cool-midway/core';",
"",
"/**",
" * 模块配置",
" */",
"export default () => {",
" return {",
" // 模块名称",
" name: 'xxx',",
" // 模块描述",
" description: 'xxx',",
" // 中间件,只对本模块有效",
" middlewares: [],",
" // 中间件,全局有效",
" globalMiddlewares: [],",
" // 模块加载顺序默认为0值越大越优先加载",
" order: 0,",
" } as ModuleConfig;",
"};",
""
],
"description": "cool-admin config代码片段"
}
}

19
.vscode/controller.code-snippets vendored Normal file
View File

@ -0,0 +1,19 @@
{
"controller": {
"prefix": "controller",
"body": [
"import { CoolController, BaseController } from '@cool-midway/core';",
"",
"/**",
" * 描述",
" */",
"@CoolController({",
" api: ['add', 'delete', 'update', 'info', 'list', 'page'],",
" entity: 实体,",
"})",
"export class XxxController extends BaseController {}",
""
],
"description": "cool-admin controller代码片段"
}
}

20
.vscode/entity.code-snippets vendored Normal file
View File

@ -0,0 +1,20 @@
{
"entity": {
"prefix": "entity",
"body": [
"import { BaseEntity } from '@cool-midway/core';",
"import { Column, Entity } from 'typeorm';",
"",
"/**",
" * 描述",
" */",
"@Entity('xxx_xxx_xxx')",
"export class XxxEntity extends BaseEntity {",
" @Column({ comment: '描述' })",
" xxx: string;",
"}",
""
],
"description": "cool-admin entity代码片段"
}
}

21
.vscode/event.code-snippets vendored Normal file
View File

@ -0,0 +1,21 @@
{
"event": {
"prefix": "event",
"body": [
"import { CoolEvent, Event } from '@cool-midway/core';",
"",
"/**",
" * 接收事件",
" */",
"@CoolEvent()",
"export class xxxEvent {",
" @Event('updateUser')",
" async updateUser(msg, a) {",
" console.log('ImEvent', 'updateUser', msg, a);",
" }",
"}",
""
],
"description": "cool-admin event代码片段"
}
}

29
.vscode/middleware.code-snippets vendored Normal file
View File

@ -0,0 +1,29 @@
{
"middleware": {
"prefix": "middleware",
"body": [
"import { Middleware } from '@midwayjs/decorator';",
"import { NextFunction, Context } from '@midwayjs/koa';",
"import { IMiddleware } from '@midwayjs/core';",
"",
"/**",
" * 描述",
" */",
"@Middleware()",
"export class XxxMiddleware implements IMiddleware<Context, NextFunction> {",
" resolve() {",
" return async (ctx: Context, next: NextFunction) => {",
" // 控制器前执行的逻辑",
" const startTime = Date.now();",
" // 执行下一个 Web 中间件,最后执行到控制器",
" await next();",
" // 控制器之后执行的逻辑",
" console.log(Date.now() - startTime);",
" };",
" }",
"}",
""
],
"description": "cool-admin middleware代码片段"
}
}

21
.vscode/queue.code-snippets vendored Normal file
View File

@ -0,0 +1,21 @@
{
"queue": {
"prefix": "queue",
"body": [
"import { BaseCoolQueue, CoolQueue } from '@cool-midway/task';",
"",
"/**",
" * 队列",
" */",
"@CoolQueue()",
"export abstract class xxxQueue extends BaseCoolQueue {",
" async data(job: any, done: any) {",
" console.log('收到的数据', job.data);",
" done();",
" }",
"}",
""
],
"description": "cool-admin service代码片段"
}
}

34
.vscode/service.code-snippets vendored Normal file
View File

@ -0,0 +1,34 @@
{
"service": {
"prefix": "service",
"body": [
"import { Init, Provide } from '@midwayjs/decorator';",
"import { BaseService } from '@cool-midway/core';",
"import { InjectEntityModel } from '@midwayjs/typeorm';",
"import { Repository } from 'typeorm';",
"",
"/**",
" * 描述",
" */",
"@Provide()",
"export class XxxService extends BaseService {",
" @InjectEntityModel(实体)",
" xxxEntity: Repository<实体>;",
""
" @Init()"
" async init() {",
" await super.init();",
" this.setEntity(this.xxxEntity);",
" }",
"",
" /**",
" * 描述",
" */",
" async xxx() {}",
"}",
""
],
"description": "cool-admin service代码片段"
}
}

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
FROM node:lts-alpine
WORKDIR /app
# 配置alpine国内镜像加速
RUN sed -i "s@http://dl-cdn.alpinelinux.org/@https://repo.huaweicloud.com/@g" /etc/apk/repositories
# 安装tzdata,默认的alpine基础镜像不包含时区组件安装后可通过TZ环境变量配置时区
RUN apk add --no-cache tzdata
# 设置时区为中国东八区这里的配置可以被docker-compose.yml或docker run时指定的时区覆盖
ENV TZ="Asia/Shanghai"
# 如果各公司有自己的私有源可以替换registry地址,如使用官方源注释下一行
RUN npm config set registry https://registry.npm.taobao.org
# 安装开发期依赖
COPY package.json ./package.json
RUN npm install
# 构建项目
COPY . .
RUN npm run build
# 删除开发期依赖
RUN rm -rf node_modules && rm package-lock.json
# 安装生产环境依赖
RUN npm install --production
# 如果端口更换,这边可以更新一下
EXPOSE 8001
CMD ["npm", "run", "start"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 cool-team-official
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

167
README.md Normal file
View File

@ -0,0 +1,167 @@
<p align="center">
<a href="https://midwayjs.org/" target="blank"><img src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/logo.png" width="200" alt="Midway Logo" /></a>
</p>
<p align="center">cool-admin(midway版)一个很酷的后台权限管理系统开源免费模块化、插件化、极速开发CRUD方便快速构建迭代后台管理系统支持serverless、docker、普通服务器等多种方式部署
<a href="https://cool-js.com" target="_blank">官网</a> 进一步了解。
<p align="center">
<a href="https://github.com/cool-team-official/cool-admin-midway/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="GitHub license" />
<a href=""><img src="https://img.shields.io/github/package-json/v/cool-team-official/cool-admin-midway?style=flat-square" alt="GitHub tag"></a>
<img src="https://img.shields.io/github/last-commit/cool-team-official/cool-admin-midway?style=flat-square" alt="GitHub tag"></a>
</p>
## 技术栈
- 后端:**`node.js` `midway.js` `koa.js` `typescript`**
- 前端:**`vue.js` `element-plus` `jsx` `pinia` `vue-router`**
- 数据库:**`mysql` `postgresql` `sqlite`**
如果你是前端,后端的这些技术选型对你是特别友好的,前端开发者可以较快速地上手。
如果你是后端Typescript 的语法又跟 java、php 等特别类似,一切看起来也是那么得熟悉。
如果你想使用java版本后端请移步[cool-admin-java](https://cool-js.com/admin/java/introduce.html)
#### 官网
[https://cool-js.com](https://cool-js.com)
<!-- 在此次添加使用文档 -->
## 演示
[AI极速编码](https://cool-js.com/ai/introduce.html)
[https://show.cool-admin.com](https://show.cool-admin.com)
- 账户admin
- 密码123456
<img src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/home-mini.png" alt="Admin Home"></a>
#### 项目前端
[https://github.com/cool-team-official/cool-admin-vue](https://github.com/cool-team-official/cool-admin-vue)
## 微信群
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/wechat.jpeg?v=1" alt="Admin Wechat"></a>
## 运行
#### 修改数据库配置,配置文件位于`src/config/config.local.ts`
以Mysql为例其他数据库请参考[数据库配置文档](https://cool-js.com/admin/node/quick.html#%E6%95%B0%E6%8D%AE%E5%BA%93%E9%85%8D%E7%BD%AE)
Mysql(`>=5.7版本`),建议 8.0node 版本(`>=16.x`),建议 18.x首次启动会自动初始化并导入数据
```ts
// mysql驱动已经内置无需安装
typeorm: {
dataSource: {
default: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'root',
password: '123456',
database: 'cool',
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
synchronize: true,
// 打印日志
logging: false,
// 字符集
charset: 'utf8mb4',
// 是否开启缓存
cache: true,
// 实体路径
entities: ['**/modules/*/entity'],
},
},
},
```
#### 安装依赖并运行
```bash
$ npm i
$ npm run dev
$ open http://localhost:8001/
```
注: `npm i`如果安装失败可以尝试使用[cnpm](https://developer.aliyun.com/mirror/NPM?from=tnpm),或者切换您的镜像源,推荐使用[pnpm](https://pnpm.io/)
## CURD(快速增删改查)
大部分的后台管理系统,或者 API 服务都是对数据进行管理,所以可以看到大量的 CRUD 场景(增删改查)cool-admin 对此进行了大量地封装,让这块的编码量变得极其地少。
#### 新建一个数据表
`src/modules/demo/entity/goods.ts`,项目启动数据库会自动创建该表,无需手动创建
```ts
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity, Index } from 'typeorm';
/**
* 商品
*/
@Entity('demo_app_goods')
export class DemoAppGoodsEntity extends BaseEntity {
@Column({ comment: '标题' })
title: string;
@Column({ comment: '图片' })
pic: string;
@Column({ comment: '价格', type: 'decimal', precision: 5, scale: 2 })
price: number;
}
```
#### 编写 api 接口
`src/modules/demo/controller/app/goods.ts`,快速编写 6 个 api 接口
```ts
import { CoolController, BaseController } from '@cool-midway/core';
import { DemoAppGoodsEntity } from '../../entity/goods';
/**
* 商品
*/
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: DemoAppGoodsEntity,
})
export class DemoAppGoodsController extends BaseController {
/**
* 其他接口
*/
@Get('/other')
async other() {
return this.ok('hello, cool-admin!!!');
}
}
```
这样我们就完成了 6 个接口的编写,对应的接口如下:
- `POST /app/demo/goods/add` 新增
- `POST /app/demo/goods/delete` 删除
- `POST /app/demo/goods/update` 更新
- `GET /app/demo/goods/info` 单个信息
- `POST /app/demo/goods/list` 列表信息
- `POST /app/demo/goods/page` 分页查询(包含模糊查询、字段全匹配等)
### 部署
[部署教程](https://cool-js.com/admin/node/other/deploy.html)
### 内置指令
- 使用 `npm run lint` 来做代码风格检查。
[midway]: https://midwayjs.org
### 低价服务器
[阿里云、腾讯云、华为云低价云服务器,不限新老](https://cool-js.com/ad/server.html)

2
bootstrap.js vendored Normal file
View File

@ -0,0 +1,2 @@
const { Bootstrap } = require('@midwayjs/bootstrap');
Bootstrap.run();

36
docker-compose.yml Normal file
View File

@ -0,0 +1,36 @@
# 本地数据库环境
# 数据存放在当前目录下的 data里
# 推荐使用安装了docker扩展的vscode打开目录 在本文件上右键可以快速启动,停止
# 如不需要相关容器开机自启动,可注释掉 restart: always
# 如遇端口冲突 可调整ports下 :前面的端口号
version: "3.1"
services:
coolDB:
image: mysql
command:
--default-authentication-plugin=mysql_native_password
--sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
--group_concat_max_len=102400
restart: always
volumes:
- ./data/mysql/:/var/lib/mysql/
environment:
TZ: Asia/Shanghai # 指定时区
MYSQL_ROOT_PASSWORD: "123456" # 配置root用户密码
MYSQL_DATABASE: "cool" # 业务库名
MYSQL_USER: "root" # 业务库用户名
MYSQL_PASSWORD: "123456" # 业务库密码
ports:
- 3306:3306
coolRedis:
image: redis
#command: --requirepass "12345678" # redis库密码,不需要密码注释本行
restart: always
environment:
TZ: Asia/Shanghai # 指定时区
volumes:
- ./data/redis/:/data/
ports:
- 6379:6379

6
jest.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
};

83
package.json Normal file
View File

@ -0,0 +1,83 @@
{
"name": "cool-admin",
"version": "7.1.0",
"description": "一个项目用COOL就够了",
"private": true,
"dependencies": {
"@cool-midway/core": "^7.1.19",
"@cool-midway/rpc": "^7.0.0",
"@cool-midway/task": "^7.0.0",
"@midwayjs/bootstrap": "^3.15.0",
"@midwayjs/cache-manager": "^3.15.5",
"@midwayjs/core": "^3.15.0",
"@midwayjs/cron": "^3.15.2",
"@midwayjs/cross-domain": "^3.15.2",
"@midwayjs/decorator": "^3.15.0",
"@midwayjs/info": "^3.15.2",
"@midwayjs/koa": "^3.15.2",
"@midwayjs/logger": "^3.3.0",
"@midwayjs/socketio": "^3.15.2",
"@midwayjs/static-file": "^3.15.2",
"@midwayjs/typeorm": "^3.15.2",
"@midwayjs/upload": "^3.15.2",
"@midwayjs/validate": "^3.15.2",
"@midwayjs/view-ejs": "^3.15.2",
"@socket.io/redis-adapter": "^8.3.0",
"axios": "^1.6.8",
"bignumber.js": "^9.1.2",
"cache-manager-ioredis-yet": "^2.0.2",
"decompress": "^4.2.1",
"download": "^8.0.0",
"ioredis": "^5.3.2",
"ipip-ipdb": "^0.6.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"mini-svg-data-uri": "^1.4.4",
"moment": "^2.30.1",
"mysql2": "^3.9.2",
"semver": "^7.6.0",
"svg-captcha": "^1.4.0",
"svg2png-wasm": "^1.4.1",
"typeorm": "^0.3.20",
"uuid": "^9.0.1",
"ws": "^8.16.0"
},
"devDependencies": {
"@midwayjs/cli": "^2.1.1",
"@midwayjs/mock": "^3.15.2",
"@types/jest": "^29.5.12",
"@types/koa": "^2.15.0",
"@types/node": "20",
"cross-env": "^7.0.3",
"jest": "^29.7.0",
"mwts": "^1.3.0",
"mwtsc": "^1.7.2",
"ts-jest": "^29.1.2",
"typescript": "~5.4.2"
},
"engines": {
"node": ">=12.0.0"
},
"scripts": {
"start": "NODE_ENV=production node ./bootstrap.js",
"dev": "cross-env && cross-env NODE_ENV=local TS_NODE_TYPE_CHECK=false TS_NODE_TRANSPILE_ONLY=true midway-bin dev --ts",
"cov": "midway-bin cov --ts",
"lint": "mwts check",
"lint:fix": "mwts fix",
"ci": "npm run cov",
"build": "midway-bin build -c",
"pm2:start": "pm2 start ./bootstrap.js -i max --name cool-admin",
"pm2:stop": "pm2 stop cool-admin & pm2 delete cool-admin"
},
"midway-bin-clean": [
".vscode/.tsbuildinfo",
"dist"
],
"repository": {
"type": "git",
"url": "https://cool-js.com"
},
"author": "COOL",
"license": "MIT"
}

89
public/css/welcome.css Normal file
View File

@ -0,0 +1,89 @@
body {
display: flex;
height: 100vh;
justify-content: center;
align-items: center;
text-align: center;
background: #222;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
color: #6ee1f5;
padding: 10px 0 20px 0;
text-align: center;
opacity: 0; /* 开始时隐藏 */
animation: fadeIn 5s forwards; /* 应用动画 */
}
.link {
color: #6ee1f5;
}
.reveal {
position: relative;
display: flex;
color: #6ee1f5;
font-size: 2em;
font-family: Raleway, sans-serif;
letter-spacing: 3px;
text-transform: uppercase;
white-space: pre;
}
.reveal span {
opacity: 0;
transform: scale(0);
animation: fadeIn 2.4s forwards;
}
.reveal::before, .reveal::after {
position: absolute;
content: "";
top: 0;
bottom: 0;
width: 2px;
height: 100%;
background: white;
opacity: 0;
transform: scale(0);
}
.reveal::before {
left: 50%;
animation: slideLeft 1.5s cubic-bezier(0.7, -0.6, 0.3, 1.5) forwards;
}
.reveal::after {
right: 50%;
animation: slideRight 1.5s cubic-bezier(0.7, -0.6, 0.3, 1.5) forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideLeft {
to {
left: -6%;
opacity: 1;
transform: scale(0.9);
}
}
@keyframes slideRight {
to {
right: -6%;
opacity: 1;
transform: scale(0.9);
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

14
public/js/welcome.js Normal file
View File

@ -0,0 +1,14 @@
const duration = 0.8;
const delay = 0.3;
// eslint-disable-next-line no-undef
const revealText = document.querySelector('.reveal');
const letters = revealText.textContent.split('');
revealText.textContent = '';
const middle = letters.filter(e => e !== ' ').length / 2;
letters.forEach((letter, i) => {
// eslint-disable-next-line no-undef
const span = document.createElement('span');
span.textContent = letter;
span.style.animationDelay = `${delay + Math.abs(i - middle) * 0.1}s`;
revealText.append(span);
});

BIN
src/comm/ipipfree.ipdb Normal file

Binary file not shown.

161
src/comm/utils.ts Normal file
View File

@ -0,0 +1,161 @@
import { Inject, Provide } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
import * as ipdb from 'ipip-ipdb';
import * as _ from 'lodash';
import * as moment from 'moment';
/**
*
*/
@Provide()
export class Utils {
@Inject()
baseDir;
/**
* IP
*/
async getReqIP(ctx: Context) {
const req = ctx.req;
return (
req.headers['x-forwarded-for'] ||
req.socket.remoteAddress.replace('::ffff:', '')
);
}
/**
* IP获得请求地址
* @param ip IP地址
*/
async getIpAddr(ctx: Context, ip?: string | string[]) {
try {
if (!ip) {
ip = await this.getReqIP(ctx);
}
const bst = new ipdb.BaseStation(`${this.baseDir}/comm/ipipfree.ipdb`);
const result = bst.findInfo(ip, 'CN');
const addArr: any = [];
if (result) {
addArr.push(result.countryName);
addArr.push(result.regionName);
addArr.push(result.cityName);
return _.uniq(addArr).join('');
}
} catch (err) {
return '无法获取地址信息';
}
}
/**
*
* @param obj
*/
async removeEmptyP(obj) {
Object.keys(obj).forEach(key => {
if (obj[key] === null || obj[key] === '' || obj[key] === 'undefined') {
delete obj[key];
}
});
}
/**
* 线
* @param ms
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
*
* @param recently
*/
getRecentlyDates(recently, format = 'YYYY-MM-DD') {
moment.locale('zh-cn');
const dates = [];
for (let i = 0; i < recently; i++) {
dates.push(moment().subtract(i, 'days').format(format));
}
return dates.reverse();
}
/**
*
* @param recently
*/
getRecentlyMonths(recently, format = 'YYYY-MM') {
moment.locale('zh-cn');
const dates = [];
const date = moment(Date.now()).format('YYYY-MM');
for (let i = 0; i < recently; i++) {
dates.push(moment(date).subtract(i, 'months').format(format));
}
return dates.reverse();
}
/**
*
* @param start
* @param end
*/
getBetweenDays(start, end, format = 'YYYY-MM-DD') {
moment.locale('zh-cn');
const dates = [];
const startTime = moment(start).format(format);
const endTime = moment(end).format(format);
const days = moment(endTime).diff(moment(startTime), 'days');
for (let i = 0; i <= days; i++) {
dates.push(moment(startTime).add(i, 'days').format(format));
}
return dates;
}
/**
*
* @param start
* @param end
*/
getBetweenMonths(start, end, format = 'YYYY-MM') {
moment.locale('zh-cn');
const dates = [];
const startTime = moment(start).format(format);
const endTime = moment(end).format(format);
const months = moment(endTime).diff(moment(startTime), 'months');
for (let i = 0; i <= months; i++) {
dates.push(moment(startTime).add(i, 'months').format(format));
}
return dates;
}
/**
*
* @param start
* @param end
*/
getBetweenHours(start, end, format = 'YYYY-MM-DD HH') {
moment.locale('zh-cn');
const dates = [];
const startTime = moment(start).format(format);
const endTime = moment(end).format(format);
const hours = moment(endTime).diff(moment(startTime), 'hours');
for (let i = 0; i <= hours; i++) {
dates.push(moment(startTime).add(i, 'hours').format(format));
}
return dates;
}
/**
*
* @param obj
* @returns
*/
toCamelCase(obj) {
let camelCaseObject = {};
for (let i in obj) {
let camelCase = i.replace(/([-_][a-z])/gi, $1 => {
return $1.toUpperCase().replace('-', '').replace('_', '');
});
camelCaseObject[camelCase] = obj[i];
}
return camelCaseObject;
}
}

View File

@ -0,0 +1,72 @@
import { CoolConfig } from '@cool-midway/core';
import { MidwayConfig } from '@midwayjs/core';
import { CoolCacheStore } from '@cool-midway/core';
// redis缓存
// import { redisStore } from 'cache-manager-ioredis-yet';
export default {
// use for cookie sign key, should change to your own and keep security
keys: 'cool-admin-keys-xxxxxx',
koa: {
port: 8001,
},
// 模板渲染
view: {
mapping: {
'.html': 'ejs',
},
},
// 静态文件配置
staticFile: {
buffer: true,
},
// 文件上传
upload: {
fileSize: '200mb',
whitelist: null,
},
// 缓存 可切换成其他缓存如redis http://www.midwayjs.org/docs/extensions/caching
cacheManager: {
clients: {
default: {
store: CoolCacheStore,
options: {
path: 'cache',
ttl: 0,
},
},
},
},
// cacheManager: {
// clients: {
// default: {
// store: redisStore,
// options: {
// port: 6379,
// host: '127.0.0.1',
// password: '',
// ttl: 0,
// db: 0,
// },
// },
// },
// },
cool: {
redis: {
host: '127.0.0.1',
password: '',
port: 6379,
db: 10,
},
// 已经插件化,本地文件上传查看 plugin/config.ts其他云存储查看对应插件的使用
file: {},
// crud配置
crud: {
// 插入模式save不会校验字段(允许传入不存在的字段)insert会校验字段
upsert: 'save',
// 软删除
softDelete: true,
},
} as CoolConfig,
} as MidwayConfig;

View File

@ -0,0 +1,38 @@
import { CoolConfig } from '@cool-midway/core';
import { MidwayConfig } from '@midwayjs/core';
/**
* npm run dev
*/
export default {
typeorm: {
dataSource: {
default: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'root',
password: '123456',
database: 'mall',
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
synchronize: true,
// 打印日志
logging: false,
// 字符集
charset: 'utf8mb4',
// 是否开启缓存
cache: true,
// 实体路径
entities: ['**/modules/*/entity'],
},
},
},
cool: {
// 实体与路径跟生成代码、前端请求、swagger文档相关 注意:线上不建议开启,以免暴露敏感信息
eps: true,
// 是否自动导入模块数据库
initDB: true,
// 是否自动导入模块菜单
initMenu: true,
} as CoolConfig,
} as MidwayConfig;

50
src/config/config.prod.ts Normal file
View File

@ -0,0 +1,50 @@
import { CoolConfig } from '@cool-midway/core';
import { MidwayConfig } from '@midwayjs/core';
import { createAdapter } from '@socket.io/redis-adapter';
import Redis from 'ioredis';
const redis = {
host: '127.0.0.1',
port: 6379,
password: '',
db: 0,
};
const pubClient = new Redis(redis);
const subClient = pubClient.duplicate();
/**
* npm run prod
*/
export default {
socketIO: {
upgrades: ['websocket'], // 可升级的协议
adapter: createAdapter(pubClient, subClient),
},
typeorm: {
dataSource: {
default: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'root',
password: '123456',
database: 'mall',
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
synchronize: false,
// 打印日志
logging: false,
// 字符集
charset: 'utf8mb4',
// 是否开启缓存
cache: true,
// 实体路径
entities: ['**/modules/*/entity'],
},
},
},
cool: {
// 是否自动导入数据库,生产环境不建议开,用本地的数据库手动初始化
initDB: false,
} as CoolConfig,
} as MidwayConfig;

63
src/configuration.ts Normal file
View File

@ -0,0 +1,63 @@
import * as orm from '@midwayjs/typeorm';
import { Configuration, App, Inject } from '@midwayjs/decorator';
import * as koa from '@midwayjs/koa';
import * as validate from '@midwayjs/validate';
import * as info from '@midwayjs/info';
import { join } from 'path';
import * as view from '@midwayjs/view-ejs';
import * as staticFile from '@midwayjs/static-file';
import * as cron from '@midwayjs/cron';
// import * as crossDomain from '@midwayjs/cross-domain';
import * as cool from '@cool-midway/core';
import { ILogger } from '@midwayjs/logger';
import * as upload from '@midwayjs/upload';
import * as socketio from '@midwayjs/socketio';
import { IMidwayApplication } from '@midwayjs/core';
// import * as swagger from '@midwayjs/swagger';
// import * as rpc from '@cool-midway/rpc';
import * as task from '@cool-midway/task';
@Configuration({
imports: [
// https://koajs.com/
koa,
// 是否开启跨域(注:顺序不能乱放!!!) http://www.midwayjs.org/docs/extensions/cross_domain
// crossDomain,
// 模板渲染 https://midwayjs.org/docs/extensions/render
view,
// 静态文件托管 https://midwayjs.org/docs/extensions/static_file
staticFile,
// orm https://midwayjs.org/docs/extensions/orm
orm,
// 参数验证 https://midwayjs.org/docs/extensions/validate
validate,
// 本地任务 http://www.midwayjs.org/docs/extensions/cron
cron,
// 文件上传
upload,
// cool-admin 官方组件 https://cool-js.com
cool,
// rpc 微服务 远程调用
// rpc,
// 任务与队列
task,
// swagger 文档 http://www.midwayjs.org/docs/extensions/swagger
// swagger,
// 即时通讯
socketio,
{
component: info,
enabledEnvironment: ['local'],
},
],
importConfigs: [join(__dirname, './config')],
})
export class ContainerLifeCycle {
@App()
app: IMidwayApplication;
@Inject()
logger: ILogger;
async onReady() {}
}

6
src/interface.ts Normal file
View File

@ -0,0 +1,6 @@
/**
* @description User-Service parameters
*/
export interface IUserOptions {
uid: number;
}

19
src/modules/app/config.ts Normal file
View File

@ -0,0 +1,19 @@
import { ModuleConfig } from '@cool-midway/core';
/**
*
*/
export default () => {
return {
// 模块名称
name: '应用管理',
// 模块描述
description: '投诉举报、意见反馈、更新升级、套餐设置等',
// 中间件,只对本模块有效
middlewares: [],
// 中间件,全局有效
globalMiddlewares: [],
// 模块加载顺序默认为0值越大越优先加载
order: 0,
} as ModuleConfig;
};

View File

@ -0,0 +1,32 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { AppComplainEntity } from '../../entity/complain';
import { BaseSysUserEntity } from '../../../base/entity/sys/user';
import { AppComplainService } from '../../service/complain';
import { UserInfoEntity } from '../../../user/entity/info';
/**
*
*/
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: AppComplainEntity,
service: AppComplainService,
pageQueryOp: {
keyWordLikeFields: ['a.contact', 'b.nickName', 'c.name'],
select: ['a.*', 'b.nickName', 'b.avatarUrl', 'c.name as handlerName'],
fieldEq: ['a.status', 'a.type'],
join: [
{
entity: UserInfoEntity,
alias: 'b',
condition: 'a.userId = b.id',
},
{
entity: BaseSysUserEntity,
alias: 'c',
condition: 'a.handlerId = c.id',
},
],
},
})
export class AdminAppComplainController extends BaseController {}

View File

@ -0,0 +1,32 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { AppFeedbackEntity } from '../../entity/feedback';
import { UserInfoEntity } from '../../../user/entity/info';
import { BaseSysUserEntity } from '../../../base/entity/sys/user';
import { AppFeedbackService } from '../../service/feedback';
/**
*
*/
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: AppFeedbackEntity,
service: AppFeedbackService,
pageQueryOp: {
keyWordLikeFields: ['a.contact', 'b.nickName', 'c.name'],
select: ['a.*', 'b.nickName', 'b.avatarUrl', 'c.name as handlerName'],
fieldEq: ['a.status'],
join: [
{
entity: UserInfoEntity,
alias: 'b',
condition: 'a.userId = b.id',
},
{
entity: BaseSysUserEntity,
alias: 'c',
condition: 'a.handlerId = c.id',
},
],
},
})
export class AdminAppFeedbackController extends BaseController {}

View File

@ -0,0 +1,15 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { AppGoodsEntity } from '../../entity/goods';
/**
*
*/
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: AppGoodsEntity,
pageQueryOp: {
keyWordLikeFields: ['a.title'],
fieldEq: ['a.status', 'a.type'],
},
})
export class AdminAppGoodsController extends BaseController {}

View File

@ -0,0 +1,17 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { AppVersionEntity } from '../../entity/version';
import { AppVersionService } from '../../service/version';
/**
*
*/
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: AppVersionEntity,
service: AppVersionService,
pageQueryOp: {
keyWordLikeFields: ['a.name', 'a.version'],
fieldEq: ['a.status', 'a.type'],
},
})
export class AdminAppVersionController extends BaseController {}

View File

@ -0,0 +1,38 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { Body, Inject, Post } from '@midwayjs/core';
import { AppComplainService } from '../../service/complain';
import { AppComplainEntity } from '../../entity/complain';
/**
*
*/
@CoolController({
api: ['page', 'info'],
entity: AppComplainEntity,
insertParam: ctx => {
return {
userId: ctx.user.id,
};
},
pageQueryOp: {
fieldEq: ['a.type'],
where: ctx => {
const userId = ctx.user.id;
return [['a.userId = :userId', { userId }]];
},
},
})
export class AppAppComplainController extends BaseController {
@Inject()
appComplainService: AppComplainService;
@Inject()
ctx;
@Post('/submit', { summary: '提交投诉举报' })
async submit(@Body() info) {
info.userId = this.ctx.user.id;
await this.appComplainService.submit(info);
return this.ok();
}
}

View File

@ -0,0 +1,38 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { Body, Inject, Post } from '@midwayjs/core';
import { AppFeedbackService } from '../../service/feedback';
import { AppFeedbackEntity } from '../../entity/feedback';
/**
*
*/
@CoolController({
api: ['page', 'info'],
entity: AppFeedbackEntity,
insertParam: ctx => {
return {
userId: ctx.user.id,
};
},
pageQueryOp: {
fieldEq: ['a.type'],
where: ctx => {
const userId = ctx.user.id;
return [['a.userId = :userId', { userId }]];
},
},
})
export class AppAppFeedbackController extends BaseController {
@Inject()
appFeedbackService: AppFeedbackService;
@Inject()
ctx;
@Post('/submit', { summary: '提交意见反馈' })
async submit(@Body() info) {
info.userId = this.ctx.user.id;
await this.appFeedbackService.submit(info);
return this.ok();
}
}

View File

@ -0,0 +1,19 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { AppGoodsEntity } from '../../entity/goods';
/**
*
*/
@CoolController({
api: ['list'],
entity: AppGoodsEntity,
listQueryOp: {
addOrderBy: {
sort: 'ASC',
},
where: () => {
return [['a.status = :status', { status: 1 }]];
},
},
})
export class AppAppGoodsController extends BaseController {}

View File

@ -0,0 +1,30 @@
import {
CoolController,
BaseController,
CoolUrlTag,
CoolTag,
TagTypes,
} from '@cool-midway/core';
import { Get, Inject, Query } from '@midwayjs/core';
import { AppVersionService } from '../../service/version';
import { AppVersionEntity } from '../../entity/version';
/**
*
*/
@CoolUrlTag()
@CoolController({
api: [],
entity: AppVersionEntity,
})
export class AppAppVersionController extends BaseController {
@Inject()
appVersionService: AppVersionService;
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/check', { summary: '检查版本' })
async check(@Query('version') version: string, @Query('type') type = 0) {
const result = await this.appVersionService.check(version, type);
return this.ok(result);
}
}

124
src/modules/app/db.json Normal file
View File

@ -0,0 +1,124 @@
{
"dict_type": [
{
"name": "升级类型",
"key": "upgradeType",
"@childDatas": {
"dict_info": [
{
"typeId": "@id",
"name": "安卓",
"orderNum": 1,
"remark": null,
"parentId": null,
"value": "0"
},
{
"typeId": "@id",
"name": "IOS",
"orderNum": 1,
"remark": null,
"parentId": null,
"value": "1"
}
]
}
},
{
"name": "投诉类型",
"key": "complainType",
"@childDatas": {
"dict_info": [
{
"typeId": "@id",
"name": "崩溃与错误",
"orderNum": 1,
"remark": null,
"parentId": null,
"value": "0"
},
{
"typeId": "@id",
"name": "支付问题",
"orderNum": 1,
"remark": null,
"parentId": null,
"value": "1"
},
{
"typeId": "@id",
"name": "体验不佳",
"orderNum": 1,
"remark": null,
"parentId": null,
"value": "2"
},
{
"typeId": "@id",
"name": "功能缺失",
"orderNum": 1,
"remark": null,
"parentId": null,
"value": "3"
},
{
"typeId": "@id",
"name": "其他",
"orderNum": 1,
"remark": null,
"parentId": null,
"value": "4"
}
]
}
},
{
"name": "反馈类型",
"key": "feedbackType",
"@childDatas": {
"dict_info": [
{
"typeId": "@id",
"name": "崩溃与错误",
"value": "0",
"orderNum": 1,
"remark": null,
"parentId": null
},
{
"typeId": "@id",
"name": "支付问题",
"value": "1",
"orderNum": 1,
"remark": null,
"parentId": null
},
{
"typeId": "@id",
"name": "体验不佳",
"value": "2",
"orderNum": 1,
"remark": null,
"parentId": null
},
{
"typeId": "@id",
"name": "功能缺失",
"value": "3",
"orderNum": 1,
"remark": null,
"parentId": null
},
{
"typeId": "@id",
"name": "其他",
"value": "-1",
"orderNum": 1,
"remark": null,
"parentId": null
}
]
}
}
]
}

View File

@ -0,0 +1,33 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity, Index } from 'typeorm';
/**
*
*/
@Entity('app_complain')
export class AppComplainEntity extends BaseEntity {
@Index()
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '类型' })
type: number;
@Column({ comment: '联系方式' })
contact: string;
@Column({ comment: '内容' })
content: string;
@Column({ comment: '图片', type: 'json', nullable: true })
images: string[];
@Column({ comment: '状态 0-未处理 1-已处理', default: 0 })
status: number;
@Column({ comment: '处理人ID', nullable: true })
handlerId: number;
@Column({ comment: '备注', type: 'text', nullable: true })
remark: string;
}

View File

@ -0,0 +1,33 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity, Index } from 'typeorm';
/**
*
*/
@Entity('app_feedback')
export class AppFeedbackEntity extends BaseEntity {
@Index()
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '联系方式' })
contact: string;
@Column({ comment: '类型' })
type: number;
@Column({ comment: '内容' })
content: string;
@Column({ comment: '图片', type: 'json', nullable: true })
images: string[];
@Column({ comment: '状态 0-未处理 1-已处理', default: 0 })
status: number;
@Column({ comment: '处理人ID', nullable: true })
handlerId: number;
@Column({ comment: '备注', type: 'text', nullable: true })
remark: string;
}

View File

@ -0,0 +1,48 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('app_goods')
export class AppGoodsEntity extends BaseEntity {
@Column({ comment: '标题' })
title: string;
@Column({
comment: '价格',
type: 'decimal',
precision: 12,
scale: 2,
})
price: number;
@Column({
comment: '原价',
type: 'decimal',
precision: 12,
scale: 2,
})
originalPrice: number;
@Column({ comment: '描述', type: 'text', nullable: true })
description: string;
@Column({ comment: '状态 0-禁用 1-启用', default: 1 })
status: number;
@Column({ comment: '排序', default: 0 })
sort: number;
@Column({ comment: '类型 0-天 1-月 2-年 3-永久', default: 0 })
type: number;
@Column({ comment: '时长', default: 1 })
duration: number;
@Column({ comment: '标签', nullable: true })
tag: string;
@Column({ comment: '标签颜色', default: '#26A7FD' })
tagColor: string;
}

View File

@ -0,0 +1,32 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('app_version')
export class AppVersionEntity extends BaseEntity {
@Column({ comment: '名称' })
name: string;
@Column({ comment: '版本号' })
version: string;
@Column({ comment: '类型', default: 0 })
type: number;
@Column({ comment: '下载地址' })
url: string;
@Column({ comment: '强制更新 0-否 1-是', default: 0 })
forceUpdate: number;
@Column({ comment: '状态 0-禁用 1-启用', default: 1 })
status: number;
@Column({ comment: '热更新 0-否 1-是', default: 0 })
hotUpdate: number;
@Column({ comment: '描述', type: 'text' })
description: string;
}

270
src/modules/app/menu.json Normal file
View File

@ -0,0 +1,270 @@
[
{
"name": "应用管理",
"router": null,
"perms": null,
"type": 0,
"icon": "icon-app",
"orderNum": 4,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "版本管理",
"router": "/app/version",
"perms": null,
"type": 1,
"icon": "icon-tag",
"orderNum": 0,
"viewPath": "modules/app/views/version.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "删除",
"router": null,
"perms": "app:version:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "修改",
"router": null,
"perms": "app:version:update,app:version:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "单个信息",
"router": null,
"perms": "app:version:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "列表查询",
"router": null,
"perms": "app:version:list",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "分页查询",
"router": null,
"perms": "app:version:page",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "新增",
"router": null,
"perms": "app:version:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
},
{
"name": "意见反馈",
"router": "/app/feedback",
"perms": null,
"type": 1,
"icon": "icon-info",
"orderNum": 0,
"viewPath": "modules/app/views/feedback.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "删除",
"router": null,
"perms": "app:feedback:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "修改",
"router": null,
"perms": "app:feedback:update,app:feedback:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "单个信息",
"router": null,
"perms": "app:feedback:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "列表查询",
"router": null,
"perms": "app:feedback:list",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "分页查询",
"router": null,
"perms": "app:feedback:page",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "新增",
"router": null,
"perms": "app:feedback:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
},
{
"name": "投诉举报",
"router": "/app/complain",
"perms": null,
"type": 1,
"icon": "icon-new",
"orderNum": 0,
"viewPath": "modules/app/views/complain.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "删除",
"router": null,
"perms": "app:complain:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "修改",
"router": null,
"perms": "app:complain:update,app:complain:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "单个信息",
"router": null,
"perms": "app:complain:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "列表查询",
"router": null,
"perms": "app:complain:list",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "分页查询",
"router": null,
"perms": "app:complain:page",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "新增",
"router": null,
"perms": "app:complain:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
}
]
}
]

View File

@ -0,0 +1,5 @@
{
"dependencies": {
"semver": "^7.5.4"
}
}

View File

@ -0,0 +1,36 @@
import { Inject, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { AppComplainEntity } from '../entity/complain';
/**
*
*/
@Provide()
export class AppComplainService extends BaseService {
@InjectEntityModel(AppComplainEntity)
appComplainEntity: Repository<AppComplainEntity>;
@Inject()
ctx;
/**
*
* @param info
*/
async submit(info: AppComplainEntity) {
await this.appComplainEntity.insert(info);
}
/**
*
* @param param
*/
async update(param: AppComplainEntity) {
if (param.status == 1) {
param.handlerId = this.ctx.admin.userId;
}
await super.update(param);
}
}

View File

@ -0,0 +1,36 @@
import { Inject, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { AppFeedbackEntity } from '../entity/feedback';
/**
*
*/
@Provide()
export class AppFeedbackService extends BaseService {
@InjectEntityModel(AppFeedbackEntity)
appFeedbackEntity: Repository<AppFeedbackEntity>;
@Inject()
ctx;
/**
*
* @param info
*/
async submit(info: AppFeedbackEntity) {
await this.appFeedbackEntity.insert(info);
}
/**
*
* @param param
*/
async update(param: AppFeedbackEntity) {
if (param.status == 1) {
param.handlerId = this.ctx.admin.userId;
}
await super.update(param);
}
}

View File

@ -0,0 +1,45 @@
import { Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Not, Repository } from 'typeorm';
import { AppVersionEntity } from '../entity/version';
import * as semver from 'semver';
/**
*
*/
@Provide()
export class AppVersionService extends BaseService {
@InjectEntityModel(AppVersionEntity)
appVersionEntity: Repository<AppVersionEntity>;
/**
*
* @param version
*/
async check(version: string, type = 0) {
const info = await this.appVersionEntity.findOneBy({ type, status: 1 });
if (info && semver.gt(info.version, version)) {
return info;
}
return;
}
/**
*
* @param data
* @param type
*/
async modifyAfter(data: any, type: 'add' | 'update' | 'delete') {
if (type == 'add' || type == 'update') {
const info = await this.appVersionEntity.findOneBy({ id: data.id });
if (info.status == 1) {
// 将其他的版本设置为禁用
await this.appVersionEntity.update(
{ type: info.type, id: Not(info.id) },
{ status: 0 }
);
}
}
}
}

View File

@ -0,0 +1,35 @@
import { BaseLogMiddleware } from './middleware/log';
import { BaseAuthorityMiddleware } from './middleware/authority';
import { ModuleConfig } from '@cool-midway/core';
/**
*
*/
export default () => {
return {
// 模块名称
name: '权限管理',
// 模块描述
description: '基础的权限管理功能,包括登录,权限校验',
// 中间件
globalMiddlewares: [BaseAuthorityMiddleware, BaseLogMiddleware],
// 模块加载顺序默认为0值越大越优先加载
order: 10,
// app参数配置允许读取的key
allowKeys: ['privacyPolicy', 'userAgreement'],
// jwt 生成解密token的
jwt: {
// 单点登录
sso: false,
// 注意: 最好重新修改,防止破解
secret: 'f7ad2d70e28a11eeb7e2214c12a8b138',
// token
token: {
// 2小时过期需要用刷新token
expire: 2 * 3600,
// 15天内如果没操作过就需要重新登录
refreshExpire: 24 * 3600 * 15,
},
},
} as ModuleConfig;
};

View File

@ -0,0 +1,86 @@
import { Provide, Inject, Get, Post, Body, ALL } from '@midwayjs/decorator';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysUserEntity } from '../../entity/sys/user';
import { BaseSysLoginService } from '../../service/sys/login';
import { BaseSysPermsService } from '../../service/sys/perms';
import { BaseSysUserService } from '../../service/sys/user';
import { Context } from '@midwayjs/koa';
import { PluginService } from '../../../plugin/service/info';
/**
* Base
*/
@Provide()
@CoolController()
export class BaseCommController extends BaseController {
@Inject()
baseSysUserService: BaseSysUserService;
@Inject()
baseSysPermsService: BaseSysPermsService;
@Inject()
baseSysLoginService: BaseSysLoginService;
@Inject()
ctx: Context;
@Inject()
pluginService: PluginService;
/**
*
*/
@Get('/person', { summary: '个人信息' })
async person() {
return this.ok(
await this.baseSysUserService.person(this.ctx.admin?.userId)
);
}
/**
*
*/
@Post('/personUpdate', { summary: '修改个人信息' })
async personUpdate(@Body(ALL) user: BaseSysUserEntity) {
await this.baseSysUserService.personUpdate(user);
return this.ok();
}
/**
*
*/
@Get('/permmenu', { summary: '权限与菜单' })
async permmenu() {
return this.ok(
await this.baseSysPermsService.permmenu(this.ctx.admin.roleIds)
);
}
/**
*
*/
@Post('/upload', { summary: '文件上传' })
async upload() {
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.upload(this.ctx));
}
/**
*
*/
@Get('/uploadMode', { summary: '文件上传模式' })
async uploadMode() {
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.getMode());
}
/**
* 退
*/
@Post('/logout', { summary: '退出' })
async logout() {
await this.baseSysLoginService.logout();
return this.ok();
}
}

View File

@ -0,0 +1,99 @@
import { Provide, Body, Inject, Post, Get, Query } from '@midwayjs/decorator';
import {
CoolController,
BaseController,
CoolEps,
CoolUrlTag,
CoolTag,
TagTypes,
RESCODE,
} from '@cool-midway/core';
import { LoginDTO } from '../../dto/login';
import { BaseSysLoginService } from '../../service/sys/login';
import { BaseSysParamService } from '../../service/sys/param';
import { Context } from '@midwayjs/koa';
import { Validate } from '@midwayjs/validate';
/**
*
*/
@Provide()
@CoolController({ description: '开放接口' })
@CoolUrlTag()
export class BaseOpenController extends BaseController {
@Inject()
baseSysLoginService: BaseSysLoginService;
@Inject()
baseSysParamService: BaseSysParamService;
@Inject()
ctx: Context;
@Inject()
eps: CoolEps;
/**
*
* @returns
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/eps', { summary: '实体信息与路径' })
public async getEps() {
return this.ok(this.eps.admin);
}
/**
* key获得网页内容()
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/html', { summary: '获得网页内容的参数值' })
async htmlByKey(@Query('key') key: string) {
this.ctx.body = await this.baseSysParamService.htmlByKey(key);
}
/**
*
* @param login
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/login', { summary: '登录' })
@Validate()
async login(@Body() login: LoginDTO) {
return this.ok(await this.baseSysLoginService.login(login));
}
/**
*
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/captcha', { summary: '验证码' })
async captcha(
@Query('type') type: string,
@Query('width') width: number,
@Query('height') height: number,
@Query('color') color: string
) {
return this.ok(
await this.baseSysLoginService.captcha(type, width, height, color)
);
}
/**
* token
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/refreshToken', { summary: '刷新token' })
async refreshToken(@Query('refreshToken') refreshToken: string) {
try {
const token = await this.baseSysLoginService.refreshToken(refreshToken);
return this.ok(token);
} catch (e) {
this.ctx.status = 401;
this.ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效~',
};
}
}
}

View File

@ -0,0 +1,27 @@
import { ALL, Body, Inject, Post, Provide } from '@midwayjs/decorator';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysDepartmentEntity } from '../../../entity/sys/department';
import { BaseSysDepartmentService } from '../../../service/sys/department';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'list'],
entity: BaseSysDepartmentEntity,
service: BaseSysDepartmentService,
})
export class BaseDepartmentController extends BaseController {
@Inject()
baseDepartmentService: BaseSysDepartmentService;
/**
*
*/
@Post('/order', { summary: '排序' })
async order(@Body(ALL) params: any) {
await this.baseDepartmentService.order(params);
return this.ok();
}
}

View File

@ -0,0 +1,64 @@
import { Provide, Post, Inject, Body, Get } from '@midwayjs/decorator';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysLogEntity } from '../../../entity/sys/log';
import { BaseSysUserEntity } from '../../../entity/sys/user';
import { BaseSysConfService } from '../../../service/sys/conf';
import { BaseSysLogService } from '../../../service/sys/log';
/**
*
*/
@Provide()
@CoolController({
api: ['page'],
entity: BaseSysLogEntity,
urlTag: {
name: 'a',
url: ['add'],
},
pageQueryOp: {
keyWordLikeFields: ['b.name', 'a.params', 'a.ipAddr'],
select: ['a.*', 'b.name'],
join: [
{
entity: BaseSysUserEntity,
alias: 'b',
condition: 'a.userId = b.id',
type: 'leftJoin',
},
],
},
})
export class BaseSysLogController extends BaseController {
@Inject()
baseSysLogService: BaseSysLogService;
@Inject()
baseSysConfService: BaseSysConfService;
/**
*
*/
@Post('/clear', { summary: '清理' })
public async clear() {
await this.baseSysLogService.clear(true);
return this.ok();
}
/**
*
*/
@Post('/setKeep', { summary: '日志保存时间' })
public async setKeep(@Body('value') value: number) {
await this.baseSysConfService.updateVaule('logKeep', value);
return this.ok();
}
/**
*
*/
@Get('/getKeep', { summary: '获得日志保存时间' })
public async getKeep() {
return this.ok(await this.baseSysConfService.getValue('logKeep'));
}
}

View File

@ -0,0 +1,46 @@
import { Body, Inject, Post, Provide } from '@midwayjs/decorator';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysMenuEntity } from '../../../entity/sys/menu';
import { BaseSysMenuService } from '../../../service/sys/menu';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: BaseSysMenuEntity,
service: BaseSysMenuService,
})
export class BaseSysMenuController extends BaseController {
@Inject()
baseSysMenuService: BaseSysMenuService;
@Post('/parse', { summary: '解析' })
async parse(
@Body('entity') entity: string,
@Body('controller') controller: string,
@Body('module') module: string
) {
return this.ok(
await this.baseSysMenuService.parse(entity, controller, module)
);
}
@Post('/create', { summary: '创建代码' })
async create(@Body() body) {
await this.baseSysMenuService.create(body);
return this.ok();
}
@Post('/export', { summary: '导出' })
async export(@Body('ids') ids: number[]) {
return this.ok(await this.baseSysMenuService.export(ids));
}
@Post('/import', { summary: '导入' })
async import(@Body('menus') menus: any[]) {
await this.baseSysMenuService.import(menus);
return this.ok();
}
}

View File

@ -0,0 +1,34 @@
import { Get, Inject, Provide, Query } from '@midwayjs/decorator';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysParamEntity } from '../../../entity/sys/param';
import { BaseSysParamService } from '../../../service/sys/param';
import { Context } from '@midwayjs/koa';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'info', 'page'],
entity: BaseSysParamEntity,
service: BaseSysParamService,
pageQueryOp: {
keyWordLikeFields: ['name', 'keyName'],
fieldEq: ['dataType'],
},
})
export class BaseSysParamController extends BaseController {
@Inject()
baseSysParamService: BaseSysParamService;
@Inject()
ctx: Context;
/**
* key获得网页内容()
*/
@Get('/html', { summary: '获得网页内容的参数值' })
async htmlByKey(@Query('key') key: string) {
this.ctx.body = await this.baseSysParamService.htmlByKey(key);
}
}

View File

@ -0,0 +1,38 @@
import { Provide } from '@midwayjs/decorator';
import { CoolController, BaseController } from '@cool-midway/core';
import { Context } from 'vm';
import { BaseSysRoleEntity } from '../../../entity/sys/role';
import { BaseSysRoleService } from '../../../service/sys/role';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: BaseSysRoleEntity,
service: BaseSysRoleService,
// 新增的时候插入当前用户ID
insertParam: async (ctx: Context) => {
return {
userId: ctx.admin.userId,
};
},
pageQueryOp: {
keyWordLikeFields: ['a.name', 'a.label'],
where: async (ctx: Context) => {
const { userId, roleIds, username } = ctx.admin;
return [
// 超级管理员的角色不展示
['label != :label', { label: 'admin' }],
// 如果不是超管,只能看到自己新建的或者自己有的角色
[
'(userId=:userId or id in (:...roleIds))',
{ userId, roleIds },
username !== 'admin',
],
];
},
},
})
export class BaseSysRoleController extends BaseController {}

View File

@ -0,0 +1,30 @@
import { Body, Inject, Post, Provide } from '@midwayjs/decorator';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysUserEntity } from '../../../entity/sys/user';
import { BaseSysUserService } from '../../../service/sys/user';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: BaseSysUserEntity,
service: BaseSysUserService,
})
export class BaseSysUserController extends BaseController {
@Inject()
baseSysUserService: BaseSysUserService;
/**
*
*/
@Post('/move', { summary: '移动部门' })
async move(
@Body('departmentId') departmentId: number,
@Body('userIds') userIds: []
) {
await this.baseSysUserService.move(departmentId, userIds);
return this.ok();
}
}

View File

@ -0,0 +1 @@
这里写对外的api接口

View File

@ -0,0 +1,72 @@
import { Provide, Inject, Get, Post, Query, Config } from '@midwayjs/decorator';
import {
CoolController,
BaseController,
CoolEps,
TagTypes,
CoolUrlTag,
CoolTag,
} from '@cool-midway/core';
import { Context } from '@midwayjs/koa';
import { BaseSysParamService } from '../../service/sys/param';
import { PluginService } from '../../../plugin/service/info';
/**
*
*/
@CoolUrlTag()
@Provide()
@CoolController()
export class BaseAppCommController extends BaseController {
@Inject()
pluginService: PluginService;
@Inject()
ctx: Context;
@Config('module.base.allowKeys')
allowKeys: string[];
@Inject()
eps: CoolEps;
@Inject()
baseSysParamService: BaseSysParamService;
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/param', { summary: '参数配置' })
async param(@Query('key') key: string) {
if (!this.allowKeys.includes(key)) {
return this.fail('非法操作');
}
return this.ok(await this.baseSysParamService.dataByKey(key));
}
/**
*
* @returns
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/eps', { summary: '实体信息与路径' })
public async getEps() {
return this.ok(this.eps.app);
}
/**
*
*/
@Post('/upload', { summary: '文件上传' })
async upload() {
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.upload(this.ctx));
}
/**
*
*/
@Get('/uploadMode', { summary: '文件上传模式' })
async uploadMode() {
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.getMode());
}
}

103
src/modules/base/db.json Normal file
View File

@ -0,0 +1,103 @@
{
"base_sys_param": [
{
"keyName": "orderTimeout",
"name": "订单超时时间",
"data": "120",
"dataType": 0,
"remark": "订单超时未支付,自动关闭。单位:分钟"
},
{
"keyName": "orderConfirm",
"name": "自动确认收货",
"data": "15",
"dataType": 0,
"remark": "自动确认收货时间,从支付成功起算,单位:天"
},
{
"keyName": "userAgreement",
"name": "用户协议",
"data": "<h3 style=\"text-align: center;\"><strong>用户协议</strong></h3><p><br></p><p>xxxxxx</p>",
"dataType": 1,
"remark": null
},
{
"keyName": "privacyPolicy",
"name": "隐私政策",
"data": "<h3 style=\"text-align: center;\"><strong>隐私政策</strong></h3><p><br></p><p>xxxxxx</p>",
"dataType": 1,
"remark": null
}
],
"base_sys_conf": [
{
"cKey": "logKeep",
"cValue": "31"
},
{
"cKey": "recycleKeep",
"cValue": "31"
}
],
"base_sys_department": [
{
"id": 1,
"name": "COOL",
"parentId": null,
"orderNum": 0
},
{
"id": 11,
"name": "开发",
"parentId": 12,
"orderNum": 2
},
{
"id": 12,
"name": "测试",
"parentId": 1,
"orderNum": 1
},
{
"id": 13,
"name": "游客",
"parentId": 1,
"orderNum": 3
}
],
"base_sys_role": [
{
"id": 1,
"userId": "1",
"name": "超管",
"label": "admin",
"remark": "最高权限的角色",
"relevance": 1,
"menuIdList": "null",
"departmentIdList": "null"
}
],
"base_sys_user": [
{
"id": 1,
"departmentId": 1,
"name": "超级管理员",
"username": "admin",
"password": "e10adc3949ba59abbe56e057f20f883e",
"passwordV": 7,
"nickName": "管理员",
"headImg": "https://cool-js.com/admin/headimg.jpg",
"phone": "18000000000",
"email": "team@cool-js.com",
"status": 1,
"remark": "拥有最高权限的用户",
"socketId": null
}
],
"base_sys_user_role": [
{
"userId": 1,
"roleId": 1
}
]
}

View File

@ -0,0 +1,21 @@
import { Rule, RuleType } from '@midwayjs/validate';
/**
*
*/
export class LoginDTO {
// 用户名
@Rule(RuleType.string().required())
username: string;
// 密码
@Rule(RuleType.string().required())
password: string;
// 验证码ID
@Rule(RuleType.string().required())
captchaId: string;
// 验证码
@Rule(RuleType.required())
verifyCode: number;
}

View File

@ -0,0 +1,15 @@
import { Column, Index, Entity } from 'typeorm';
import { BaseEntity } from '@cool-midway/core';
/**
*
*/
@Entity('base_sys_conf')
export class BaseSysConfEntity extends BaseEntity {
@Index({ unique: true })
@Column({ comment: '配置键' })
cKey: string;
@Column({ comment: '配置值' })
cValue: string;
}

View File

@ -0,0 +1,19 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_department')
export class BaseSysDepartmentEntity extends BaseEntity {
@Column({ comment: '部门名称' })
name: string;
@Column({ comment: '上级部门ID', nullable: true })
parentId: number;
@Column({ comment: '排序', default: 0 })
orderNum: number;
// 父菜单名称
parentName: string;
}

View File

@ -0,0 +1,27 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Index, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_log')
export class BaseSysLogEntity extends BaseEntity {
@Index()
@Column({ comment: '用户ID', nullable: true })
userId: number;
@Index()
@Column({ comment: '行为' })
action: string;
@Index()
@Column({ comment: 'ip', nullable: true })
ip: string;
@Index()
@Column({ comment: 'ip地址', nullable: true, length: 50 })
ipAddr: string;
@Column({ comment: '参数', nullable: true, type: 'json' })
params: string;
}

View File

@ -0,0 +1,44 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_menu')
export class BaseSysMenuEntity extends BaseEntity {
@Column({ comment: '父菜单ID', nullable: true })
parentId: number;
@Column({ comment: '菜单名称' })
name: string;
@Column({ comment: '菜单地址', nullable: true })
router: string;
@Column({ comment: '权限标识', type: 'text', nullable: true })
perms: string;
@Column({
comment: '类型 0-目录 1-菜单 2-按钮',
default: 0,
})
type: number;
@Column({ comment: '图标', nullable: true })
icon: string;
@Column({ comment: '排序', default: 0 })
orderNum: number;
@Column({ comment: '视图地址', nullable: true })
viewPath: string;
@Column({ comment: '路由缓存', default: true })
keepAlive: boolean;
// 父菜单名称
parentName: string;
@Column({ comment: '是否显示', default: true })
isShow: boolean;
}

View File

@ -0,0 +1,27 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Index, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_param')
export class BaseSysParamEntity extends BaseEntity {
@Index({ unique: true })
@Column({ comment: '键' })
keyName: string;
@Column({ comment: '名称' })
name: string;
@Column({ comment: '数据', type: 'text' })
data: string;
@Column({
comment: '数据类型 0-字符串 1-富文本 2-文件 ',
default: 0,
})
dataType: number;
@Column({ comment: '备注', nullable: true })
remark: string;
}

View File

@ -0,0 +1,31 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Index, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_role')
export class BaseSysRoleEntity extends BaseEntity {
@Column({ comment: '用户ID' })
userId: string;
@Index({ unique: true })
@Column({ comment: '名称' })
name: string;
@Index({ unique: true })
@Column({ comment: '角色标签', nullable: true, length: 50 })
label: string;
@Column({ comment: '备注', nullable: true })
remark: string;
@Column({ comment: '数据权限是否关联上下级', default: false })
relevance: boolean;
@Column({ comment: '菜单权限', type: 'json' })
menuIdList: number[];
@Column({ comment: '部门权限', type: 'json' })
departmentIdList: number[];
}

View File

@ -0,0 +1,14 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_role_department')
export class BaseSysRoleDepartmentEntity extends BaseEntity {
@Column({ comment: '角色ID' })
roleId: number;
@Column({ comment: '部门ID' })
departmentId: number;
}

View File

@ -0,0 +1,14 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_role_menu')
export class BaseSysRoleMenuEntity extends BaseEntity {
@Column({ comment: '角色ID' })
roleId: number;
@Column({ comment: '菜单ID' })
menuId: number;
}

View File

@ -0,0 +1,54 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Index, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_user')
export class BaseSysUserEntity extends BaseEntity {
@Index()
@Column({ comment: '部门ID', nullable: true })
departmentId: number;
@Column({ comment: '姓名', nullable: true })
name: string;
@Index({ unique: true })
@Column({ comment: '用户名', length: 100 })
username: string;
@Column({ comment: '密码' })
password: string;
@Column({
comment: '密码版本, 作用是改完密码让原来的token失效',
default: 1,
})
passwordV: number;
@Column({ comment: '昵称', nullable: true })
nickName: string;
@Column({ comment: '头像', nullable: true })
headImg: string;
@Index()
@Column({ comment: '手机', nullable: true, length: 20 })
phone: string;
@Column({ comment: '邮箱', nullable: true })
email: string;
@Column({ comment: '备注', nullable: true })
remark: string;
@Column({ comment: '状态 0-禁用 1-启用', default: 1 })
status: number;
// 部门名称
departmentName: string;
// 角色ID列表
roleIdList: number[];
@Column({ comment: 'socketId', nullable: true })
socketId: string;
}

View File

@ -0,0 +1,14 @@
import { BaseEntity } from '@cool-midway/core';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_user_role')
export class BaseSysUserRoleEntity extends BaseEntity {
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '角色ID' })
roleId: number;
}

View File

@ -0,0 +1,92 @@
import { CoolEvent, Event } from '@cool-midway/core';
import { App, Config, ILogger, Logger } from '@midwayjs/core';
import { IMidwayKoaApplication } from '@midwayjs/koa';
import * as fs from 'fs';
import * as path from 'path';
import { v1 as uuid } from 'uuid';
/**
* jwt.secret
*/
@CoolEvent()
export class BaseAppEvent {
@Logger()
coreLogger: ILogger;
@Config('module')
config;
@Config('keys')
configKeys;
@Config('koa.port')
port;
@App()
app: IMidwayKoaApplication;
@Event('onMenuInit')
async onMenuInit() {
this.checkConfig();
this.checkKeys();
}
/**
*
*/
async checkConfig() {
if (this.config.base.jwt.secret == 'cool-admin-xxxxxx') {
this.coreLogger.warn(
'\x1B[36m 检测到模块[base] jwt.secret 配置是默认值,请不要关闭!即将自动修改... \x1B[0m'
);
setTimeout(() => {
const filePath = path.join(
this.app.getBaseDir(),
'modules',
'base',
'config.ts'
);
// 替换文件内容
let fileData = fs.readFileSync(filePath, 'utf8');
const secret = uuid().replace(/-/g, '');
this.config.base.jwt.secret = secret;
fs.writeFileSync(
filePath,
fileData.replace('cool-admin-xxxxxx', secret)
);
this.coreLogger.info(
'\x1B[36m [cool:module:base] midwayjs cool module base auto modify jwt.secret\x1B[0m'
);
}, 6000);
}
}
/**
* keys
*/
async checkKeys() {
if (this.configKeys == 'cool-admin-keys-xxxxxx') {
this.coreLogger.warn(
'\x1B[36m 检测到基础配置[Keys] 是默认值,请不要关闭!即将自动修改... \x1B[0m'
);
setTimeout(() => {
const filePath = path.join(
this.app.getBaseDir(),
'config',
'config.default.ts'
);
// 替换文件内容
let fileData = fs.readFileSync(filePath, 'utf8');
const secret = uuid().replace(/-/g, '');
this.config.base.jwt.secret = secret;
fs.writeFileSync(
filePath,
fileData.replace('cool-admin-keys-xxxxxx', secret)
);
this.coreLogger.info(
'\x1B[36m [cool:module:base] midwayjs cool keys auto modify \x1B[0m'
);
}, 6000);
}
}
}

View File

@ -0,0 +1,36 @@
import { CoolEvent, Event } from '@cool-midway/core';
import { BaseSysMenuService } from '../service/sys/menu';
import {
App,
ILogger,
IMidwayApplication,
Inject,
Logger,
} from '@midwayjs/core';
/**
*
*/
@CoolEvent()
export class BaseMenuEvent {
@Logger()
coreLogger: ILogger;
@Inject()
baseSysMenuService: BaseSysMenuService;
@App()
app: IMidwayApplication;
@Event('onMenuImport')
async onMenuImport(datas) {
for (const module in datas) {
await this.baseSysMenuService.import(datas[module]);
this.coreLogger.info(
'\x1B[36m [cool:module:base] midwayjs cool module base import [' +
module +
'] module menu success \x1B[0m'
);
}
}
}

View File

@ -0,0 +1,25 @@
import { Job, IJob } from '@midwayjs/cron';
import { FORMAT, ILogger, Inject } from '@midwayjs/core';
import { BaseSysLogService } from '../service/sys/log';
/**
*
*/
@Job({
cronTime: FORMAT.CRONTAB.EVERY_DAY,
start: true,
})
export class BaseLogJob implements IJob {
@Inject()
baseSysLogService: BaseSysLogService;
@Inject()
logger: ILogger;
async onTick() {
this.logger.info('清除日志定时任务开始执行');
const startTime = Date.now();
await this.baseSysLogService.clear();
this.logger.info(`清除日志定时任务结束,耗时:${Date.now() - startTime}ms`);
}
}

1571
src/modules/base/menu.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
import { App, Config, Inject, Middleware } from '@midwayjs/decorator';
import * as _ from 'lodash';
import { CoolUrlTagData, RESCODE, TagTypes } from '@cool-midway/core';
import * as jwt from 'jsonwebtoken';
import { NextFunction, Context } from '@midwayjs/koa';
import {
IMiddleware,
IMidwayApplication,
Init,
InjectClient,
} from '@midwayjs/core';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
/**
*
*/
@Middleware()
export class BaseAuthorityMiddleware
implements IMiddleware<Context, NextFunction>
{
@Config('koa.globalPrefix')
prefix;
@Config('module.base')
jwtConfig;
@InjectClient(CachingFactory, 'default')
midwayCache: MidwayCache;
@Inject()
coolUrlTagData: CoolUrlTagData;
@App()
app: IMidwayApplication;
ignoreUrls: string[] = [];
@Init()
async init() {
this.ignoreUrls = this.coolUrlTagData.byKey(TagTypes.IGNORE_TOKEN, 'admin');
}
resolve() {
return async (ctx: Context, next: NextFunction) => {
let statusCode = 200;
let { url } = ctx;
url = url.replace(this.prefix, '').split('?')[0];
const token = ctx.get('Authorization');
const adminUrl = '/admin/';
// 路由地址为 admin前缀的 需要权限校验
if (_.startsWith(url, adminUrl)) {
try {
ctx.admin = jwt.verify(token, this.jwtConfig.jwt.secret);
if (ctx.admin.isRefresh) {
ctx.status = 401;
ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效~',
};
return;
}
} catch (error) {}
// 使用matchUrl方法来检查URL是否应该被忽略
const isIgnored = this.ignoreUrls.some(pattern =>
this.matchUrl(pattern, url)
);
if (isIgnored) {
await next();
return;
}
if (ctx.admin) {
const rToken = await this.midwayCache.get(
`admin:token:${ctx.admin.userId}`
);
// 判断密码版本是否正确
const passwordV = await this.midwayCache.get(
`admin:passwordVersion:${ctx.admin.userId}`
);
if (passwordV != ctx.admin.passwordVersion) {
ctx.status = 401;
ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效~',
};
return;
}
// 超管拥有所有权限
if (ctx.admin.username == 'admin' && !ctx.admin.isRefresh) {
if (rToken !== token && this.jwtConfig.jwt.sso) {
ctx.status = 401;
ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效~',
};
return;
} else {
await next();
return;
}
}
// 要登录每个人都有权限的接口
if (
new RegExp(`^${adminUrl}?.*/comm/`).test(url) ||
// 字典接口
url == '/admin/dict/info/data'
) {
await next();
return;
}
// 如果传的token是refreshToken则校验失败
if (ctx.admin.isRefresh) {
ctx.status = 401;
ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效~',
};
return;
}
if (!rToken) {
ctx.status = 401;
ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效或无权限访问~',
};
return;
}
if (rToken !== token && this.jwtConfig.jwt.sso) {
statusCode = 401;
} else {
let perms: string[] = await this.midwayCache.get(
`admin:perms:${ctx.admin.userId}`
);
if (!_.isEmpty(perms)) {
perms = perms.map(e => {
return e.replace(/:/g, '/');
});
if (!perms.includes(url.split('?')[0].replace('/admin/', ''))) {
statusCode = 403;
}
} else {
statusCode = 403;
}
}
} else {
statusCode = 401;
}
if (statusCode > 200) {
ctx.status = statusCode;
ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效或无权限访问~',
};
return;
}
}
await next();
};
}
// 匹配URL的方法
matchUrl(pattern, url) {
const patternSegments = pattern.split('/').filter(Boolean);
const urlSegments = url.split('/').filter(Boolean);
// 如果段的数量不同,则无法匹配
if (patternSegments.length !== urlSegments.length) {
return false;
}
// 逐段进行匹配
for (let i = 0; i < patternSegments.length; i++) {
if (patternSegments[i].startsWith(':')) {
// 如果模式段以':'开始,我们认为它是一个参数,可以匹配任何内容
continue;
}
// 如果两个段不相同,则不匹配
if (patternSegments[i] !== urlSegments[i]) {
return false;
}
}
// 所有段都匹配
return true;
}
}

View File

@ -0,0 +1,26 @@
import { Middleware } from '@midwayjs/decorator';
import * as _ from 'lodash';
import { NextFunction, Context } from '@midwayjs/koa';
import { IMiddleware } from '@midwayjs/core';
import { BaseSysLogService } from '../service/sys/log';
/**
*
*/
@Middleware()
export class BaseLogMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
const baseSysLogService = await ctx.requestContext.getAsync(
BaseSysLogService
);
baseSysLogService.record(
ctx,
ctx.url,
ctx.req.method === 'GET' ? ctx.request.query : ctx.request.body,
ctx.admin ? ctx.admin.userId : null
);
await next();
};
}
}

View File

@ -0,0 +1,39 @@
import { Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseSysConfEntity } from '../../entity/sys/conf';
/**
*
*/
@Provide()
export class BaseSysConfService extends BaseService {
@InjectEntityModel(BaseSysConfEntity)
baseSysConfEntity: Repository<BaseSysConfEntity>;
/**
*
* @param key
*/
async getValue(key) {
const conf = await this.baseSysConfEntity.findOneBy({ cKey: key });
if (conf) {
return conf.cValue;
}
}
/**
*
* @param cKey
* @param cValue
*/
async updateVaule(cKey, cValue) {
await this.baseSysConfEntity
.createQueryBuilder()
.update()
.where({ cKey })
.set({ cKey, cValue })
.execute();
}
}

View File

@ -0,0 +1,10 @@
import { DataSource } from 'typeorm';
export class TempDataSource extends DataSource {
/**
*
*/
async buildMetadatas() {
await super.buildMetadatas();
}
}

View File

@ -0,0 +1,124 @@
import { Inject, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { In, Repository } from 'typeorm';
import { BaseSysDepartmentEntity } from '../../entity/sys/department';
import * as _ from 'lodash';
import { BaseSysRoleDepartmentEntity } from '../../entity/sys/role_department';
import { BaseSysPermsService } from './perms';
import { BaseSysUserEntity } from '../../entity/sys/user';
/**
*
*/
@Provide()
export class BaseSysDepartmentService extends BaseService {
@InjectEntityModel(BaseSysDepartmentEntity)
baseSysDepartmentEntity: Repository<BaseSysDepartmentEntity>;
@InjectEntityModel(BaseSysUserEntity)
baseSysUserEntity: Repository<BaseSysUserEntity>;
@InjectEntityModel(BaseSysRoleDepartmentEntity)
baseSysRoleDepartmentEntity: Repository<BaseSysRoleDepartmentEntity>;
@Inject()
baseSysPermsService: BaseSysPermsService;
@Inject()
ctx;
/**
*
*/
async list() {
// 部门权限
const permsDepartmentArr = await this.baseSysPermsService.departmentIds(
this.ctx.admin.userId
);
// 过滤部门权限
const find = this.baseSysDepartmentEntity.createQueryBuilder('a');
if (this.ctx.admin.username !== 'admin')
find.andWhere('a.id in (:...ids)', {
ids: !_.isEmpty(permsDepartmentArr) ? permsDepartmentArr : [null],
});
find.addOrderBy('a.orderNum', 'ASC');
const departments: BaseSysDepartmentEntity[] = await find.getMany();
if (!_.isEmpty(departments)) {
departments.forEach(e => {
const parentMenu = departments.filter(m => {
e.parentId = parseInt(e.parentId + '');
if (e.parentId == m.id) {
return m.name;
}
});
if (!_.isEmpty(parentMenu)) {
e.parentName = parentMenu[0].name;
}
});
}
return departments;
}
/**
* ID获得部门权限信息
* @param {[]} roleIds
* @param isAdmin
*/
async getByRoleIds(roleIds: number[], isAdmin) {
if (!_.isEmpty(roleIds)) {
if (isAdmin) {
const result = await this.baseSysDepartmentEntity.find();
return result.map(e => {
return e.id;
});
}
const result = await this.baseSysRoleDepartmentEntity
.createQueryBuilder('a')
.where('a.roleId in (:...roleIds)', { roleIds })
.getMany();
if (!_.isEmpty(result)) {
return _.uniq(
result.map(e => {
return e.departmentId;
})
);
}
}
return [];
}
/**
*
* @param params
*/
async order(params) {
for (const e of params) {
await this.baseSysDepartmentEntity.update(e.id, e);
}
}
/**
*
*/
async delete(ids: number[]) {
const { deleteUser } = this.ctx.request.body;
await super.delete(ids);
if (deleteUser) {
await this.baseSysUserEntity.delete({ departmentId: In(ids) });
} else {
const topDepartment = await this.baseSysDepartmentEntity
.createQueryBuilder('a')
.where('a.parentId is null')
.getOne();
if (topDepartment) {
await this.baseSysUserEntity.update(
{ departmentId: In(ids) },
{ departmentId: topDepartment.id }
);
}
}
}
}

View File

@ -0,0 +1,66 @@
import { Inject, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { LessThan, Repository } from 'typeorm';
import * as _ from 'lodash';
import { BaseSysLogEntity } from '../../entity/sys/log';
import * as moment from 'moment';
import { Utils } from '../../../../comm/utils';
import { BaseSysConfService } from './conf';
import { Context } from '@midwayjs/koa';
/**
*
*/
@Provide()
export class BaseSysLogService extends BaseService {
@Inject()
ctx;
@Inject()
utils: Utils;
@InjectEntityModel(BaseSysLogEntity)
baseSysLogEntity: Repository<BaseSysLogEntity>;
@Inject()
baseSysConfService: BaseSysConfService;
/**
*
* @param url URL地址
* @param params
* @param userId ID
*/
async record(context: Context, url, params, userId) {
const ip = await this.utils.getReqIP(context);
const sysLog = new BaseSysLogEntity();
sysLog.userId = userId;
sysLog.ip = typeof ip === 'string' ? ip : ip.join(',');
const ipAddrArr = [];
for (const e of sysLog.ip.split(','))
ipAddrArr.push(await this.utils.getIpAddr(context, e));
sysLog.ipAddr = ipAddrArr.join(',');
sysLog.action = url.split('?')[0];
sysLog.params = params;
await this.baseSysLogEntity.insert(sysLog);
}
/**
*
* @param isAll
*/
async clear(isAll?) {
if (isAll) {
await this.baseSysLogEntity.clear();
return;
}
const keepDay = await this.baseSysConfService.getValue('logKeep');
if (keepDay) {
const beforeDate = moment().add(-keepDay, 'days').startOf('day').toDate();
await this.baseSysLogEntity.delete({ createTime: LessThan(beforeDate) });
} else {
await this.baseSysLogEntity.clear();
}
}
}

View File

@ -0,0 +1,251 @@
import { Inject, Provide, Config, InjectClient } from '@midwayjs/decorator';
import { BaseService, CoolCommException } from '@cool-midway/core';
import { LoginDTO } from '../../dto/login';
import * as svgCaptcha from 'svg-captcha';
import { v1 as uuid } from 'uuid';
import { BaseSysUserEntity } from '../../entity/sys/user';
import { Repository } from 'typeorm';
import { InjectEntityModel } from '@midwayjs/typeorm';
import * as md5 from 'md5';
import { BaseSysRoleService } from './role';
import * as _ from 'lodash';
import { BaseSysMenuService } from './menu';
import { BaseSysDepartmentService } from './department';
import * as jwt from 'jsonwebtoken';
import * as svgToDataURL from 'mini-svg-data-uri';
import { Context } from '@midwayjs/koa';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
import { readFileSync } from 'fs';
const { svg2png, initialize } = require('svg2png-wasm');
initialize(readFileSync('./node_modules/svg2png-wasm/svg2png_wasm_bg.wasm'));
/**
*
*/
@Provide()
export class BaseSysLoginService extends BaseService {
@InjectClient(CachingFactory, 'default')
midwayCache: MidwayCache;
@InjectEntityModel(BaseSysUserEntity)
baseSysUserEntity: Repository<BaseSysUserEntity>;
@Inject()
baseSysRoleService: BaseSysRoleService;
@Inject()
baseSysMenuService: BaseSysMenuService;
@Inject()
baseSysDepartmentService: BaseSysDepartmentService;
@Inject()
ctx: Context;
@Config('module.base')
coolConfig;
/**
*
* @param login
*/
async login(login: LoginDTO) {
const { username, captchaId, verifyCode, password } = login;
// 校验验证码
const checkV = await this.captchaCheck(captchaId, verifyCode);
if (checkV) {
const user = await this.baseSysUserEntity.findOneBy({ username });
// 校验用户
if (user) {
// 校验用户状态及密码
if (user.status === 0 || user.password !== md5(password)) {
throw new CoolCommException('账户或密码不正确~');
}
} else {
throw new CoolCommException('账户或密码不正确~');
}
// 校验角色
const roleIds = await this.baseSysRoleService.getByUser(user.id);
if (_.isEmpty(roleIds)) {
throw new CoolCommException('该用户未设置任何角色,无法登录~');
}
// 生成token
const { expire, refreshExpire } = this.coolConfig.jwt.token;
const result = {
expire,
token: await this.generateToken(user, roleIds, expire),
refreshExpire,
refreshToken: await this.generateToken(
user,
roleIds,
refreshExpire,
true
),
};
// 将用户相关信息保存到缓存
const perms = await this.baseSysMenuService.getPerms(roleIds);
const departments = await this.baseSysDepartmentService.getByRoleIds(
roleIds,
user.username === 'admin'
);
await this.midwayCache.set(`admin:department:${user.id}`, departments);
await this.midwayCache.set(`admin:perms:${user.id}`, perms);
await this.midwayCache.set(`admin:token:${user.id}`, result.token);
await this.midwayCache.set(
`admin:token:refresh:${user.id}`,
result.token
);
return result;
} else {
throw new CoolCommException('验证码不正确');
}
}
/**
*
* @param type svg
* @param width
* @param height
*/
async captcha(type: string, width = 150, height = 50, color = '#fff') {
const svg = svgCaptcha.create({
ignoreChars: 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM',
width,
height,
});
const result = {
captchaId: uuid(),
data: svg.data.replace(/"/g, "'"),
};
// 文字变白
const rpList = [
'#111',
'#222',
'#333',
'#444',
'#555',
'#666',
'#777',
'#888',
'#999',
];
rpList.forEach(rp => {
result.data = result.data['replaceAll'](rp, color);
});
if (type === 'base64') {
result.data = svgToDataURL(result.data);
}
if (type === 'png') {
result.data = await svg2png(result.data, {
scale: 2, // optional
width, // optional
height, // optional
backgroundColor: 'white', // optional
});
result.data =
'data:image/png;base64,' +
Buffer.from(result.data, 'binary').toString('base64');
}
// 半小时过期
await this.midwayCache.set(
`verify:img:${result.captchaId}`,
svg.text.toLowerCase(),
1800 * 1000
);
return result;
}
/**
* 退
*/
async logout() {
if (!this.coolConfig.jwt.sso) return;
const { userId } = this.ctx.admin;
await this.midwayCache.del(`admin:department:${userId}`);
await this.midwayCache.del(`admin:perms:${userId}`);
await this.midwayCache.del(`admin:token:${userId}`);
await this.midwayCache.del(`admin:token:refresh:${userId}`);
await this.midwayCache.del(`admin:passwordVersion:${userId}`);
}
/**
*
* @param captchaId ID
* @param value
*/
async captchaCheck(captchaId, value) {
const rv = await this.midwayCache.get(`verify:img:${captchaId}`);
if (!rv || !value || value.toLowerCase() !== rv) {
return false;
} else {
this.midwayCache.del(`verify:img:${captchaId}`);
return true;
}
}
/**
* token
* @param user
* @param roleIds
* @param expire
* @param isRefresh
*/
async generateToken(user, roleIds, expire, isRefresh?) {
await this.midwayCache.set(
`admin:passwordVersion:${user.id}`,
user.passwordV
);
const tokenInfo = {
isRefresh: false,
roleIds,
username: user.username,
userId: user.id,
passwordVersion: user.passwordV,
};
if (isRefresh) {
tokenInfo.isRefresh = true;
}
return jwt.sign(tokenInfo, this.coolConfig.jwt.secret, {
expiresIn: expire,
});
}
/**
* token
* @param token
*/
async refreshToken(token: string) {
const decoded = jwt.verify(token, this.coolConfig.jwt.secret);
if (decoded && decoded['isRefresh']) {
delete decoded['exp'];
delete decoded['iat'];
const { expire, refreshExpire } = this.coolConfig.jwt.token;
decoded['isRefresh'] = false;
const result = {
expire,
token: jwt.sign(decoded, this.coolConfig.jwt.secret, {
expiresIn: expire,
}),
refreshExpire,
refreshToken: '',
};
decoded['isRefresh'] = true;
result.refreshToken = jwt.sign(decoded, this.coolConfig.jwt.secret, {
expiresIn: refreshExpire,
});
await this.midwayCache.set(
`admin:passwordVersion:${decoded['userId']}`,
decoded['passwordVersion']
);
await this.midwayCache.set(
`admin:token:${decoded['userId']}`,
result.token
);
return result;
}
}
}

View File

@ -0,0 +1,466 @@
import { App, IMidwayApplication, Scope, ScopeEnum } from '@midwayjs/core';
import { ALL, Config, Inject, Provide } from '@midwayjs/decorator';
import { BaseService, CoolCommException } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { In, Repository } from 'typeorm';
import { BaseSysMenuEntity } from '../../entity/sys/menu';
import * as _ from 'lodash';
import { BaseSysPermsService } from './perms';
import { Context } from '@midwayjs/koa';
import { TempDataSource } from './data';
// eslint-disable-next-line node/no-unpublished-import
import * as ts from 'typescript';
import * as fs from 'fs';
import * as pathUtil from 'path';
import { BaseSysRoleMenuEntity } from '../../entity/sys/role_menu';
import { BaseSysUserRoleEntity } from '../../entity/sys/user_role';
/**
*
*/
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@Provide()
export class BaseSysMenuService extends BaseService {
@Inject()
ctx: Context;
@InjectEntityModel(BaseSysMenuEntity)
baseSysMenuEntity: Repository<BaseSysMenuEntity>;
@InjectEntityModel(BaseSysRoleMenuEntity)
baseSysRoleMenuEntity: Repository<BaseSysRoleMenuEntity>;
@Inject()
baseSysPermsService: BaseSysPermsService;
@Config(ALL)
config;
@App()
app: IMidwayApplication;
/**
*
*/
async list() {
const menus = await this.getMenus(
this.ctx.admin.roleIds,
this.ctx.admin.username === 'admin'
);
if (!_.isEmpty(menus)) {
menus.forEach((e: any) => {
const parentMenu = menus.filter(m => {
e.parentId = parseInt(e.parentId);
if (e.parentId == m.id) {
return m.name;
}
});
if (!_.isEmpty(parentMenu)) {
e.parentName = parentMenu[0].name;
}
});
}
return menus;
}
/**
*
* @param param
*/
async modifyAfter(param) {
if (param.id) {
await this.refreshPerms(param.id);
}
}
/**
*
* @param {[]} roleIds
*/
async getPerms(roleIds) {
let perms = [];
if (!_.isEmpty(roleIds)) {
const find = await this.baseSysMenuEntity.createQueryBuilder('a');
if (!roleIds.includes(1)) {
find.innerJoinAndSelect(
BaseSysRoleMenuEntity,
'b',
'a.id = b.menuId AND b.roleId in (:...roleIds)',
{ roleIds }
);
}
find.where('a.perms is not NULL');
const result = await find.getMany();
if (result) {
result.forEach(d => {
if (d.perms) {
perms = perms.concat(d.perms.split(','));
}
});
}
perms = _.uniq(perms);
perms = _.remove(perms, n => {
return !_.isEmpty(n);
});
}
return _.uniq(perms);
}
/**
*
* @param roleIds
* @param isAdmin
*/
async getMenus(roleIds, isAdmin) {
const find = this.baseSysMenuEntity.createQueryBuilder('a');
if (!isAdmin) {
find.innerJoinAndSelect(
BaseSysRoleMenuEntity,
'b',
'a.id = b.menuId AND b.roleId in (:...roleIds)',
{ roleIds }
);
}
find.orderBy('a.orderNum', 'ASC');
const list = await find.getMany();
return _.uniqBy(list, 'id');
}
/**
*
* @param ids
*/
async delete(ids) {
let idArr;
if (ids instanceof Array) {
idArr = ids;
} else {
idArr = ids.split(',');
}
for (const id of idArr) {
await this.baseSysMenuEntity.delete({ id });
await this.delChildMenu(id);
}
}
/**
*
* @param id
*/
private async delChildMenu(id) {
await this.refreshPerms(id);
const delMenu = await this.baseSysMenuEntity.findBy({ parentId: id });
if (_.isEmpty(delMenu)) {
return;
}
const delMenuIds = delMenu.map(e => {
return e.id;
});
await this.baseSysMenuEntity.delete(delMenuIds);
for (const menuId of delMenuIds) {
await this.delChildMenu(menuId);
}
}
/**
*
* @param menuId
*/
async refreshPerms(menuId) {
const find = this.baseSysRoleMenuEntity.createQueryBuilder('a');
find.leftJoinAndSelect(BaseSysUserRoleEntity, 'b', 'a.roleId = b.roleId');
find.where('a.menuId = :menuId', { menuId: menuId });
find.select('b.userId', 'userId');
const users = await find.getRawMany();
// 刷新admin权限
await this.baseSysPermsService.refreshPerms(1);
if (!_.isEmpty(users)) {
// 刷新其他权限
for (const user of _.uniqBy(users, 'userId')) {
await this.baseSysPermsService.refreshPerms(user.userId);
}
}
}
/**
* Controller
* @param entityString
* @param controller
* @param module
*/
async parse(entityString: string, controller: string, module: string) {
const tempDataSource = new TempDataSource({
...this.config.typeorm.dataSource.default,
entities: [],
});
// 连接数据库
await tempDataSource.initialize();
const { newCode, className, oldTableName } = this.parseCode(entityString);
const code = ts.transpile(
`${newCode}
tempDataSource.options.entities.push(${className})
`,
{
emitDecoratorMetadata: true,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2018,
removeComments: true,
experimentalDecorators: true,
noImplicitThis: true,
noUnusedLocals: true,
stripInternal: true,
skipLibCheck: true,
pretty: true,
declaration: true,
noImplicitAny: false,
}
);
eval(code);
await tempDataSource.buildMetadatas();
const meta = tempDataSource.getMetadata(className);
const columnArr = meta.columns;
await tempDataSource.destroy();
const commColums = [];
const columns = _.filter(
columnArr.map(e => {
return {
propertyName: e.propertyName,
type: typeof e.type == 'string' ? e.type : e.type.name.toLowerCase(),
length: e.length,
comment: e.comment,
nullable: e.isNullable,
};
}),
o => {
if (['createTime', 'updateTime'].includes(o.propertyName)) {
commColums.push(o);
}
return o && !['createTime', 'updateTime'].includes(o.propertyName);
}
).concat(commColums);
if (!controller) {
const tableNames = oldTableName.split('_');
const fileName = tableNames[tableNames.length - 1];
return {
columns,
className: className.replace('TEMP', ''),
tableName: oldTableName,
fileName,
path: `/admin/${module}/${fileName}`,
};
}
const fileName = await this.fileName(controller);
return {
columns,
path: `/admin/${module}/${fileName}`,
};
}
/**
* Entity类名
* @param code
* @returns
*/
parseCode(code: string) {
try {
const oldClassName = code
.match('class(.*)extends')[1]
.replace(/\s*/g, '');
const oldTableStart = code.indexOf('@Entity(');
const oldTableEnd = code.indexOf(')');
const oldTableName = code
.substring(oldTableStart + 9, oldTableEnd - 1)
.replace(/\s*/g, '')
// eslint-disable-next-line no-useless-escape
.replace(/\"/g, '')
// eslint-disable-next-line no-useless-escape
.replace(/\'/g, '');
const className = `${oldClassName}TEMP`;
return {
newCode: code
.replace(oldClassName, className)
.replace(oldTableName, `func_${oldTableName}`),
className,
tableName: `func_${oldTableName}`,
oldTableName,
};
} catch (err) {
throw new CoolCommException('代码结构不正确,请检查');
}
}
/**
*
* @param body body
*/
async create(body) {
const { module, entity, controller, fileName } = body;
const basePath = this.app.getBaseDir();
// const fileName = await this.fileName(controller);
// 生成Entity
const entityPath = pathUtil.join(
basePath,
'modules',
module,
'entity',
`${fileName}.ts`
);
// 生成Controller
const controllerPath = pathUtil.join(
basePath,
'modules',
module,
'controller',
'admin',
`${fileName}.ts`
);
this.createConfigFile(module);
this.createFile(entityPath, entity);
this.createFile(controllerPath, controller);
}
/**
*
* @param module
*/
async createConfigFile(module: string) {
const basePath = this.app.getBaseDir();
const configFilePath = pathUtil.join(
basePath,
'modules',
module,
'config.ts'
);
if (!fs.existsSync(configFilePath)) {
const data = `import { ModuleConfig } from '@cool-midway/core';
/**
*
*/
export default () => {
return {
// 模块名称
name: 'xxx',
// 模块描述
description: 'xxx',
// 中间件,只对本模块有效
middlewares: [],
// 中间件,全局有效
globalMiddlewares: [],
// 模块加载顺序默认为0值越大越优先加载
order: 0,
} as ModuleConfig;
};
`;
await this.createFile(configFilePath, data);
}
}
/**
*
* @param controller
* @returns
*/
async fileName(controller: string) {
const regex = /import\s*{\s*\w+\s*}\s*from\s*'[^']*\/([\w-]+)';/;
const match = regex.exec(controller);
if (match && match.length > 1) {
return match[1];
}
return null;
}
/**
*
* @param filePath
* @param content
*/
async createFile(filePath: string, content: string) {
const folderPath = pathUtil.dirname(filePath);
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true });
}
fs.writeFileSync(filePath, content);
}
/**
*
* @param ids
* @returns
*/
async export(ids: number[]) {
const result: any[] = [];
const menus = await this.baseSysMenuEntity.findBy({ id: In(ids) });
// 递归取出子菜单
const getChildMenus = (parentId: number): any[] => {
const children = _.remove(menus, e => e.parentId == parentId);
children.forEach(child => {
child.childMenus = getChildMenus(child.id);
// 删除不需要的字段
delete child.id;
delete child.createTime;
delete child.updateTime;
delete child.parentId;
});
return children;
};
// lodash取出父级菜单(parentId为 null) 并从menus 删除
const parentMenus = _.remove(menus, e => {
return e.parentId == null;
});
// 对于每个父级菜单,获取它的子菜单
parentMenus.forEach(parent => {
parent.childMenus = getChildMenus(parent.id);
// 删除不需要的字段
delete parent.id;
delete parent.createTime;
delete parent.updateTime;
delete parent.parentId;
result.push(parent);
});
return result;
}
/**
*
* @param menus
*/
async import(menus: any[]) {
// 递归保存子菜单
const saveChildMenus = async (parentMenu: any, parentId: number | null) => {
const children = parentMenu.childMenus || [];
for (let child of children) {
const childData = { ...child, parentId: parentId }; // 保持与数据库的parentId字段的一致性
delete childData.childMenus; // 删除childMenus属性因为我们不想将它保存到数据库中
// 保存子菜单并获取其ID以便为其子菜单设置parentId
const savedChild = await this.baseSysMenuEntity.save(childData);
if (!_.isEmpty(child.childMenus)) {
await saveChildMenus(child, savedChild.id);
}
}
};
for (let menu of menus) {
const menuData = { ...menu };
delete menuData.childMenus; // 删除childMenus属性因为我们不想将它保存到数据库中
// 保存主菜单并获取其ID
const savedMenu = await this.baseSysMenuEntity.save(menuData);
if (menu.childMenus && menu.childMenus.length > 0) {
await saveChildMenus(menu, savedMenu.id);
}
}
}
}

View File

@ -0,0 +1,91 @@
import { Inject, InjectClient, Provide } from '@midwayjs/decorator';
import { BaseService, CoolCommException } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Not, Repository } from 'typeorm';
import { BaseSysParamEntity } from '../../entity/sys/param';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
/**
*
*/
@Provide()
export class BaseSysParamService extends BaseService {
@InjectEntityModel(BaseSysParamEntity)
baseSysParamEntity: Repository<BaseSysParamEntity>;
@InjectClient(CachingFactory, 'default')
midwayCache: MidwayCache;
/**
* key获得对应的参数
* @param key
*/
async dataByKey(key) {
let result: any = await this.midwayCache.get(`param:${key}`);
if (!result) {
result = await this.baseSysParamEntity.findOneBy({ keyName: key });
this.midwayCache.set(`param:${key}`, result);
}
if (result) {
if (result.dataType == 0) {
try {
return JSON.parse(result.data);
} catch (error) {
return result.data;
}
}
if (result.dataType == 1) {
return result.data;
}
if (result.dataType == 2) {
return result.data.split(',');
}
}
return;
}
/**
* key获得对应的网页数据
* @param key
*/
async htmlByKey(key) {
let html = '<html><title>@title</title><body>@content</body></html>';
let result: any = await this.midwayCache.get(`param:${key}`);
if (result) {
html = html
.replace('@content', result.data)
.replace('@title', result.name);
} else {
html = html.replace('@content', 'key notfound');
}
return html;
}
/**
*
* @param param
*/
async addOrUpdate(param: any, type): Promise<void> {
const find = {
keyName: param.keyName,
};
if (param.id) {
find['id'] = Not(param.id);
}
const check = await this.baseSysParamEntity.findOneBy(find);
if (check) {
throw new CoolCommException('存在相同的keyName');
}
await super.addOrUpdate(param, type);
}
/**
*
*/
async modifyAfter() {
const params = await this.baseSysParamEntity.find();
for (const param of params) {
await this.midwayCache.set(`param:${param.keyName}`, param);
}
}
}

View File

@ -0,0 +1,90 @@
import { Inject, InjectClient, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { BaseSysMenuService } from './menu';
import { BaseSysRoleService } from './role';
import { BaseSysDepartmentService } from './department';
import { Context } from '@midwayjs/koa';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
import { BaseSysRoleEntity } from '../../entity/sys/role';
import { In, Repository } from 'typeorm';
import { InjectEntityModel } from '@midwayjs/typeorm';
/**
*
*/
@Provide()
export class BaseSysPermsService extends BaseService {
@InjectClient(CachingFactory, 'default')
midwayCache: MidwayCache;
@Inject()
baseSysMenuService: BaseSysMenuService;
@Inject()
baseSysRoleService: BaseSysRoleService;
@Inject()
baseSysDepartmentService: BaseSysDepartmentService;
@InjectEntityModel(BaseSysRoleEntity)
baseSysRoleEntity: Repository<BaseSysRoleEntity>;
@Inject()
ctx: Context;
base: any;
/**
*
* @param userId ID
*/
async refreshPerms(userId) {
const roleIds = await this.baseSysRoleService.getByUser(userId);
const perms = await this.baseSysMenuService.getPerms(roleIds);
await this.midwayCache.set(`admin:perms:${userId}`, perms);
// 更新部门权限
const departments = await this.baseSysDepartmentService.getByRoleIds(
roleIds,
await this.isAdmin(roleIds)
);
await this.midwayCache.set(`admin:department:${userId}`, departments);
}
/**
*
* @param roleIds
*/
async isAdmin(roleIds: number[]) {
const roles = await this.baseSysRoleEntity.findBy({ id: In(roleIds) });
const roleLabels = roles.map(item => item.label);
return roleLabels.includes('admin');
}
/**
*
* @param roleIds
*/
async permmenu(roleIds: number[]) {
const perms = await this.baseSysMenuService.getPerms(roleIds);
const menus = await this.baseSysMenuService.getMenus(
roleIds,
this.ctx.admin.username === 'admin'
);
return { perms, menus };
}
/**
* ID获得部门权限
* @param userId
* @return ID数组
*/
async departmentIds(userId: number) {
const department: any = await this.midwayCache.get(
`admin:department:${userId}`
);
if (department) {
return department;
} else {
return [];
}
}
}

View File

@ -0,0 +1,136 @@
import { Inject, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseSysRoleEntity } from '../../entity/sys/role';
import { BaseSysUserRoleEntity } from '../../entity/sys/user_role';
import * as _ from 'lodash';
import { BaseSysRoleMenuEntity } from '../../entity/sys/role_menu';
import { BaseSysRoleDepartmentEntity } from '../../entity/sys/role_department';
import { BaseSysPermsService } from './perms';
import { Brackets } from 'typeorm';
/**
*
*/
@Provide()
export class BaseSysRoleService extends BaseService {
@InjectEntityModel(BaseSysRoleEntity)
baseSysRoleEntity: Repository<BaseSysRoleEntity>;
@InjectEntityModel(BaseSysUserRoleEntity)
baseSysUserRoleEntity: Repository<BaseSysUserRoleEntity>;
@InjectEntityModel(BaseSysRoleMenuEntity)
baseSysRoleMenuEntity: Repository<BaseSysRoleMenuEntity>;
@InjectEntityModel(BaseSysRoleDepartmentEntity)
baseSysRoleDepartmentEntity: Repository<BaseSysRoleDepartmentEntity>;
@Inject()
baseSysPermsService: BaseSysPermsService;
@Inject()
ctx;
/**
* ID获得所有用户角色
* @param userId
*/
async getByUser(userId: number): Promise<number[]> {
const userRole = await this.baseSysUserRoleEntity.findBy({ userId });
if (!_.isEmpty(userRole)) {
return userRole.map(e => {
return e.roleId;
});
}
return [];
}
/**
*
* @param param
*/
async modifyAfter(param) {
if (param.id) {
this.updatePerms(param.id, param.menuIdList, param.departmentIdList);
}
}
/**
*
* @param roleId
* @param menuIdList
* @param departmentIds
*/
async updatePerms(roleId, menuIdList?, departmentIds = []) {
// 更新菜单权限
await this.baseSysRoleMenuEntity.delete({ roleId });
await Promise.all(
menuIdList.map(async e => {
return await this.baseSysRoleMenuEntity.save({ roleId, menuId: e });
})
);
// 更新部门权限
await this.baseSysRoleDepartmentEntity.delete({ roleId });
await Promise.all(
departmentIds.map(async e => {
return await this.baseSysRoleDepartmentEntity.save({
roleId,
departmentId: e,
});
})
);
// 刷新权限
const userRoles = await this.baseSysUserRoleEntity.findBy({ roleId });
for (const userRole of userRoles) {
await this.baseSysPermsService.refreshPerms(userRole.userId);
}
}
/**
*
* @param id
*/
async info(id) {
const info = await this.baseSysRoleEntity.findOneBy({ id });
if (info) {
const menus = await this.baseSysRoleMenuEntity.findBy(
id !== 1 ? { roleId: id } : {}
);
const menuIdList = menus.map(e => {
return parseInt(e.menuId + '');
});
const departments = await this.baseSysRoleDepartmentEntity.findBy(
id !== 1 ? { roleId: id } : {}
);
const departmentIdList = departments.map(e => {
return parseInt(e.departmentId + '');
});
return {
...info,
menuIdList,
departmentIdList,
};
}
return {};
}
async list() {
return this.baseSysRoleEntity
.createQueryBuilder('a')
.where(
new Brackets(qb => {
qb.where('a.id !=:id', { id: 1 }); // 超级管理员的角色不展示
// 如果不是超管,只能看到自己新建的或者自己有的角色
if (this.ctx.admin.username !== 'admin') {
qb.andWhere('(a.userId=:userId or a.id in (:...roleId))', {
userId: this.ctx.admin.userId,
roleId: this.ctx.admin.roleIds,
});
}
})
)
.getMany();
}
}

View File

@ -0,0 +1,235 @@
import { Inject, InjectClient, Provide } from '@midwayjs/decorator';
import { BaseService, CoolCommException } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Equal, In, Repository } from 'typeorm';
import { BaseSysUserEntity } from '../../entity/sys/user';
import { BaseSysPermsService } from './perms';
import * as _ from 'lodash';
import { BaseSysUserRoleEntity } from '../../entity/sys/user_role';
import * as md5 from 'md5';
import { BaseSysDepartmentEntity } from '../../entity/sys/department';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
/**
*
*/
@Provide()
export class BaseSysUserService extends BaseService {
@InjectEntityModel(BaseSysUserEntity)
baseSysUserEntity: Repository<BaseSysUserEntity>;
@InjectEntityModel(BaseSysUserRoleEntity)
baseSysUserRoleEntity: Repository<BaseSysUserRoleEntity>;
@InjectEntityModel(BaseSysDepartmentEntity)
baseSysDepartmentEntity: Repository<BaseSysDepartmentEntity>;
@InjectClient(CachingFactory, 'default')
midwayCache: MidwayCache;
@Inject()
baseSysPermsService: BaseSysPermsService;
@Inject()
ctx;
/**
*
* @param query
*/
async page(query) {
const { keyWord, status, departmentIds = [] } = query;
const permsDepartmentArr = await this.baseSysPermsService.departmentIds(
this.ctx.admin.userId
); // 部门权限
const sql = `
SELECT
a.id,a.name,a.nickName,a.headImg,a.email,a.remark,a.status,a.createTime,a.updateTime,a.username,a.phone,a.departmentId,
b.name as "departmentName"
FROM
base_sys_user a
LEFT JOIN base_sys_department b on a.departmentId = b.id
WHERE 1 = 1
${this.setSql(
!_.isEmpty(departmentIds),
'and a.departmentId in (?)',
[departmentIds]
)}
${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.username != ?', ['admin'])}
${this.setSql(
this.ctx.admin.username !== 'admin',
'and a.departmentId in (?)',
[!_.isEmpty(permsDepartmentArr) ? permsDepartmentArr : [null]]
)} `;
const result = await this.sqlRenderPage(sql, query);
// 匹配角色
if (!_.isEmpty(result.list)) {
const userIds = result.list.map(e => e.id);
const roles = await this.nativeQuery(
'SELECT b.name, a.userId FROM base_sys_user_role a LEFT JOIN base_sys_role b ON a.roleId = b.id WHERE a.userId in (?) ',
[userIds]
);
result.list.forEach(e => {
e['roleName'] = roles
.filter(role => role.userId == e.id)
.map(role => role.name)
.join(',');
});
}
return result;
}
/**
*
* @param departmentId
* @param userIds
*/
async move(departmentId, userIds) {
await this.baseSysUserEntity.update({ id: In(userIds) }, { departmentId });
}
/**
*
*/
async person(userId: number) {
const info = await this.baseSysUserEntity.findOneBy({
id: Equal(userId),
});
delete info?.password;
return info;
}
/**
*
* @param user
*/
async updateUserRole(user) {
if (_.isEmpty(user.roleIdList)) {
return;
}
if (user.username === 'admin') {
throw new CoolCommException('非法操作~');
}
await this.baseSysUserRoleEntity.delete({ userId: user.id });
if (user.roleIdList) {
for (const roleId of user.roleIdList) {
await this.baseSysUserRoleEntity.save({ userId: user.id, roleId });
}
}
await this.baseSysPermsService.refreshPerms(user.id);
}
/**
*
* @param param
*/
async add(param) {
const exists = await this.baseSysUserEntity.findOneBy({
username: param.username,
});
if (!_.isEmpty(exists)) {
throw new CoolCommException('用户名已经存在~');
}
param.password = md5(param.password);
await this.baseSysUserEntity.save(param);
await this.updateUserRole(param);
return param.id;
}
/**
* ID获得信息
* @param id
*/
public async info(id) {
const info = await this.baseSysUserEntity.findOneBy({ id });
const userRoles = await this.nativeQuery(
'select a.roleId from base_sys_user_role a where a.userId = ?',
[id]
);
const department = await this.baseSysDepartmentEntity.findOneBy({
id: info.departmentId,
});
if (info) {
delete info.password;
if (userRoles) {
info.roleIdList = userRoles.map(e => {
return parseInt(e.roleId);
});
}
}
delete info.password;
if (department) {
info.departmentName = department.name;
}
return info;
}
/**
*
* @param param
*/
public async personUpdate(param) {
param.id = this.ctx.admin.userId;
if (!_.isEmpty(param.password)) {
param.password = md5(param.password);
const oldPassword = md5(param.oldPassword);
const userInfo = await this.baseSysUserEntity.findOneBy({ id: param.id });
if (!userInfo) {
throw new CoolCommException('用户不存在');
}
if (oldPassword !== userInfo.password) {
throw new CoolCommException('原密码错误');
}
param.passwordV = userInfo.passwordV + 1;
await this.midwayCache.set(
`admin:passwordVersion:${param.id}`,
param.passwordV
);
} else {
delete param.password;
}
await this.baseSysUserEntity.save(param);
}
/**
*
* @param param
*/
async update(param) {
if (param.id && param.username === 'admin') {
throw new CoolCommException('非法操作~');
}
if (!_.isEmpty(param.password)) {
param.password = md5(param.password);
const userInfo = await this.baseSysUserEntity.findOneBy({ id: param.id });
if (!userInfo) {
throw new CoolCommException('用户不存在');
}
param.passwordV = userInfo.passwordV + 1;
await this.midwayCache.set(
`admin:passwordVersion:${param.id}`,
param.passwordV
);
} else {
delete param.password;
}
if (param.status === 0) {
await this.forbidden(param.id);
}
await this.baseSysUserEntity.save(param);
await this.updateUserRole(param);
}
/**
*
* @param userId
*/
async forbidden(userId) {
await this.midwayCache.del(`admin:token:${userId}`);
}
}

View File

@ -0,0 +1,19 @@
import { ModuleConfig } from '@cool-midway/core';
/**
*
*/
export default () => {
return {
// 模块名称
name: '统计分析',
// 模块描述
description: '订单、商品、用户等统计',
// 中间件,只对本模块有效
middlewares: [],
// 中间件,全局有效
globalMiddlewares: [],
// 模块加载顺序默认为0值越大越优先加载
order: 0,
} as ModuleConfig;
};

View File

@ -0,0 +1,69 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { Body, Inject, Post } from '@midwayjs/core';
import { CountUserService } from '../../service/user';
import { CountOrderService } from '../../service/order';
import { CountGoodsService } from '../../service/goods';
/**
*
*/
@CoolController()
export class AdminCountHomeController extends BaseController {
@Inject()
countUserService: CountUserService;
@Inject()
countOrderService: CountOrderService;
@Inject()
countGoodsService: CountGoodsService;
@Post('/userSummary', { summary: '用户概况' })
async userSummary() {
return this.ok(await this.countUserService.summary());
}
@Post('/userChart', { summary: '用户图表' })
async userChart(
// 天数
@Body('dayCount') dayCount: number
) {
return this.ok(await this.countUserService.chart(dayCount));
}
@Post('/orderSummary', { summary: '订单概况' })
async orderSummary(
// 类型 count-数量 amount-金额
@Body('type') type: 'count' | 'amount'
) {
return this.ok(await this.countOrderService.summary(type));
}
@Post('/orderChart', { summary: '订单图表' })
async orderChart(
// 天数
@Body('dayCount') dayCount: number,
// 类型 count-数量 amount-金额
@Body('type') type: 'count' | 'amount'
) {
return this.ok(await this.countOrderService.chart(dayCount, type));
}
@Post('/goodsRank', { summary: '商品排行' })
async goodsRank(
// 类型 count-数量 amount-金额
@Body('type') type: 'count' | 'amount',
// 条数
@Body('limit') limit: number
) {
return this.ok(await this.countGoodsService.rank(type, limit));
}
@Post('/goodsCategory', { summary: '商品分类' })
async goodsCategory(
// 类型 count-数量 amount-金额
@Body('type') type: 'count' | 'amount'
) {
return this.ok(await this.countGoodsService.category(type));
}
}

View File

@ -0,0 +1,71 @@
import { Init, Inject, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import * as moment from 'moment';
import { Utils } from '../../../comm/utils';
/**
*
*/
@Provide()
export class CountCommService extends BaseService {
@Inject()
utils: Utils;
/**
*
* @param unit
*/
getTimeRange(unit: 'day' | 'week' | 'month' | 'year') {
let start, end;
if (unit === 'week') {
// 周的开始时间加一天,以匹配您的需求
start = moment()
.startOf(unit)
.add(1, 'days')
.format('YYYY-MM-DD HH:mm:ss');
} else {
start = moment().startOf(unit).format('YYYY-MM-DD HH:mm:ss');
}
end = moment().endOf(unit).format('YYYY-MM-DD HH:mm:ss');
return { start, end };
}
/**
*
* @param tableName
* @param dayCount
* @param column
* @returns
*/
async chart(tableName: string, dayCount: number, column = 'count(a.id)') {
const result = {
datas: [],
dates: [],
};
const dates = this.utils.getRecentlyDates(dayCount);
const count = await this.nativeQuery(
`SELECT
${column} as total,
LEFT(a.createTime, 10) AS time
FROM
${tableName} a
WHERE
DATE_SUB( CURDATE( ), INTERVAL ? DAY ) <= a.createTime
GROUP BY
LEFT(a.createTime, 10)`,
[dayCount]
);
dates.forEach(date => {
let data = 0;
for (const item of count) {
if (date == item.time) {
data = item.total;
break;
}
}
result.dates.push(date);
result.datas.push(data);
});
return result;
}
}

View File

@ -0,0 +1,55 @@
import { Init, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { OrderGoodsEntity } from '../../order/entity/goods';
/**
*
*/
@Provide()
export class CountGoodsService extends BaseService {
@InjectEntityModel(OrderGoodsEntity)
orderGoodsEntity: Repository<OrderGoodsEntity>;
/**
*
* @param type count- amount-
* @param limit
*/
async rank(type: 'count' | 'amount', limit = 10) {
const sql = `SELECT * FROM (
SELECT
a.goodsId,
b.title,
b.mainPic,
${type === 'count' ? 'SUM(a.count)' : 'SUM(a.price * a.count)'} AS total
FROM
order_goods a
LEFT JOIN goods_info b ON a.goodsId = b.id
GROUP BY
a.goodsId
ORDER BY
total DESC
) a LIMIT ${parseInt(limit.toString())}`;
return this.nativeQuery(sql);
}
/**
*
* @param type count- amount-
*/
async category(type: 'count' | 'amount') {
const sql = `SELECT
b.typeId,
c.name AS typeName,
${type === 'count' ? 'SUM(a.count)' : 'SUM(a.price * a.count)'} AS total
FROM
order_goods a
LEFT JOIN goods_info b ON a.goodsId = b.id
LEFT JOIN goods_type c ON b.typeId = c.id
GROUP BY
b.typeId`;
return this.nativeQuery(sql);
}
}

View File

@ -0,0 +1,94 @@
import { Init, Inject, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { OrderInfoEntity } from '../../order/entity/info';
import { CountCommService } from './comm';
/**
*
*/
@Provide()
export class CountOrderService extends BaseService {
@InjectEntityModel(OrderInfoEntity)
orderInfoEntity: Repository<OrderInfoEntity>;
@Inject()
countCommService: CountCommService;
/**
*
* @param type count- amount-
* @returns
*/
async summary(type: 'count' | 'amount' = 'count') {
// 定义一个函数来创建查询
const createQuery = async (start, end) => {
if (type === 'count') {
return await this.orderInfoEntity
.createQueryBuilder('a')
.where('a.createTime >= :start', { start })
.andWhere('a.createTime <= :end', { end })
.getCount();
}
if (type === 'amount') {
const result = await this.orderInfoEntity
.createQueryBuilder('a')
.select('SUM(a.price)', 'amount')
.where('a.createTime >= :start', { start })
.andWhere('a.createTime <= :end', { end })
.getRawOne();
return result.amount || 0;
}
};
// 总数
const total = await (type == 'amount'
? this.orderInfoEntity.sum('price')
: this.orderInfoEntity.count());
// 获取今天的时间范围
const { start: todayStart, end: todayEnd } =
this.countCommService.getTimeRange('day');
// 今天
const today = await createQuery(todayStart, todayEnd);
// 获取本周的时间范围
const { start: weekStart, end: weekEnd } =
this.countCommService.getTimeRange('week');
// 本周
const week = await createQuery(weekStart, weekEnd);
// 获取本月的时间范围
const { start: monthStart, end: monthEnd } =
this.countCommService.getTimeRange('month');
// 本月
const month = await createQuery(monthStart, monthEnd);
// 获取年的时间范围
const { start: yearStart, end: yearEnd } =
this.countCommService.getTimeRange('year');
// 今年
const year = await createQuery(yearStart, yearEnd);
return { total, today, week, month, year };
}
/**
*
* @param dayCount
* @param type count- amount-
*/
async chart(dayCount = 30, type: 'count' | 'amount' = 'count') {
if (type == 'count') {
return await this.countCommService.chart('order_info', dayCount);
}
if (type == 'amount') {
return await this.countCommService.chart(
'order_info',
dayCount,
'SUM(a.price)'
);
}
}
}

View File

@ -0,0 +1,69 @@
import { Init, Inject, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { UserInfoEntity } from '../../user/entity/info';
import { CountCommService } from './comm';
/**
*
*/
@Provide()
export class CountUserService extends BaseService {
@InjectEntityModel(UserInfoEntity)
userInfoEntity: Repository<UserInfoEntity>;
@Inject()
countCommService: CountCommService;
/**
*
*/
async summary() {
// 定义一个函数来创建查询
const createQuery = async (start, end) => {
return await this.userInfoEntity
.createQueryBuilder('a')
.where('a.createTime >= :start', { start })
.andWhere('a.createTime <= :end', { end })
.getCount();
};
// 总数
const total = await this.userInfoEntity.count();
// 获取今天的时间范围
const { start: todayStart, end: todayEnd } =
this.countCommService.getTimeRange('day');
// 今天
const today = await createQuery(todayStart, todayEnd);
// 获取本周的时间范围
const { start: weekStart, end: weekEnd } =
this.countCommService.getTimeRange('week');
// 本周
const week = await createQuery(weekStart, weekEnd);
// 获取本月的时间范围
const { start: monthStart, end: monthEnd } =
this.countCommService.getTimeRange('month');
// 本月
const month = await createQuery(monthStart, monthEnd);
// 获取年的时间范围
const { start: yearStart, end: yearEnd } =
this.countCommService.getTimeRange('year');
// 今年
const year = await createQuery(yearStart, yearEnd);
return { total, today, week, month, year };
}
/**
*
* @param dayCount
*/
async chart(dayCount = 30) {
return await this.countCommService.chart('user_info', dayCount);
}
}

19
src/modules/cs/config.ts Normal file
View File

@ -0,0 +1,19 @@
import { ModuleConfig } from '@cool-midway/core';
/**
*
*/
export default () => {
return {
// 模块名称
name: '客服模块',
// 模块描述
description: '客服系统',
// 中间件,只对本模块有效
middlewares: [],
// 中间件,全局有效
globalMiddlewares: [],
// 模块加载顺序默认为0值越大越优先加载
order: 0,
} as ModuleConfig;
};

View File

@ -0,0 +1,58 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { CsMsgEntity } from '../../entity/msg';
import { UserInfoEntity } from '../../../user/entity/info';
import { BaseSysUserEntity } from '../../../base/entity/sys/user';
import { CsMsgService } from '../../service/msg';
import { Body, Get, Inject, Post } from '@midwayjs/core';
/**
*
*/
@CoolController({
api: ['page'],
entity: CsMsgEntity,
service: CsMsgService,
pageQueryOp: {
select: [
'a.*',
'b.nickName',
'b.avatarUrl',
'c.name as adminUserName',
'c.headImg as adminUserHeadImg',
],
join: [
{
entity: UserInfoEntity,
alias: 'b',
condition: 'a.userId = b.id and a.type = 0',
},
{
entity: BaseSysUserEntity,
alias: 'c',
condition: 'a.userId = c.id and a.type = 1',
},
],
where: ctx => {
const { sessionId } = ctx.request.body;
return [['a.sessionId = :sessionId', { sessionId }]];
},
},
})
export class AdminCsMsgController extends BaseController {
@Inject()
ctx;
@Inject()
csMsgService: CsMsgService;
@Get('/unreadCount', { summary: '未读消息数' })
async unreadCount() {
return this.ok(await this.csMsgService.unreadCount(null, 0));
}
@Post('/read', { summary: '标记已读' })
async read(@Body('msgIds') msgIds: number[]) {
this.csMsgService.read(msgIds);
return this.ok();
}
}

View File

@ -0,0 +1,22 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { CsSessionEntity } from '../../entity/session';
import { UserInfoEntity } from '../../../user/entity/info';
/**
*
*/
@CoolController({
api: ['page'],
entity: CsSessionEntity,
pageQueryOp: {
select: ['a.*', 'b.nickName', 'b.avatarUrl'],
join: [
{
entity: UserInfoEntity,
alias: 'b',
condition: 'a.userId = b.id',
},
],
},
})
export class Controller extends BaseController {}

View File

@ -0,0 +1,62 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { CsMsgEntity } from '../../entity/msg';
import { CsSessionEntity } from '../../entity/session';
import { BaseSysUserEntity } from '../../../base/entity/sys/user';
import { UserInfoEntity } from '../../../user/entity/info';
import { Body, Get, Inject, Post } from '@midwayjs/core';
import { CsMsgService } from '../../service/msg';
/**
*
*/
@CoolController({
api: ['page'],
entity: CsMsgEntity,
pageQueryOp: {
select: [
'a.*',
'b.nickName',
'b.avatarUrl',
'c.name as adminUserName',
'c.headImg as adminUserHeadImg',
],
join: [
{
entity: UserInfoEntity,
alias: 'b',
condition: 'a.userId = b.id and a.type = 0',
},
{
entity: BaseSysUserEntity,
alias: 'c',
condition: 'a.userId = c.id and a.type = 1',
},
{
entity: CsSessionEntity,
alias: 'd',
condition: 'a.sessionId = d.id',
},
],
where: ctx => {
return [['d.userId = :userId', { userId: ctx.user?.id }]];
},
},
})
export class AppCsMsgController extends BaseController {
@Inject()
ctx;
@Inject()
csMsgService: CsMsgService;
@Get('/unreadCount', { summary: '未读消息数' })
async unreadCount() {
return this.ok(await this.csMsgService.unreadCount(this.ctx.user?.id, 1));
}
@Post('/read', { summary: '标记已读' })
async read(@Body('msgIds') msgIds: number[]) {
this.csMsgService.read(msgIds);
return this.ok();
}
}

View File

@ -0,0 +1,25 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { Get, Inject, Post } from '@midwayjs/core';
import { CsSessionService } from '../../service/session';
/**
*
*/
@CoolController()
export class Controller extends BaseController {
@Inject()
csSessionService: CsSessionService;
@Inject()
ctx;
@Get('/detail', { summary: '会话详情' })
async detail() {
return this.ok(await this.csSessionService.detail(this.ctx.user?.id));
}
@Post('/create', { summary: '创建会话' })
async create() {
return this.ok(await this.csSessionService.create(this.ctx.user?.id));
}
}

Some files were not shown because too many files have changed in this diff Show More