feat:基本功能完成

This commit is contained in:
fanxb 2022-02-18 16:59:56 +08:00
parent 8917a985b6
commit 50b5dbfe95
78 changed files with 28035 additions and 829 deletions

8
.env Normal file
View File

@ -0,0 +1,8 @@
# 修改配置
MYSQL_ADDRESS=localhost:3306
MYSQL_USER=root
MYSQL_PASSWORD=123456
REDIS_HOST=localhost
REDIS_PORT=6379
# 服务端口
NGINX_PORT=8080

109
.gitignore vendored
View File

@ -1,109 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
sqliteHistory.json
mysqlHistory.json
database.db

20
.vscode/launch.json vendored
View File

@ -1,20 +0,0 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/dist/index.js",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
}
]
}

View File

@ -1,22 +0,0 @@
import { Context } from "koa";
import { create } from "svg-captcha";
import { v4 as uuid } from 'uuid';
import { RedisHelper } from '../util/RedisHelper';
const router = {};
/**
*
*/
router["GET /captcha"] = async function (ctx: Context) {
let key: string = uuid().replaceAll("-", "");
let obj = create();
await RedisHelper.client.set(key, obj.text);
ctx.body = {
key,
data: obj.data
};
};
export default router;

View File

@ -1,6 +0,0 @@
import { Context } from "koa";
const router = {};
export default router;

View File

@ -1,50 +0,0 @@
import * as path from 'path';
//后台所在绝对路径
const rootPath = path.resolve(__dirname, '..');
let config = {
rootPath,
port: process.env.PORT ? parseInt(process.env.PORT) : 8089,
urlPrefix: '/qiezi/api',
//是否为windows平台
isWindows: process.platform.toLocaleLowerCase().includes("win"),
//redis相关配置
redis: {
enable: true,
url: "redis://localhost:6379"
},
//sqlite相关配置
sqlite: {
enable: false, //是否启用sqlite
//相对于项目根目录
filePath: "database.db",
//sql存放地址用于执行sql
sqlFolder: "sqliteSqls"
},
//mysql相关配置
mysql: {
enable: true, //是否启用mysql
sqlFolder: "mysqlSqls",
connection: {
host: "localhost",
port: 3306,
user: "root",
password: "123456",
database: "qiezi",
}
},
bodyLimit: {
formLimit: '2mb',
urlencoded: true,
multipart: true,
formidable: {
uploadDir: path.join(rootPath, 'files', 'temp', 'uploads'),
keepExtenstions: true,
maxFieldsSize: 1024 * 1024
}
}
};
export default config;

52
config/nginx.conf Normal file
View File

@ -0,0 +1,52 @@
user root;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
# Basic Settings
sendfile on;
# SSL Settings
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# Logging Settings
access_log /dev/stdout;
error_log /dev/stderr;
# Gzip Settings
gzip on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;
gzip_min_length 1K;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_prefer_server_ciphers on;
server {
listen 8080;
listen [::]:8080;
index index.html;
root /opt/dist/;
server_name _;
location /bookmark/api/ {
proxy_pass http://backend:8088;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 100m;
}
location /files/public/{
root /opt;
}
location / {
root /opt/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
}
}

1
config/timezone Normal file
View File

@ -0,0 +1 @@
Asia/Shanghai

View File

@ -1,16 +0,0 @@
import ErrorHelper from "../util/ErrorHelper";
import SqliteHelper from "../util/SqliteHelper";
export default class ApplicationRuleDao {
/**
*
* @param obj
* @returns
*/
static async getAll(): Promise<Array<any>> {
let res = await SqliteHelper.pool.all('select id,createdDate,updatedDate,name,comment,content from application_rule');
return res;
}
}

38
docker-compose.yml Normal file
View File

@ -0,0 +1,38 @@
version: "2"
services:
front:
image: nginx
networks:
- bookmark
volumes:
- /etc/localtime:/etc/localtime
- ./config/timezone:/etc/timezone
- ./qiezi_front/dist:/opt/dist
- ./data/nginx/nginx.conf:/etc/nginx/nginx.conf
ports:
- ${NGINX_PORT}:8080
backend:
image: openjdk:11.0
networks:
- bookmark
volumes:
- /etc/localtime:/etc/localtime
- ./config/timezone:/etc/timezone
- ./qieziBackend/target/backend-0.0.1-SNAPSHOT.jar:/opt/app/service.jar
working_dir: /opt/app
command:
- /bin/bash
- -c
- |
sleep 5 && \
java -jar -Dspring.profiles.active=prd \
-Dmybatis-plus.configuration.log-impl=org.apache.ibatis.logging.nologging.NoLoggingImpl \
-Dspring.datasource.user='${MYSQL_USER}' \
-Dspring.datasource.password='${MYSQL_PASSWORD}' \
-Dspring.datasource.url=jdbc:mysql://${MYSQL_ADDRESS}/bookmark?useUnicode=true\&characterEncoding=utf-8\&useSSL=false\&useJDBCCompliantTimezoneShift=true\&useLegacyDatetimeCode=false\&serverTimezone=UTC \
-Dspring.redis.host=${REDIS_HOST} \
-Dspring.redis.port=${REDIS_PORT} \
service.jar
networks:
bookmark:

View File

@ -1,30 +0,0 @@
export default class HostPo {
/**
* id
*/
id: number;
/**
* key,
*/
key: string;
/**
*
*/
secret: string;
/**
*
*/
name: string;
/**
*
*/
host: string;
/**
* pv
*/
pv: number;
/**
* uv
*/
uv: number;
}

View File

View File

@ -1,52 +0,0 @@
import koa from "koa";
import Router from "koa-router";
import koaBody from "koa-body";
import * as path from "path";
import RouterMW from "./middleware/controllerEngine";
import config from "./config";
import handleError from "./middleware/handleError";
import init from "./middleware/init";
import SqliteUtil from './util/SqliteHelper';
import log from './util/LogUtil';
import { MysqlUtil } from "./util/MysqlHelper";
import { RedisHelper } from "./util/RedisHelper";
log.info(config);
const app = new koa();
let router = new Router({
prefix: config.urlPrefix
});
app.use(require('koa-static')(path.join(config.rootPath, 'static')));
//表单解析
app.use(koaBody(config.bodyLimit));
//请求预处理
app.use(init);
//错误处理
app.use(handleError);
app.use(RouterMW(router, path.join(config.rootPath, "dist/api")));
(async () => {
//初始化sqlite
if (config.sqlite.enable) {
await SqliteUtil.createPool();
}
//初始化mysql
if (config.mysql.enable) {
await MysqlUtil.createPool();
}
//初始化redis
if (config.redis.enable) {
await RedisHelper.create();
}
app.listen(config.port);
log.info(`server listened `, config.port);
})();
app.on("error", (error) => {
console.error(error);
})

View File

@ -1,50 +0,0 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import log from '../util/LogUtil';
async function addMapping(router, filePath: string) {
let mapping = require(filePath).default;
for (let url in mapping) {
if (url.startsWith('GET ')) {
let temp = url.substring(4);
router.get(temp, mapping[url]);
log.info(`----GET${temp}`);
} else if (url.startsWith('POST ')) {
let temp = url.substring(5);
router.post(temp, mapping[url]);
log.info(`----POST${temp}`);
} else if (url.startsWith('PUT ')) {
let temp = url.substring(4);
router.put(temp, mapping[url]);
log.info(`----PUT${temp}`);
} else if (url.startsWith('DELETE ')) {
let temp = url.substring(7);
router.delete(temp, mapping[url]);
log.info(`----DELETE: ${temp}`);
} else {
log.info(`xxxxx无效路径${url}`);
}
}
}
function addControllers(router, filePath) {
let files = fs.readdirSync(filePath).filter(item => item.endsWith('.js'));
for (let index in files) {
let element = files[index];
let temp = path.join(filePath, element);
let state = fs.statSync(temp);
if (state.isDirectory()) {
addControllers(router, temp);
} else {
if (!temp.endsWith('Helper.js')) {
log.info('\n--开始处理: ' + element + '路由');
addMapping(router, temp);
}
}
}
}
export default function engine(router, folder) {
addControllers(router, folder);
return router.routes();
}

View File

@ -1,17 +0,0 @@
import log from '../util/LogUtil';
let f = async (ctx, next) => {
try {
await next();
} catch (error: any) {
if (error.status != undefined) {
ctx.status = error.status;
} else {
ctx.status = 500;
}
ctx.body = error.message;
log.error(error);
}
}
export default f;

View File

@ -1,51 +0,0 @@
import config from '../config';
import ObjectHelper from '../util/ObjectOperate';
let doSuccess = (ctx, body) => {
switch (ctx.method) {
case 'GET':
ctx.status = body !== null ? 200 : 204;
ctx.body = body;
break;
case 'POST':
ctx.status = body !== null ? 201 : 204;
ctx.body = body;
break;
case 'PUT':
ctx.status = body !== null ? 200 : 204;
ctx.body = body;
break;
case 'DELETE':
ctx.status = body !== null ? 200 : 204;
ctx.body = body;
break;
}
Object.assign(ctx.allParams, ctx.params);
}
export default async (ctx, next) => {
//跨域
ctx.set("Access-Control-Allow-Origin", "*");
ctx.set("Access-Control-Allow-Headers", "X-Requested-With");
ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
ctx.set("X-Powered-By", ' 3.2.1');
ctx.set("Content-Type", "application/json;charset=utf-8");
//合并请求参数到allParams
let objs = new Array();
if (ctx.method == "POST" || ctx.method == "PUT") {
if (ctx.request.body) {
if (ctx.request.body.fields != undefined && ctx.request.body.files != undefined) {
objs.push(ctx.request.body.fields, ctx.request.body.files);
} else {
objs.push(ctx.request.body);
}
}
}
objs.push(ctx.query);
ctx.allParams = ObjectHelper.combineObject(objs);
ctx.onSuccess = function (body = null) {
doSuccess(ctx, body);
};
await next();
}

View File

@ -1,46 +0,0 @@
CREATE TABLE qiezi.host (
id INT auto_increment NOT NULL,
`key` CHAR(32) NOT NULL COMMENT 'key用于标识',
secret char(32) NOT NULL COMMENT '密钥',
name varchar(100) NOT NULL COMMENT '网站名',
host varchar(100) NOT NULL COMMENT '网站域名不含http前缀以及路径',
pv INT UNSIGNED DEFAULT 0 NOT NULL,
uv INT UNSIGNED DEFAULT 0 NOT NULL,
CONSTRAINT host_pk PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci
COMMENT='host表记录某个站点总的pv,uv数据';
CREATE UNIQUE INDEX host_key_IDX USING BTREE ON qiezi.host (`key`);
CREATE TABLE qiezi.host_day(
id INT auto_increment NOT NULL,
hostId INT NOT NULL COMMENT 'hostId',
dateNum INT NOT NULL COMMENT '日期比如20200202',
pv INT UNSIGNED DEFAULT 0 NOT NULL,
uv INT UNSIGNED DEFAULT 0 NOT NULL,
CONSTRAINT detail_page_pk PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci
COMMENT='记录域名日pv/uv';
CREATE INDEX detail_page_host_id_date_IDX USING BTREE ON qiezi.host_day(`hostId`,`dateNum`);
CREATE TABLE qiezi.detail_page(
id INT auto_increment NOT NULL,
hostId INT NOT NULL COMMENT 'hostId',
pv INT UNSIGNED DEFAULT 0 NOT NULL,
uv INT UNSIGNED DEFAULT 0 NOT NULL,
CONSTRAINT detail_page_pk PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci
COMMENT='detail表记录细分页面pv/uv';
CREATE INDEX detail_page_host_id_IDX USING BTREE ON qiezi.detail_page(`hostId`);

View File

@ -1,32 +0,0 @@
{
"name": "nas_backup",
"version": "1.0.0",
"description": "文件备份用",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "fxb",
"license": "ISC",
"dependencies": {
"@types/fs-extra": "^5.0.4",
"@types/koa": "^2.0.47",
"@types/node": "^11.13.4",
"axios": "^0.21.1",
"fs-extra": "^7.0.0",
"koa": "^2.13.4",
"koa-body": "^4.2.0",
"koa-router": "^10.1.1",
"koa-static": "^5.0.0",
"koa2-cors": "^2.0.6",
"log4js": "^6.3.0",
"moment": "^2.22.2",
"mysql2": "^2.3.3",
"redis": "^4.0.3",
"sqlite": "^4.0.23",
"sqlite3": "^5.0.2",
"svg-captcha": "^1.4.0",
"uuid": "^8.3.2",
"winston": "^3.1.0"
}
}

33
qieziBackend/.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

95
qieziBackend/pom.xml Normal file
View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fanxb</groupId>
<artifactId>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>qieziBackend</name>
<description>qieziBackend</description>
<properties>
<java.version>11</java.version>
<skipTests>true</skipTests>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.21</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,34 @@
package com.fanxb.backend;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication(scanBasePackages = "com.fanxb.backend")
@EnableScheduling
@EnableTransactionManagement
public class QieziBackendApplication {
public static void main(String[] args) {
SpringApplication.run(QieziBackendApplication.class, args);
}
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
// 1.定义一个converters转换消息的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
// 2.添加fastjson的配置信息比如: 是否需要格式化返回的json数据
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);
// 3.在converter中添加配置信息
fastConverter.setFastJsonConfig(fastJsonConfig);
// 5.返回HttpMessageConverters对象
return new HttpMessageConverters(fastConverter);
}
}

View File

@ -0,0 +1,45 @@
package com.fanxb.backend.config;
import com.fanxb.backend.entity.ResultObject;
import com.fanxb.backend.entity.exception.CustomBaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
import java.util.Objects;
/**
* 全局异常处理类
*
* @author fanxb
* @date 2022/2/16 15:28
*/
@RestControllerAdvice
@Slf4j
public class ExceptionHandleConfig {
@ExceptionHandler(Exception.class)
public ResultObject handleException(Exception e) {
CustomBaseException be;
if (e instanceof CustomBaseException) {
be = (CustomBaseException) e;
//手动抛出的异常仅记录message
log.info("baseException:{}", be.getMessage());
} else if (e instanceof ConstraintViolationException) {
//url参数数组类参数校验报错类
ConstraintViolationException ce = (ConstraintViolationException) e;
//针对参数校验异常建立了一个异常类
be = new CustomBaseException(ce.getMessage());
} else if (e instanceof MethodArgumentNotValidException) {
//json对象类参数校验报错类
MethodArgumentNotValidException ce = (MethodArgumentNotValidException) e;
be = new CustomBaseException(Objects.requireNonNull(ce.getFieldError()).getDefaultMessage());
} else {
//其它异常非自动抛出的,无需给前端返回具体错误内容用户不需要看见空指针之类的异常信息
log.error("other exception:{}", e.getMessage(), e);
be = new CustomBaseException("系统异常,请稍后重试", e);
}
return new ResultObject(be.getCode(), be.getMessage(), null);
}
}

View File

@ -0,0 +1,24 @@
package com.fanxb.backend.constants;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* TODO
*
* @author fanxb
* @date 2022/2/15 16:41
*/
@Component
public class CommonConstant {
/**
* 是否开发环境
*/
public static boolean IS_DEV = false;
@Value("${spring.profiles.active}")
public void setIsDev(String env) {
IS_DEV = env.contains("dev");
}
}

View File

@ -0,0 +1,25 @@
package com.fanxb.backend.constants;
/**
* redis相关前后缀
*
* @author fanxb
* @date 2022/2/15 15:28
*/
public class RedisConstant {
/**
* 应用注册用
*/
public static final String APPLICATION_SIGN_PRE = "application_sign_";
/**
* key对应hostId关系
*/
public static final String KEY_ID_PRE = "key_id_";
/**
* 页面uv前缀
*/
public static final String HOST_UV_PRE = "host_uv_pre_";
public static final String PAGE_UV_PRE = "page_uv_pre_";
}

View File

@ -0,0 +1,51 @@
package com.fanxb.backend.controller;
import com.fanxb.backend.entity.ResultObject;
import com.fanxb.backend.entity.dto.ApplicationSignDto;
import com.fanxb.backend.service.ApplicationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;
import java.io.IOException;
/**
* @author fanxb
* @date 2022/2/15 15:04
*/
@RestController
@RequestMapping("/application")
@Validated
public class ApplicationController {
private final ApplicationService applicationService;
@Autowired
public ApplicationController(ApplicationService applicationService) {
this.applicationService = applicationService;
}
/**
* 注册
*/
@PostMapping("/sign")
public ResultObject sign(@RequestBody ApplicationSignDto signDto) {
return ResultObject.success(applicationService.sign(signDto));
}
/**
* 页面访问
*
* @param callBack 回调函数
* @param key key
* @author fanxb
* date 2022/2/16 15:24
*/
@GetMapping("/visit")
public void visit(HttpServletRequest request, HttpServletResponse response,
@NotBlank(message = "回调函数不能为空") String callBack, @NotBlank(message = "key不能为空") String key) throws IOException {
applicationService.visit(request, response, callBack, key);
}
}

View File

@ -0,0 +1,35 @@
package com.fanxb.backend.controller;
import com.fanxb.backend.constants.RedisConstant;
import com.fanxb.backend.entity.ResultObject;
import com.fanxb.backend.service.CaptchaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 验证码
*
* @author fanxb
* @date 2022/2/15 16:01
*/
@RestController()
@RequestMapping("/captcha")
public class CaptchaController {
private final CaptchaService captchaService;
@Autowired
public CaptchaController(CaptchaService captchaService) {
this.captchaService = captchaService;
}
/**
* 注册用验证码
*/
@GetMapping("/sign")
public ResultObject signCaptcha() {
return ResultObject.success(captchaService.create(RedisConstant.APPLICATION_SIGN_PRE));
}
}

View File

@ -0,0 +1,47 @@
package com.fanxb.backend.dao;
import com.fanxb.backend.entity.po.DetailPagePo;
import com.fanxb.backend.entity.po.HostPo;
import org.apache.ibatis.annotations.*;
/**
* @author fanxb
* @date 2022/2/15 16:37
*/
@Mapper
public interface DetailPageDao {
/***
* 新增一个(uv,pv初始化为1)
*
* @param detailPagePo 页面详情
*
* @author fanxb
* date 2022/2/15 16:39
*/
@Insert("insert into detail_page(hostId,path,pv,uv) value(#{hostId},#{path},#{pv},#{uv})")
void insertOne(DetailPagePo detailPagePo);
/**
* 获取uv,pv
*
* @param hostId hostId
* @param path path
* @return {@link HostPo}
* @author fanxb
* date 2022/2/16 11:11
*/
@Select("select id,hostId,path,uv,pv from detail_page where hostId=#{hostId} and path = #{path}")
DetailPagePo getUvPvById(@Param("hostId") int hostId, @Param("path") String path);
/**
* 更新uv,pv
*
* @param id id
* @param uvIncrement uv增量
* @author fanxb
* date 2022/2/16 11:13
*/
@Update("update detail_page set uv=uv+#{uvIncrement},pv=pv+1 where id=#{id}")
void updateUvPv(@Param("id") int id, @Param("uvIncrement") int uvIncrement);
}

View File

@ -0,0 +1,56 @@
package com.fanxb.backend.dao;
import com.fanxb.backend.entity.po.HostPo;
import org.apache.ibatis.annotations.*;
/**
* @author fanxb
* @date 2022/2/15 16:37
*/
@Mapper
public interface HostDao {
/***
* 新增一个
*
* @param host host
*
* @author fanxb
* date 2022/2/15 16:39
*/
@Insert("insert into host(`key`,secret,name,host,pv,uv) value(#{key},#{secret},#{name},#{host},0,0)")
@Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
void insertOne(HostPo host);
/**
* 根据key获取id
*
* @param key key
* @author fanxb
* date 2022/2/16 10:33
*/
@Select("select id from host where `key` = #{key}")
Integer getIdByKey(String key);
/**
* 获取uv,pv
*
* @param id id
* @return {@link HostPo}
* @author fanxb
* date 2022/2/16 11:11
*/
@Select("select id,uv,pv from host where id=#{id}")
HostPo getUvPvById(int id);
/**
* 更新uv,pv
*
* @param id id
* @param uvIncrement uv增量
* @author fanxb
* date 2022/2/16 11:13
*/
@Update("update host set uv=uv+#{uvIncrement},pv=pv+1 where id=#{id}")
void updateUvPv(@Param("id") int id, @Param("uvIncrement") int uvIncrement);
}

View File

@ -0,0 +1,41 @@
package com.fanxb.backend.entity;
import lombok.Data;
/**
* 统一返回类
*
* @author fanxb
* @date 2019/3/19 18:05
*/
@Data
public class ResultObject {
/**
* 状态1正常0异常-1未认证
*/
private int code;
private String message;
private Object data;
public ResultObject() {
}
public ResultObject(int code, String message, Object data) {
this.code = code;
this.message = message;
this.data = data;
}
public static ResultObject unAuth() {
return new ResultObject(-1, "", null);
}
public static ResultObject success() {
return new ResultObject(1, null, null);
}
public static ResultObject success(Object data) {
return new ResultObject(1, null, data);
}
}

View File

@ -0,0 +1,25 @@
package com.fanxb.backend.entity.dto;
import lombok.Data;
/**
* 注册
*
* @author fanxb
* @date 2022/2/15 16:29
*/
@Data
public class ApplicationSignDto {
/**
* 验证码code
*/
private String code;
/**
* 网站名
*/
private String name;
/**
* 域名
*/
private String host;
}

View File

@ -0,0 +1,49 @@
package com.fanxb.backend.entity.exception;
import cn.hutool.core.util.StrUtil;
/**
* 类功能简述 自定义错误类默认错误码为0,表示一般错误
* 类功能详述
*
* @author fanxb
* @date 2019/3/19 18:09
*/
public class CustomBaseException extends RuntimeException {
private String message;
/**
* 默认0
*/
private final int code = 0;
public CustomBaseException() {
this(null, null);
}
public CustomBaseException(String message) {
this(message, null);
}
public CustomBaseException(Exception e) {
this(null, e);
}
public CustomBaseException(String message, Exception e) {
super(e);
this.message = message;
}
@Override
public String getMessage() {
if (StrUtil.isEmpty(this.message)) {
return super.getMessage();
} else {
return this.message;
}
}
public int getCode() {
return code;
}
}

View File

@ -0,0 +1,36 @@
package com.fanxb.backend.entity.po;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 页面路径详情
*
* @author fanxb
* date 2022/2/15 11:12
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Accessors(chain = true)
public class DetailPagePo {
/**
* id
*/
private Integer id;
/**
* 域名
*/
private int hostId;
/**
* 页面路径
*/
private String path;
private long pv;
private long uv;
}

View File

@ -0,0 +1,24 @@
package com.fanxb.backend.entity.po;
import lombok.Data;
@Data
public class HostDayPo {
/**
* id
*/
private int id;
/**
* hostId
*/
private int hostId;
/**
* 日期20200202
*/
private int dateNum;
private long pv;
private long uv;
}

View File

@ -0,0 +1,33 @@
package com.fanxb.backend.entity.po;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HostPo {
private int id;
/**
* 标识key
*/
private String key;
/**
* 密钥
*/
private String secret;
/**
* 网站名
*/
private String name;
/**
* 网站域名
*/
private String host;
private long pv;
private long uv;
}

View File

@ -0,0 +1,19 @@
package com.fanxb.backend.entity.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 应用注册vo
*
* @author fanxb
* @date 2022/2/15 16:26
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationSignVo {
private String key;
private String secret;
}

View File

@ -0,0 +1,21 @@
package com.fanxb.backend.entity.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* uv,pv数据
*
* @author fanxb
* @date 2022/2/16 15:16
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UvPvVo {
private long totalUv;
private long totalPv;
private long pageUv;
private long pagePv;
}

View File

@ -0,0 +1,55 @@
package com.fanxb.backend.factory;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ThreadFactory;
/**
* 类功能简述
* 类功能详述线程工厂
*
* @author fanxb
* @date 2018/10/12 11:29
*/
public class CustomThreadFactory implements ThreadFactory {
/**
* 记录创建线程数
*/
private int counter;
/**
* 线程工程名
*/
private String name;
/**
* 记录最近1000条创建历史
*/
private List<String> history;
private int historyLength;
public CustomThreadFactory(String name) {
this.name = name;
this.history = new LinkedList<>();
this.historyLength = 1000;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, name + "-Thread-" + counter);
this.counter++;
history.add(String.format("Created thread %d with name %s on %s \n", t.getId(), t.getName(), new Date().toString()));
if (history.size() >= this.historyLength) {
history.remove(0);
}
return t;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
this.history.forEach(builder::append);
return builder.toString();
}
}

View File

@ -0,0 +1,130 @@
package com.fanxb.backend.factory;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 类功能简述 线程池工厂可使用此工厂创建线程池,等待队列使用ArrayBlockingQueue
* 类功能详述
*
* @author fanxb
* @date 2018/11/1 10:57
*/
public class ThreadPoolFactory {
/**
* 默认核心池大小
*/
public static final int DEFAULT_CORE_POOL_SIZE = 5;
/**
* 默认最大线程数
*/
public static final int DEFAULT_MAX_POOL_SIZE = 30;
/**
* 默认空闲线程回收时间毫秒
*/
public static final long DEFAULT_KEEP_ACTIVE_TIME = 1000;
/**
* 默认等待队列长度
*/
public static final int DEFAULT_QUEUE_LENGTH = 2000;
/**
* Description: 使用默认配置创建一个连接池
*
* @param factoryName 线程工厂名
* @return java.util.concurrent.ThreadPoolExecutor
* @author fanxb
* @date 2018/10/12 13:38
*/
public static ThreadPoolExecutor createPool(String factoryName) {
ThreadFactory threadFactory = new CustomThreadFactory(factoryName);
return createPool(DEFAULT_CORE_POOL_SIZE, DEFAULT_MAX_POOL_SIZE, DEFAULT_KEEP_ACTIVE_TIME, DEFAULT_QUEUE_LENGTH, threadFactory);
}
/**
* Description: 提供参数创建一个连接池
*
* @param corePoolSize 核心池大小
* @param maxPoolSize 线程池最大线程数
* @param keepActiveTime 空闲线程回收时间ms)
* @param queueLength 等待队列长度
* @return java.util.concurrent.ThreadPoolExecutor
* @author fanxb
* @date 2018/10/12 13:39
*/
public static ThreadPoolExecutor createPool(int corePoolSize, int maxPoolSize, long keepActiveTime
, int queueLength, String threadName) {
return new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepActiveTime
, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueLength), new CustomThreadFactory(threadName));
}
/**
* Description: 提供参数创建一个连接池,自己提供线程工厂
*
* @param corePoolSize 核心池大小
* @param maxPoolSize 线程池最大线程数
* @param keepActiveTime 空闲线程回收时间ms)
* @param queueLength 等待队列长度
* @param factory 线程工厂
* @return java.util.concurrent.ThreadPoolExecutor
* @author fanxb
* @date 2018/10/12 13:39
*/
public static ThreadPoolExecutor createPool(int corePoolSize, int maxPoolSize, long keepActiveTime
, int queueLength, ThreadFactory factory) {
return new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepActiveTime
, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueLength), factory);
}
/**
* Description: 强制关闭线程池不等待已有任务的完成
*
* @param executor 被关闭线程池对象
* @return List<Runnable>
* @author fanxb
* @date 2018/10/12 13:42
*/
public static List<Runnable> forceShutdown(ThreadPoolExecutor executor) {
if (executor != null) {
return executor.shutdownNow();
} else {
return null;
}
}
/**
* Description: 关闭一个连接池等待已有任务完成
*
* @param executor 被关闭线程池对象
* @return void
* @author fanxb
* @date 2018/10/12 13:45
*/
public static void shutdown(ThreadPoolExecutor executor) {
if (executor == null) {
return;
}
executor.shutdown();
try {
int count = 0;
int timeOut = 2;
while (executor.awaitTermination(timeOut, TimeUnit.SECONDS)) {
count++;
if (count == 100) {
executor.shutdownNow();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,39 @@
package com.fanxb.backend.service;
import com.fanxb.backend.entity.dto.ApplicationSignDto;
import com.fanxb.backend.entity.vo.ApplicationSignVo;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 应用管理
*
* @author fanxb
* @date 2022/2/15 16:25
*/
public interface ApplicationService {
/**
* 应用注册
*
* @param signDto dto
* @return {@link ApplicationSignVo}
* @author fanxb
* date 2022/2/15 16:32
*/
ApplicationSignVo sign(ApplicationSignDto signDto);
/**
* 页面访问
*
* @param request request
* @param response response
* @param callBack callBack
* @param key key
* @author fanxb
* date 2022/2/16 10:20
*/
void visit(HttpServletRequest request, HttpServletResponse response, String callBack, String key) throws IOException;
}

View File

@ -0,0 +1,20 @@
package com.fanxb.backend.service;
/**
* 验证码相关类
*
* @author fanxb
* @date 2022/2/15 15:27
*/
public interface CaptchaService {
/***
* 返回验证码base64
*
* @param type 验证码类别
* @return {@link String}
* @author fanxb
* date 2022/2/15 16:04
*/
String create(String type);
}

View File

@ -0,0 +1,156 @@
package com.fanxb.backend.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import com.alibaba.fastjson.JSON;
import com.fanxb.backend.constants.RedisConstant;
import com.fanxb.backend.constants.CommonConstant;
import com.fanxb.backend.dao.DetailPageDao;
import com.fanxb.backend.dao.HostDao;
import com.fanxb.backend.entity.dto.ApplicationSignDto;
import com.fanxb.backend.entity.exception.CustomBaseException;
import com.fanxb.backend.entity.po.DetailPagePo;
import com.fanxb.backend.entity.po.HostPo;
import com.fanxb.backend.entity.vo.ApplicationSignVo;
import com.fanxb.backend.entity.vo.UvPvVo;
import com.fanxb.backend.service.ApplicationService;
import com.fanxb.backend.util.NetUtil;
import com.fanxb.backend.util.ThreadPoolUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.concurrent.TimeUnit;
/**
* 应用管理
*
* @author fanxb
* @date 2022/2/15 16:34
*/
@Service
public class ApplicationServiceImpl implements ApplicationService {
private final StringRedisTemplate stringRedisTemplate;
private final HostDao hostDao;
private final DetailPageDao detailPageDao;
@Autowired
public ApplicationServiceImpl(StringRedisTemplate stringRedisTemplate, HostDao hostDao, DetailPageDao detailPageDao) {
this.stringRedisTemplate = stringRedisTemplate;
this.hostDao = hostDao;
this.detailPageDao = detailPageDao;
}
@Override
public ApplicationSignVo sign(ApplicationSignDto signDto) {
String redisKey = RedisConstant.APPLICATION_SIGN_PRE + signDto.getCode();
if (!CommonConstant.IS_DEV && stringRedisTemplate.opsForValue().get(redisKey) == null) {
throw new CustomBaseException("验证码错误");
}
stringRedisTemplate.delete(redisKey);
HostPo po = HostPo.builder().key(IdUtil.fastSimpleUUID()).secret(IdUtil.fastSimpleUUID())
.host(signDto.getHost())
.name(signDto.getName()).build();
hostDao.insertOne(po);
stringRedisTemplate.opsForValue().set(RedisConstant.KEY_ID_PRE + po.getKey(), String.valueOf(po.getId()));
return new ApplicationSignVo(po.getKey(), po.getSecret());
}
@Override
public void visit(HttpServletRequest request, HttpServletResponse response, String callBack, String key) throws IOException {
String refer = request.getHeader("Referer");
if (StrUtil.isEmpty(refer)) {
throw new CustomBaseException("未获取到来源路径");
}
String path;
try {
URL url = new URL(refer);
path = StrUtil.isEmpty(url.getPath()) ? "/" : url.getPath();
} catch (Exception e) {
throw new CustomBaseException("url解析错误", e);
}
if (path.length() > 100) {
throw new CustomBaseException("路径长度不能大于100," + path);
}
int hostId = getHostId(key);
HostPo uvPvData = hostDao.getUvPvById(hostId);
DetailPagePo detailUvPvData = detailPageDao.getUvPvById(hostId, path);
if (detailUvPvData == null) {
detailUvPvData = new DetailPagePo().setHostId(hostId).setPath(path).setUv(1).setPv(1);
}
UvPvVo uvPvVo = new UvPvVo(uvPvData.getUv(), uvPvData.getPv(), detailUvPvData.getUv(), detailUvPvData.getPv());
String uvPvVoStr = JSON.toJSONString(uvPvVo);
String res = String.format("try{%s(%s);}catch(e){console.error(e);console.log(%s)}", callBack, uvPvVoStr, uvPvVoStr);
response.setHeader(Header.CONTENT_TYPE.getValue(), ContentType.JSON.getValue());
response.getOutputStream().write(res.getBytes(StandardCharsets.UTF_8));
response.getOutputStream().flush();
//异步数据更新
DetailPagePo finalDetailUvPvData = detailUvPvData;
ThreadPoolUtil.execute(() -> updateData(NetUtil.getClientIp(request), uvPvData, finalDetailUvPvData));
}
/**
* 更新数据
*
* @param ip 来源ip
* @param hostPo hostPo
* @param detailPagePo detailPagePo
* @author fanxb
* date 2022/2/16 15:40
*/
private void updateData(String ip, HostPo hostPo, DetailPagePo detailPagePo) {
String hostKey = RedisConstant.HOST_UV_PRE + hostPo.getId() + ip;
String hostVal = stringRedisTemplate.opsForValue().get(hostKey);
hostDao.updateUvPv(hostPo.getId(), hostVal == null ? 1 : 0);
long tomorrowZero = LocalDate.now().plusDays(1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
if (hostVal == null) {
stringRedisTemplate.opsForValue().set(hostKey, "", tomorrowZero - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
String pageKey = RedisConstant.PAGE_UV_PRE + hostPo.getId() + ip + detailPagePo.getPath();
String pageVal = stringRedisTemplate.opsForValue().get(pageKey);
if (detailPagePo.getId() == null) {
detailPageDao.insertOne(detailPagePo);
} else {
detailPageDao.updateUvPv(detailPagePo.getId(), pageVal == null ? 1 : 0);
}
if (pageVal == null) {
stringRedisTemplate.opsForValue().set(pageKey, "", tomorrowZero - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
}
/**
* 获取hostID
*
* @param key key
* @return {@link int}
* @author fanxb
* date 2022/2/16 15:37
*/
private int getHostId(String key) {
String id = stringRedisTemplate.opsForValue().get(key);
if (id != null) {
return Integer.parseInt(id);
}
Integer hostId = hostDao.getIdByKey(key);
if (hostId == null) {
throw new CustomBaseException("key无效:" + key);
}
stringRedisTemplate.opsForValue().set(key, hostId.toString());
return hostId;
}
}

View File

@ -0,0 +1,37 @@
package com.fanxb.backend.service.impl;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.core.util.IdUtil;
import com.fanxb.backend.service.CaptchaService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 验证码service
*
* @author fanxb
* @date 2022/2/15 15:47
*/
@Service
@Slf4j
public class CaptchaServiceImpl implements CaptchaService {
private final StringRedisTemplate stringRedisTemplate;
@Autowired
public CaptchaServiceImpl(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public String create(String type) {
String key = IdUtil.fastSimpleUUID();
AbstractCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100);
stringRedisTemplate.opsForValue().set(type + captcha.getCode(), "1", 5, TimeUnit.MINUTES);
return captcha.getImageBase64();
}
}

View File

@ -0,0 +1,34 @@
package com.fanxb.backend.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.Header;
import javax.servlet.http.HttpServletRequest;
/**
* 网络相关工具类
*
* @author fanxb
* @date 2022/2/16 16:04
*/
public class NetUtil {
private static final String[] HEADERS = new String[]{"x-forwarded-for", "X-Real-IP"};
/**
* 获取客户端真实ip
*
* @param request request
* @return {@link String}
* @author fanxb
* date 2022/2/16 16:10
*/
public static String getClientIp(HttpServletRequest request) {
for (String header : HEADERS) {
String ip = request.getHeader(header);
if (StrUtil.isNotEmpty(ip)) {
return ip.split(",")[0];
}
}
return request.getRemoteAddr();
}
}

View File

@ -0,0 +1,61 @@
package com.fanxb.backend.util;
import com.fanxb.backend.factory.ThreadPoolFactory;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 类功能简述当短时间需要少量线程时,使用本类长时间占用或大量线程需求请用线程工厂ThreadPoolFactory创建新的线程池,
* 类功能详述
*
* @author fanxb
* @date 2018/11/1 11:04
*/
public class ThreadPoolUtil {
private static final ThreadPoolExecutor POOL_EXECUTOR = ThreadPoolFactory.createPool("global-pool-");
/**
* Description: 执行线程任务
*
* @param runnable 待执行对象
* @author fanxb
* @date 2018/11/1 11:27
*/
synchronized private static void executeTask(Runnable runnable) {
POOL_EXECUTOR.execute(runnable);
}
/**
* Description: 执行一个有返回值的任务
*
* @param callable 待执行对象
* @param timeOut 获取结果操时时间 , 操时抛出错误 单位
* @return T
* @author fanxb
* @date 2018/11/1 11:10
*/
@SuppressWarnings("unchecked")
public static <T> T execute(Callable<T> callable, int timeOut) throws Exception {
FutureTask<?> futureTask = new FutureTask<>(callable);
executeTask(futureTask);
return (T) futureTask.get(timeOut, TimeUnit.SECONDS);
}
/**
* Description: 执行一个无返回值任务
*
* @param runnable 待执行对象
* @author fanxb
* @date 2018/11/1 11:11
*/
public static void execute(Runnable runnable) {
executeTask(runnable);
}
}

View File

@ -0,0 +1,48 @@
server:
port: 8088
servlet:
# 不要在最后加/
context-path: /qiezi/api
spring:
jackson:
default-property-inclusion: non_null
servlet:
# 表单配置
multipart:
max-request-size: 10MB
max-file-size: 10MB
profiles:
active: dev
application:
name: qiezi
flyway:
baseline-on-migrate: true
cache:
type: redis
redis:
database: 0
host: localhost
port: 6379
password:
timeout: 20000ms
lettuce:
pool:
max-active: 20
max-wait: 500ms
max-idle: 20
min-idle: 2
datasource:
name: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
username: root
password: 123456
url: jdbc:mysql://localhost:3306/qiezi?useUnicode=true&characterEncoding=utf-8&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
hikari:
maximum-pool-size: 10
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# classpath后面加*,值里面的*才起作用
mapper-locations: classpath*:mapper/*.xml
debug: false

View File

@ -0,0 +1,46 @@
CREATE TABLE qiezi.host
(
id INT auto_increment NOT NULL,
`key` CHAR(32) NOT NULL COMMENT 'key用于标识',
secret char(32) NOT NULL COMMENT '密钥',
name varchar(100) NOT NULL COMMENT '网站名',
host varchar(100) NOT NULL COMMENT '网站域名不含http前缀以及路径',
pv INT UNSIGNED DEFAULT 0 NOT NULL,
uv INT UNSIGNED DEFAULT 0 NOT NULL,
CONSTRAINT host_pk PRIMARY KEY (id)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci
COMMENT='host表记录某个站点总的pv,uv数据';
CREATE UNIQUE INDEX host_key_IDX USING BTREE ON qiezi.host (`key`);
CREATE TABLE qiezi.host_day
(
id INT auto_increment NOT NULL,
hostId INT NOT NULL COMMENT 'hostId',
dateNum INT NOT NULL COMMENT '日期比如20200202',
pv INT UNSIGNED DEFAULT 0 NOT NULL,
uv INT UNSIGNED DEFAULT 0 NOT NULL,
CONSTRAINT detail_page_pk PRIMARY KEY (id)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci
COMMENT='记录域名日pv/uv';
CREATE INDEX detail_page_host_id_date_IDX USING BTREE ON qiezi.host_day(`hostId`,`dateNum`);
CREATE TABLE qiezi.detail_page
(
id INT auto_increment NOT NULL,
hostId INT NOT NULL COMMENT 'hostId',
path varchar(100) NOT NULL COMMENT '页面路径',
pv INT UNSIGNED DEFAULT 0 NOT NULL,
uv INT UNSIGNED DEFAULT 0 NOT NULL,
CONSTRAINT detail_page_pk PRIMARY KEY (id)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci
COMMENT='detail表记录细分页面pv/uv';
CREATE INDEX detail_page_host_id_IDX USING BTREE ON qiezi.detail_page(`hostId`,`path`(20));

View File

@ -0,0 +1,13 @@
package com.fanxb.backend;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class QieziBackendApplicationTests {
@Test
void contextLoads() {
}
}

23
qiezi_front/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

19
qiezi_front/README.md Normal file
View File

@ -0,0 +1,19 @@
# qiezi_front
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

26168
qiezi_front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
qiezi_front/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "qiezi_front",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"ant-design-vue": "^2.2.8",
"axios": "^0.26.0",
"core-js": "^3.6.5",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"less": "^3.0.4",
"less-loader": "^5.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script src="/indexResource/qieziStatistic_v1.js" type="text/javascript"></script>
</body>
</html>

View File

@ -0,0 +1 @@
<!-- 真正的主页 -->

View File

@ -0,0 +1,34 @@
(function () {
var name = "qieziStatistic9527";
var callback = name + "CallBack";
window[callback] = function (a) {
var hostNode = document.getElementById(name + "Host");
if (hostNode != null) {
document.getElementById(name + "HostPv").innerText = a.totalPv;
document.getElementById(name + "HostUv").innerText = a.totalUv;
hostNode.style.display = "inline";
}
var postNode = document.getElementById(name + "Post");
if (postNode != null) {
document.getElementById(name + "PostPv").innerText = a.pagePv;
postNode.style.display = "inline";
}
};
setTimeout(
() => {
var script = document.createElement("script");
script.type = "text/javascript";
script.defer = true;
script.src =
(window.qieziStatisticHost == undefined ? "https://qiezi.fleyx.com" : window.qieziStatisticHost) +
"/qiezi/api/application/visit?callBack=" +
callback +
"&key=" +
window.qieziStatisticKey;
document.getElementsByTagName("head")[0].appendChild(script);
},
window.qieziStatisticKey == undefined ? 1000 : 1,
);
})();

13
qiezi_front/src/App.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<router-view />
</template>
<style lang="less">
html {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,58 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

10
qiezi_front/src/main.js Normal file
View File

@ -0,0 +1,10 @@
import { createApp } from "vue";
import { message, Button, Form, Input, Typography } from "ant-design-vue";
import App from "./App.vue";
import router from "./router";
import "ant-design-vue/dist/antd.css";
const app = createApp(App);
app.use(router).use(Button).use(Form).use(Input).use(Typography).mount("#app");
app.config.globalProperties.$message = message;
window.globalVue = app;

View File

@ -0,0 +1,22 @@
import { createRouter, createWebHashHistory } from "vue-router";
import Home from "../views/Home.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/application/sign",
name: "ApplicationSign",
component: () => import("../views/ApplicationSign"),
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;

View File

@ -0,0 +1,106 @@
import * as http from "axios";
import router from "../router/index";
import { message as aMessage, notification } from "ant-design-vue";
/**
* 请求
* @param {*} url url
* @param {*} method 方法
* @param {*} params url参数
* @param {*} body 请求体
* @param {*} isForm 是否form
* @param {*} redirect 接口返回未认证是否跳转到登陆
* @returns 数据
*/
async function request(url, method, params, body, isForm, redirect) {
let options = {
url,
baseURL: "/qiezi/api",
method,
params,
headers: {
// "jwt-token": vuex.state.globalConfig.token,
},
};
//如果是表单类型的请求,添加请求头
if (isForm) {
options.headers["Content-Type"] = "multipart/form-data";
}
if (body) {
options.data = body;
}
let res;
try {
res = await http.default.request(options);
} catch (err) {
aMessage.error("发生了某些异常问题");
console.error(err);
return;
}
const { code, data, message } = res.data;
if (code === 1) {
return data;
} else if (code === -1 && redirect) {
//未登陆根据redirect参数判断是否需要跳转到登陆页
aMessage.error("您尚未登陆,请先登陆");
router.replace(`/public/login?redirect=${encodeURIComponent(router.currentRoute.fullPath)}`);
throw new Error(message);
} else if (code === 0) {
//通用异常使用error提示
notification.error({
message: "异常",
description: message,
});
throw new Error(message);
} else if (code === -2) {
//表单异常使用message提示
aMessage.error(message);
throw new Error(message);
}
}
/**
* get方法
* @param {*} url url
* @param {*} params url参数
* @param {*} redirect 未登陆是否跳转到登陆页
*/
async function get(url, params = null, redirect = true) {
return request(url, "get", params, null, false, redirect);
}
/**
* post方法
* @param {*} url url
* @param {*} params url参数
* @param {*} body body参数
* @param {*} isForm 是否表单数据
* @param {*} redirect 是否重定向
*/
async function post(url, params, body, isForm = false, redirect = true) {
return request(url, "post", params, body, isForm, redirect);
}
/**
* put方法
* @param {*} url url
* @param {*} params url参数
* @param {*} body body参数
* @param {*} isForm 是否表单数据
* @param {*} redirect 是否重定向
*/
async function put(url, params, body, isForm = false, redirect = true) {
return request(url, "put", params, body, isForm, redirect);
}
/**
* delete方法
* @param {*} url url
* @param {*} params url参数
* @param {*} redirect 是否重定向
*/
async function hDelete(url, params = null, redirect = true) {
return request(url, "delete", params, null, redirect);
}
export { get, post, put, hDelete };

View File

@ -0,0 +1,61 @@
<template>
<div class="title">应用注册</div>
<a-form :model="form" :label-col="{ span: 4 }" :wrapper-col="{ span: 14 }" style="width: 80%; margin: 0 auto">
<a-form-item label="网站名">
<a-input type="text" v-model:value="form.name" />
</a-form-item>
<a-form-item label="域名">
<a-input type="text" v-model:value="form.host" />
</a-form-item>
<a-form-item label="验证码">
<div class="captchaItem">
<a-input type="text" v-model:value="form.code" style="flex: 1" />
<img style="height: 3em; cursor: pointer" :src="imgData" alt="验证码" @click="onCaptchaClick" title="点击刷新" />
</div>
</a-form-item>
<a-form-item :wrapper-col="{ span: 14, offset: 4 }">
<a-button type="primary" @click="onSubmit">创建</a-button>
</a-form-item>
<a-form-item :wrapper-col="{ span: 14 }" v-if="keySecret.key" style="text-align: left">
<div>请注意记录以下的key和secret,key用于uv,pv统计时身份标识,secret用于后续进阶功能的身份认证</div>
key: <a-typography-text strong>{{ keySecret.key }}</a-typography-text
><br />
secret: <a-typography-text strong>{{ keySecret.secret }}</a-typography-text>
</a-form-item>
</a-form>
</template>
<script setup>
import { onMounted, ref, reactive } from "vue";
import { get, post } from "../util/HttpUtil";
let imgData = ref("");
let getCaptcha = async () => "data:image/png;base64," + (await get("/captcha/sign"));
onMounted(async () => {
window.qieziStatisticHost = "http://localhost:8080";
window.qieziStatisticKey = "238f2c45fa53454b95644280a12bc735";
imgData.value = await getCaptcha();
});
let onCaptchaClick = async () => (imgData.value = await getCaptcha());
let form = reactive({
code: "",
name: "",
host: "",
});
let keySecret = ref({});
let onSubmit = async () => {
keySecret.value = reactive(await post("/application/sign", null, form));
onCaptchaClick();
};
</script>
<style lang="less" scoped>
.title {
font-size: 2em;
font-weight: 600;
}
.captchaItem {
display: flex;
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<div class="home">
<router-link to="/application/sign">应用注册</router-link><br />
<router-link to="/manage">应用管理</router-link>
</div>
</template>
<script>
// @ is an alias to /src
export default {
name: "Home",
components: {},
};
</script>
<style lang="less" scoped></style>

16
qiezi_front/vue.config.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
devServer: {
proxy: {
"/qiezi/api": {
//这里最好有一个 /
target: "http://localhost:8088", // 服务器端接口地址
ws: true, //如果要代理 websockets配置这个参数
// 如果是https接口需要配置这个参数
changeOrigin: true, //是否跨域
pathRewrite: {
"^/qiezi/api": "/qiezi/api",
},
},
},
},
};

View File

View File

@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"noImplicitAny": false,
"module": "commonjs",
"sourceMap": true,
"outDir": "./dist",
"baseUrl":".",
"rootDir": "./",
"watch": false,
"strict": true,
"strictNullChecks": false,
"esModuleInterop": true
}
}

View File

@ -1,32 +0,0 @@
class ErrorHelper {
/**
*
* @param {String} message
* @param {Number} status
*/
static newError(message, status) {
return getError(message, status);
}
static Error403(message){
return getError(message,403);
}
static Error404(message){
return getError(message,404);
}
static Error406(message){
return getError(message,406);
}
static Error400(message){
return getError(message,400);
}
}
let getError = (message, status) => {
let error = new Error(message);
error['status'] = status;
return error;
}
export default ErrorHelper;

View File

@ -1,9 +0,0 @@
import { getLogger, configure } from "log4js";
configure({
appenders: { cheese: { type: "console" } },
categories: { default: { appenders: ["cheese"], level: "info" } }
});
const logger = getLogger();
logger.level = "debug";
export default logger;

View File

@ -1,108 +0,0 @@
import mysql from "mysql2/promise";
import config from '../config';
import * as fs from "fs-extra";
import * as path from 'path';
import log from '../util/LogUtil';
const HISTORY_NAME = "mysqlHistory.json";
interface Res {
rows: any;
fields: mysql.FieldPacket;
}
class MysqlUtil {
public static pool: mysql.Pool = null;
static async createPool() {
MysqlUtil.pool = await mysql.createPool(config.mysql.connection);
let basePath = path.join(config.rootPath, config.mysql.sqlFolder);
let hisPath = path.join(config.rootPath, HISTORY_NAME);
let history: Array<string>;
if (fs.existsSync(hisPath)) {
history = JSON.parse(await fs.readFile(hisPath, "utf-8"));
} else {
history = new Array();
}
//执行数据库
let files = (await fs.readdir(basePath)).sort((a, b) => a.localeCompare(b)).filter(item => !(item === HISTORY_NAME));
let error = null;
for (let i = 0; i < files.length; i++) {
if (history.indexOf(files[i]) > -1) {
log.info("sql无需重复执行:", files[i]);
continue;
}
let sqlLines = (await fs.readFile(path.join(basePath, files[i]), 'utf-8')).split(/[\r\n]/g);
try {
let sql = "";
for (let j = 0; j < sqlLines.length; j++) {
sql = sql + " " + sqlLines[j];
if (sqlLines[j].endsWith(";")) {
await MysqlUtil.pool.execute(sql);
sql = "";
}
}
log.info("sql执行成功:", files[i]);
history.push(files[i]);
} catch (err) {
error = err;
break;
}
}
await fs.writeFile(hisPath, JSON.stringify(history));
if (error != null) {
throw error;
}
}
static async getRows(sql: string, params: Array<any>, connection: mysql.PoolConnection = null): Promise<Array<any>> {
return (await MysqlUtil.execute(sql, params, connection)).rows;
}
static async getRow(sql: string, params: Array<any>, connection: mysql.PoolConnection = null): Promise<any> {
let rows = (await MysqlUtil.execute(sql, params, connection)).rows;
return rows.length > 0 ? rows[0] : null;
}
static async getSingle(sql: string, params: Array<any>, connection: mysql.PoolConnection = null): Promise<any> {
let rows = (await MysqlUtil.execute(sql, params, connection)).rows;
if (rows.length == 0) {
return null;
}
let row = rows[0];
return row[Object.keys(row)[0]];
}
static async execute(sql: string, params: Array<any>, connection: mysql.PoolConnection = null): Promise<Res> {
let res: any = {};
if (connection == null) {
let [rows, fields] = await MysqlUtil.pool.query(sql, params);
res['rows'] = fields === undefined ? null : rows;
res['fields'] = fields === undefined ? rows : fields;
} else {
let [rows, fields] = await connection.query(sql, params);
res['rows'] = rows;
res['fields'] = fields;
}
return res;
}
static async test() {
let connection = await MysqlUtil.pool.getConnection();
connection.beginTransaction();
connection.query(`insert into url value(6,"GET","asd","public")`);
connection.query(`insert into url value(7,"GET","asd","public")`);
await connection.commit();
connection.release();
}
}
export {
MysqlUtil,
Res,
mysql
}

View File

@ -1,8 +0,0 @@
class NumberUtil {
static getRandom(min: number, max: number): number {
return Math.floor((Math.random() * (max - min + 1) + min));
}
}
export default NumberUtil;

View File

@ -1,18 +0,0 @@
/*
node对象
*/
class ObjectOperation {
static combineObject(...objs) {
if (objs.length == 1 && objs[0] instanceof Array) {
objs = objs[0];
}
let sum = {};
let length = objs.length;
for (let i = 0; i < length; i++) {
sum = Object.assign(sum,objs[i]);
}
return sum;
}
}
export default ObjectOperation

View File

@ -1,24 +0,0 @@
import * as childPrecess from 'child_process';
class ProcessHelper {
static exec(cmd): Promise<string> {
return new Promise((resolve, reject) => {
childPrecess.exec(cmd, (error, stdout, stderr) => {
if (error) {
reject(error);
} if (stderr) {
reject(stderr);
} else {
resolve(stdout)
}
})
})
}
}
// (async()=>{
// let res= await ProcessHelper.exec('cd /d e://workspace&&dir');
// console.log(res);
// })()
export default ProcessHelper

View File

@ -1,14 +0,0 @@
import { createClient, RedisClientType } from "redis";
import config from "../config";
class RedisHelper {
public static client: RedisClientType<any>;
static async create() {
this.client = await createClient({ url: config.redis.url });
this.client.set("1","1")
}
}
export {
RedisHelper
}

View File

@ -1,64 +0,0 @@
import sqlite3 from 'sqlite3';
import { open, Database } from 'sqlite';
import config from '../config';
import * as fs from "fs-extra";
import * as path from 'path';
import log from './LogUtil';
const HISTORY_NAME = "sqliteHistory.json";
class SqliteHelper {
public static pool: Database = null;
static async createPool() {
let fullPath = path.join(config.rootPath, config.sqlite.filePath);
let dataFolder = path.dirname(fullPath);
if (!fs.existsSync(dataFolder)) {
fs.mkdir(dataFolder);
}
SqliteHelper.pool = await open({
filename: fullPath,
driver: sqlite3.Database
});
let sqlFolder = path.join(config.rootPath, config.sqlite.sqlFolder);
let hisPath = path.join(config.rootPath, HISTORY_NAME);
let history: Array<string>;
if (fs.existsSync(hisPath)) {
history = JSON.parse(await fs.readFile(hisPath, "utf-8"));
} else {
history = new Array();
}
//执行数据库
let files = (await fs.readdir(sqlFolder)).sort((a, b) => a.localeCompare(b)).filter(item => !(item === HISTORY_NAME));
let error = null;
for (let i = 0; i < files.length; i++) {
if (history.indexOf(files[i]) > -1) {
log.info("sql无需重复执行:", files[i]);
continue;
}
let sqlLines = (await fs.readFile(path.join(sqlFolder, files[i]), 'utf-8')).split(/[\r\n]/g).map(item => item.trim()).filter(item => !item.startsWith("--"));
try {
let sql = "";
for (let j = 0; j < sqlLines.length; j++) {
sql = sql + sqlLines[j];
if (sqlLines[j].endsWith(";")) {
await SqliteHelper.pool.run(sql);
sql = "";
}
}
log.info("sql执行成功:", files[i]);
history.push(files[i]);
} catch (err) {
error = err;
break;
}
}
await fs.writeFile(hisPath, JSON.stringify(history));
if (error != null) {
throw error;
}
}
}
export default SqliteHelper;

View File

@ -1,22 +0,0 @@
import moment from 'moment';
class TimeUtil {
/**
*
*/
static getZeroTime(): Date {
return moment()
.millisecond(0)
.second(0)
.minute(0)
.hour(0)
.toDate();
}
static async sleep(duration: number): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), duration);
});
}
}
export default TimeUtil;

View File

@ -1,14 +0,0 @@
import path, { dirname } from 'path'
class pathUtil {
static getPath(pathStr) {
return path.resolve(pathUtil.getRootPath(), pathStr);
}
static getRootPath() {
return path.resolve(__dirname, '..');
}
}
export default pathUtil