diff --git a/fullstack/.gitignore b/fullstack/.gitignore new file mode 100644 index 000000000..4443b89eb --- /dev/null +++ b/fullstack/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +tests +yarn.lock +dist +.env* \ No newline at end of file diff --git a/fullstack/README.md b/fullstack/README.md index f20fa11ec..0a4a0284e 100644 --- a/fullstack/README.md +++ b/fullstack/README.md @@ -1,3 +1,42 @@ +# 项目说明 + +- 本框架原本使用 node+typescript+express+mysql 搭建,修改后使用 redis 实现数据存储。 + 因此删掉了大多数不必要的文件,只保留了实现 api 的基础框架。 +- 此项目代码只考虑了长域名和短域名的转换,长域名唯一性校验和 mysql 持久化存储没有实现。 +- 短域名存储接口 url/save +- 短域名读取接口 url/get + +# 递交作业说明 + +1. 源代码 + 见代码。 +2. 单元测试代码以及单元测试覆盖率(覆盖率请勿提交整个目录,一张图片或一个 text table 即可) + ![nyc](./images/nyc.png) +3. API 集成测试案例以及测试结果 + ![api-save](./images/api-save.png) + ![api-get](./images/api-get.png) +4. 简单的框架设计图,以及所有做的假设 + ![简单架构图](./images/architecture.png) + 假设: + - 怎么保证所有的短 url 唯一? + 使用时间戳+10 位随机数生成一个数字,不能说完全唯一,但是重复率极小极小。 + - 怎么保证短 url 长度小于 8 位? + 时间戳长度为 13 位,经过 62 进制编码后长度绝对小于等于 8 位。 + - 怎么保证长域名存储的唯一性? + 本项目只考虑了长短域名的对应,长域名如果想做唯一性校验,可以在持久化到 mysql 数据库时做唯一索引,redis 这里只考虑短域名的唯一,且不考虑获取长域名对应的短域名情况 +5. 涉及的 SQL 或者 NoSQL 的 Schema,注意标注出 Primary key 和 Index 如果有。 + 本项目数据只涉及 redis 存储,schema 为 db0,key(短域名)- value(长域名) + +## 使用说明 + +安装: + +> yarn + +启动: + +> yarn start + # TypeScript Fullstack Engineer Assignment ### Typescript 实现短域名服务(细节可以百度/谷歌) diff --git a/fullstack/app.ts b/fullstack/app.ts new file mode 100644 index 000000000..889449cbc --- /dev/null +++ b/fullstack/app.ts @@ -0,0 +1,41 @@ +const express = require("express"); +const app = express(); +const createError = require("http-errors"); +const bodyParser = require("body-parser"); + +import { ErrorMessage } from "./common/exception/exception-const"; +import { logger } from "./common/logger"; +import * as UrlApi from "./router/system/url"; + +// 服务器设置 +app.use(bodyParser.json({ limit: "50mb" })); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); + +// node服务生命监测 +app.use("*/ping", (req: any, res: any) => { + res.send("pong"); +}); + +// 注册接口 +app.use("/url", UrlApi.router); + +// 404 +app.use(function (req: any, res: any, next: any) { + next(createError(404)); +}); + +// 错误处理 +app.use(function (err: any, req: any, res: any, next: any) { + if (err.name === "UnauthorizedError") { + res.send(ErrorMessage.AUTH_LOGIN); + } + res.locals.message = err.message; + res.locals.error = err; + res.status(err.status || 500); + res.send(err.message); +}); + +app.listen(8888, () => { + logger.info("The app is listening on port 8888!"); +}); diff --git a/fullstack/blogic/base-blogic.ts b/fullstack/blogic/base-blogic.ts new file mode 100644 index 000000000..eb9681759 --- /dev/null +++ b/fullstack/blogic/base-blogic.ts @@ -0,0 +1,13 @@ +import { BLogicContext } from "../common/base"; + +export abstract class BaseBLogic { + abstract doExecute(context: BLogicContext): any; + needAuth: boolean = false; + needLogger: boolean = false; + + public async execute(context: BLogicContext) { + return await this.doExecute(context); + }; +} + + diff --git a/fullstack/blogic/url/get.ts b/fullstack/blogic/url/get.ts new file mode 100644 index 000000000..910309f12 --- /dev/null +++ b/fullstack/blogic/url/get.ts @@ -0,0 +1,20 @@ +/** + * 短域名读取接口:接受短域名信息,返回长域名信息。 + */ + +import { BLogicContext } from "../../common/base"; +import { BaseModel } from "../../model/base-model"; +import { BaseBLogic } from "../base-blogic"; + +export class ShortUrlGetBLogic extends BaseBLogic { + public async doExecute(context: BLogicContext) { + const { url } = context.input; + const baseModel: BaseModel = new BaseModel(context.proxy); + const res: string = await baseModel.get(url); + if (res) { + return { + url: res, + }; + } + } +} diff --git a/fullstack/blogic/url/save.ts b/fullstack/blogic/url/save.ts new file mode 100644 index 000000000..5279ba430 --- /dev/null +++ b/fullstack/blogic/url/save.ts @@ -0,0 +1,22 @@ +/** + * 短域名存储接口:接受长域名信息,返回短域名信息 + */ + +import { BLogicContext } from "../../common/base"; +import { BaseBLogic } from "../base-blogic"; +import { BaseModel } from "../../model/base-model"; +import { getShortUrl } from "../../utils/common"; + +export class ShortUrlSaveBLogic extends BaseBLogic { + public async doExecute(context: BLogicContext) { + const { url } = context.input; + const baseModel: BaseModel = new BaseModel(context.proxy); + const key: string = getShortUrl(); + const res: string = await baseModel.set(key, url); + if (res && res == "OK") { + return { + url: key, + }; + } + } +} diff --git a/fullstack/common/base.ts b/fullstack/common/base.ts new file mode 100644 index 000000000..58203090c --- /dev/null +++ b/fullstack/common/base.ts @@ -0,0 +1,26 @@ +import { DBProxy } from './db-proxy'; + +// 请求 +export interface BLogicContext { + session: { + permission: string; + token: string; + host: string; + uid: string; + username: string; + appid: string; + }; + input: T; + params: any; + proxy: DBProxy; + originalUrl: string; // 请求路径 + reqNo: string; // 请求编号 +} + +// 返回 +export interface ResultData { + ok: boolean; + data: object; + err_code?: number; + err_msg?: string; +} diff --git a/fullstack/common/db-proxy.ts b/fullstack/common/db-proxy.ts new file mode 100644 index 000000000..9dd9f8233 --- /dev/null +++ b/fullstack/common/db-proxy.ts @@ -0,0 +1,54 @@ +import { logger } from "./logger"; + +const redis = require("redis"); + +export class DBProxy { + private clientNo: string; + private client: any; + constructor() { + this.clientNo = "DBProxy" + Math.ceil(Math.random() * 9999); + } + + public async connect() { + if (!this.client) { + this.client = redis.createClient({ + socket: { + host: "127.0.0.1", + port: 6379, + }, + }); + this.client.on("error", function (err: any) { + console.log("Error " + err); + }); + } + await this.client.connect(); + } + + public async disconnect() { + await this.client.disconnect(); + } + + public async set(key: string, value: string) { + logger.debug( + this.clientNo + `: dbproxy set: key=[${key}] value=[${value}]` + ); + try { + const res = await this.client.set(key, value); + return res; + } catch (error) { + logger.error(`DBProxy error: ${this.clientNo} - ${error}`); + return; + } + } + + public async get(key: string) { + logger.debug(this.clientNo + `: dbproxy get: key=[${key}]`); + try { + const res = await this.client.get(key); + return res; + } catch (error) { + logger.error(`DBProxy error: ${this.clientNo} - ${error}`); + return; + } + } +} diff --git a/fullstack/common/exception/exception-const.ts b/fullstack/common/exception/exception-const.ts new file mode 100644 index 000000000..172f9e372 --- /dev/null +++ b/fullstack/common/exception/exception-const.ts @@ -0,0 +1,41 @@ +export enum ErrorConst { + AUTH_LOGIN = 999, + SQL_ERROR = 1001, + External_API = 1002, + WX_AUTH_TOKEN = 4001, + WX_CREATE_MENU = 4002, +} + +export enum ErrorMessage { + AUTH_LOGIN = "登录验证失败", + SQL_ERROR = "数据库操作失败", +} + +/** + * 返回错误信息 + * @param errCode:number 错误码 + * @param msg:string[] 错误提示 + * @returns 字符串可能是内部错误,直接返回 + * @returns 数组或者不存在,用定义好的错误提示和msg拼接 + * @returns 其余情况(对象)序列化后返回 + */ +export function getErrorMessage(errCode: number, msg?: string[]) { + if (typeof msg === "string") { + return msg; + } else if (Array.isArray(msg) || !msg) { + let code = ErrorConst[errCode]; + let info: any = ErrorMessage[`${code as keyof typeof ErrorMessage}`]; + if (info && msg && msg.length == 1) { + info = `${msg[0]}${info}`; + } + if (info && msg && msg.length > 1) { + info = `${msg[0]}${info}${msg[1]}`; + } + if (!info && msg && msg.length) { + info = msg[0]; + } + return info; + } else { + return JSON.stringify(msg); + } +} diff --git a/fullstack/common/exception/permission-exception.ts b/fullstack/common/exception/permission-exception.ts new file mode 100644 index 000000000..0acf233b3 --- /dev/null +++ b/fullstack/common/exception/permission-exception.ts @@ -0,0 +1,16 @@ +import { getErrorMessage } from './exception-const'; + +export class PermissionException extends Error { + code: number; + msg: string; + err: Error | undefined; + constructor(errCode: number, errMsg?: string[], err?: Error) { + let msg: string = getErrorMessage(errCode, errMsg); + super(msg); + this.code = errCode; + this.msg = msg; + if (err) { + this.err = err; + } + } +} diff --git a/fullstack/common/exception/system-exception.ts b/fullstack/common/exception/system-exception.ts new file mode 100644 index 000000000..02bb45315 --- /dev/null +++ b/fullstack/common/exception/system-exception.ts @@ -0,0 +1,16 @@ +import { getErrorMessage } from "./exception-const"; + +export class SystemException extends Error { + code: number; + msg: string; + err: Error | undefined; + constructor(errCode: number, errMsg?: string[], err?: Error) { + let msg: string = getErrorMessage(errCode, errMsg); + super(msg); + this.code = errCode; + this.msg = msg; + if (err) { + this.err = err; + } + } +} diff --git a/fullstack/common/logger.ts b/fullstack/common/logger.ts new file mode 100644 index 000000000..b9ff4f6f0 --- /dev/null +++ b/fullstack/common/logger.ts @@ -0,0 +1,21 @@ +import * as log4js from 'log4js'; +export const logger = log4js.getLogger(); +export const reqLog = log4js.getLogger('reqLog'); +const level = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : 'debug'; +log4js.configure({ + appenders: { + // 控制台输入 + console: { type: 'console' }, + }, + categories: { + // 默认日志 + default: { + appenders: ['console'], + level: level, + }, + reqLog: { + appenders: ['console'], + level: level, + }, + }, +}); diff --git a/fullstack/images/api-get.png b/fullstack/images/api-get.png new file mode 100644 index 000000000..1865b93a1 Binary files /dev/null and b/fullstack/images/api-get.png differ diff --git a/fullstack/images/api-save.png b/fullstack/images/api-save.png new file mode 100644 index 000000000..447c57f53 Binary files /dev/null and b/fullstack/images/api-save.png differ diff --git a/fullstack/images/architecture.png b/fullstack/images/architecture.png new file mode 100644 index 000000000..f9d132573 Binary files /dev/null and b/fullstack/images/architecture.png differ diff --git a/fullstack/images/mocha.png b/fullstack/images/mocha.png new file mode 100644 index 000000000..0f9f6f278 Binary files /dev/null and b/fullstack/images/mocha.png differ diff --git a/fullstack/images/nyc.png b/fullstack/images/nyc.png new file mode 100644 index 000000000..07a19b45d Binary files /dev/null and b/fullstack/images/nyc.png differ diff --git a/fullstack/model/base-model.ts b/fullstack/model/base-model.ts new file mode 100644 index 000000000..af417d62c --- /dev/null +++ b/fullstack/model/base-model.ts @@ -0,0 +1,17 @@ +import { ErrorConst } from "../common/exception/exception-const"; +import { SystemException } from "../common/exception/system-exception"; +import { DBProxy } from "../common/db-proxy"; +import { logger } from "../common/logger"; + +export class BaseModel { + constructor(private dbproxy: DBProxy) {} + async set(key: string, value: string) { + let r: string = await this.dbproxy.set(key, value); + return r; + } + + async get(key: string) { + let r: string = await this.dbproxy.get(key); + return r; + } +} diff --git a/fullstack/package.json b/fullstack/package.json new file mode 100644 index 000000000..6f10b84d3 --- /dev/null +++ b/fullstack/package.json @@ -0,0 +1,89 @@ +{ + "name": "nodeserver", + "version": "1.0.0", + "description": "The node server for test", + "main": "app.js", + "scripts": { + "test": "cross-env TS_NODE_PROJECT='test/tsconfig.test.json' mocha './test/**/**.spec.ts'", + "test:cover": "nyc mocha './test/**/**.spec.ts'", + "tsc": "tsc --watch", + "start": "nodemon -e ts --exec ts-node app.ts", + "start:test": "set NODE_ENV=test&& ts-node app.ts", + "start:prod": "set NODE_ENV=prod&& ts-node app.ts" + }, + "mocha": { + "require": [ + "ts-node/register", + "tsconfig-paths/register" + ], + "ui": "bdd" + }, + "nyc": { + "include": [ + "blogic/url/*.ts", + "model/*.ts", + "utils/*.ts" + ], + "exclude": [ + "**/*.d.ts" + ], + "extension": [ + ".ts", + ".tsx" + ], + "require": [ + "ts-node/register" + ], + "reporter": [ + "text", + "html" + ], + "sourceMap": true, + "instrument": true, + "all": true + }, + "repository": { + "type": "git", + "url": "http://tianjige.com:7002/backend/nodeserver.git" + }, + "keywords": [ + "nodeserver" + ], + "author": "SinanLee", + "license": "ISC", + "email": "asdfwslxn@163.com", + "dependencies": { + "@msgpack/msgpack": "^2.7.2", + "cookie-parser": "^1.4.6", + "cross-env": "^7.0.3", + "crypto-js": "^4.1.1", + "dotenv": "^16.3.1", + "express": "^4.18.1", + "express-jwt": "^7.7.5", + "formidable": "^2.0.1", + "http-errors": "^2.0.0", + "https-proxy-agent": "^5.0.1", + "joi": "^17.6.0", + "jsonwebtoken": "^8.5.1", + "jszip": "^3.10.0", + "lodash": "^4.17.21", + "log4js": "^6.6.0", + "mysql": "^2.18.1", + "redis": "^4.6.8", + "request": "^2.88.2", + "socket.io": "^4.5.1", + "ts-node": "^10.8.2", + "tsconfig-paths": "^4.2.0" + }, + "devDependencies": { + "@types/chai": "^4.3.5", + "@types/mocha": "^10.0.1", + "@types/node": "^18.0.3", + "axios": "^1.5.0", + "chai": "^4.3.8", + "istanbul": "^0.4.5", + "nodemon": "^2.0.19", + "nyc": "^15.1.0", + "typescript": "^4.7.4" + } +} diff --git a/fullstack/router/base-router.ts b/fullstack/router/base-router.ts new file mode 100644 index 000000000..a66ff0eb8 --- /dev/null +++ b/fullstack/router/base-router.ts @@ -0,0 +1,47 @@ +import { BaseBLogic } from "../blogic/base-blogic"; +import { BLogicContext, ResultData } from "../common/base"; +import { DBProxy } from "../common/db-proxy"; +import { PermissionException } from "../common/exception/permission-exception"; +import { SystemException } from "../common/exception/system-exception"; +import { logger } from "../common/logger"; + +export class BaseRouter { + public static post(router: any, path: string, BLogicClass: any) { + logger.info("bind post router " + path); + router.post(path, async (req: any, res: any, next: any) => { + const reqNo: string = "Req" + Math.ceil(Math.random() * 9999); + const dbproxy: DBProxy = new DBProxy(); + dbproxy.connect(); + const blogic: BaseBLogic = new BLogicClass(); + const originalUrl: string = req.originalUrl; + let data: any = path.includes("upload") ? { req: req } : req.body; + + let session: any = { host: req.get("host") }; + let context: BLogicContext = { + session: session, + input: data, + params: req.params, + proxy: dbproxy, + originalUrl, + reqNo, + }; + let resData: ResultData = { ok: true, data: {} }; + try { + const r = await blogic.execute(context); + resData.data = r; + } catch (err) { + logger.error(JSON.stringify(err)); + resData.ok = false; + if (err instanceof SystemException) { + resData.err_code = err.code; + resData.err_msg = err.msg; + } else if (err instanceof PermissionException) { + resData.err_code = err.code; + resData.err_msg = err.msg; + } + } + res.send(resData); + dbproxy.disconnect(); + }); + } +} diff --git a/fullstack/router/system/url.ts b/fullstack/router/system/url.ts new file mode 100644 index 000000000..12c06809c --- /dev/null +++ b/fullstack/router/system/url.ts @@ -0,0 +1,9 @@ +const express = require("express"); +import { ShortUrlSaveBLogic } from "../../blogic/url/save"; +import { ShortUrlGetBLogic } from "../../blogic/url/get"; +import { BaseRouter } from "../base-router"; +export const router = express.Router(); + +// 短域名服务相关接口 +BaseRouter.post(router, "/save", ShortUrlSaveBLogic); +BaseRouter.post(router, "/get", ShortUrlGetBLogic); diff --git a/fullstack/test/tsconfig.test.json b/fullstack/test/tsconfig.test.json new file mode 100644 index 000000000..484405d0e --- /dev/null +++ b/fullstack/test/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "ts-node": { + "transpileOnly": true + } +} diff --git a/fullstack/test/unit/api/getShortUrl.spec.ts b/fullstack/test/unit/api/getShortUrl.spec.ts new file mode 100644 index 000000000..a26da48a5 --- /dev/null +++ b/fullstack/test/unit/api/getShortUrl.spec.ts @@ -0,0 +1,50 @@ +import { assert, expect } from "chai"; +const axios = require("axios"); + +describe("api:/url/get", () => { + let shortUrl: string = ""; + + before(() => { + // 先存一个长url + return axios + .post("http://localhost:8888/url/save", { + url: "/scdt-china/interview-assignments/blob/master/fullstack/README.md", + }) + .then( + ( + res: ApiResult = { status: 0, data: { ok: false, data: { url: "" } } } + ) => { + shortUrl = res.data.data.url; + } + ); + }); + + describe("/url/get", () => { + it("post request: /url/get", async () => { + const data = { + url: shortUrl, + }; + const res: ApiResult = await axios.post( + "http://localhost:8888/url/get", + data + ); + expect(res.status)?.to.equal(200); + expect(res.data.ok)?.to.equal(true); + assert.isString(res.data.data.url); + }); + }); +}); + +interface UrlData { + url: string; +} + +interface ApiResultData { + ok: boolean; + data: UrlData; +} + +interface ApiResult { + status: number; + data: ApiResultData; +} diff --git a/fullstack/test/unit/api/saveShortUrl.spec.ts b/fullstack/test/unit/api/saveShortUrl.spec.ts new file mode 100644 index 000000000..b640b5af5 --- /dev/null +++ b/fullstack/test/unit/api/saveShortUrl.spec.ts @@ -0,0 +1,16 @@ +import { assert, expect } from "chai"; +const axios = require("axios"); +// const api = require('../src/api'); // 引入要测试的API模块 + +describe("api:/url/save", () => { + it("post request: /url/save", async () => { + const data = { + url: "/scdt-china/interview-assignments/blob/master/fullstack/README.md", + }; + const res = await axios.post("http://localhost:8888/url/save", data); + expect(res.status).to.equal(200); + expect(res.data.ok).to.equal(true); + assert.isString(res.data.data.url); + assert.match(res.data.data.url, /^[0-9a-zA-Z]{1,8}$/); + }); +}); diff --git a/fullstack/test/unit/utils/url.spec.ts b/fullstack/test/unit/utils/url.spec.ts new file mode 100644 index 000000000..25b452146 --- /dev/null +++ b/fullstack/test/unit/utils/url.spec.ts @@ -0,0 +1,13 @@ +import { assert } from "chai"; +import { getShortUrl } from "../../../utils/common"; + +describe("util:getShortUrl", () => { + it("should return a string", () => { + const actualUrl = getShortUrl(); + assert.isString(actualUrl); + }); + it("should length < 8", () => { + const actualUrl = getShortUrl(); + assert.match(actualUrl, /^[0-9a-zA-Z]{1,8}$/); + }); +}); diff --git a/fullstack/tsconfig.json b/fullstack/tsconfig.json new file mode 100644 index 000000000..1a522019a --- /dev/null +++ b/fullstack/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* 访问 https://aka.ms/tsconfig.json 以阅读有关此文件的更多信息 */ + + /* 基本选项 */ + // "incremental": true, /* 启用增量编译 */ + "target": "ES2017" /* 指定 ECMAScript 目标版本:'ES3'、'ES5'(默认)、'ES2015'、'ES2016'、'ES2017'、'ES2018'、'ES2019'、'ES2020' 或 'ESNEXT'。 */, + "module": "commonjs" /* 指定模块代码生成:“none”、“commonjs”、“amd”、“system”、“umd”、“es2015”、“es2020”或“ESNext”。 */, + "lib": ["ES2018", "DOM"] /* 指定要包含在编译中的库文件。 */, + // "allowJs": true /* 允许编译 javascript 文件。 */, + // "checkJs": true /* 报告 .js 文件中的错误。 */, + // "jsx": "preserve" /* 指定 JSX 代码生成:'preserve'、'react-native' 或 'react'。 */, + // "declaration": true /* 生成相应的“.d.ts”文件。 */, + // "declarationMap": true /* 为每个对应的“.d.ts”文件生成一个源映射。 */, + "sourceMap": true /* 生成相应的“.map”文件。 */, + // "outFile": "./" /* 连接输出到单个文件。 */, + "outDir": "./dist" /* 将输出结构重定向到目录。 */, + // "rootDir": "./" /* 指定输入文件的根目录。用于通过 --outDir 控制输出目录结构。 */, + // "composite": true /* 启用项目编译 */, + // "tsBuildInfoFile": "./" /* 指定文件存放增量编译信息 */, + // "removeComments": true /* 不要向输出发出注释。 */, + // "noEmit": true /* 不发出输出。 */, + // "importHelpers": true /* 从 'tslib' 导入发射助手。 */, + // "downlevelIteration": true /* 以“ES5”或“ES3”为目标时,为“for-of”、展开和解构中的迭代提供全面支持。 */, + // "isolatedModules": true /* 将每个文件转换为一个单独的模块(类似于 'ts.transpileModule')。 */, + + /* 严格的类型检查选项 */ + "strict": true /* 启用所有严格的类型检查选项。 */, + "noImplicitAny": true /* 使用隐含的“任何”类型在表达式和声明上引发错误。 */, + // "strictNullChecks": true /* 启用严格的空检查。 */, + // "strictFunctionTypes": true /* 启用函数类型的严格检查。 */, + // "strictBindCallApply": true /* 在函数上启用严格的“绑定”、“调用”和“应用”方法。 */, + // "strictPropertyInitialization": true /* 启用对类中属性初始化的严格检查。 */, + // "noImplicitThis": true /* 使用隐含的“any”类型在“this”表达式上引发错误。 */, + // "alwaysStrict": true /* 以严格模式解析并为每个源文件发出“使用严格”。 */, + + /* 额外检查 */ + // "noUnusedLocals": true /* 报告未使用的本地人的错误。 */, + // "noUnusedParameters": true /* 报告未使用参数的错误。 */, + // "noImplicitReturns": true /* 不是函数中的所有代码路径都返回值时报告错误。 */, + // "noFallthroughCasesInSwitch": true /* 在 switch 语句中报告失败情况的错误。 */, + + /* 模块分辨率选项 */ + // "moduleResolution": "node" /* 指定模块解析策略:'node' (Node.js) 或 'classic' (TypeScript pre-1.6)。 */, + // "baseUrl": "./" /* 解析非绝对模块名称的基目录。 */, + // "paths": {} /* 一系列将导入重新映射到相对于“baseUrl”的查找位置的条目。 */, + // "rootDirs": [] /* 根文件夹列表,其组合内容代表运行时项目的结构。 */, + "typeRoots": ["node_modules/@types"] /* 包含类型定义的文件夹列表。 */, + // "types": [] /* 类型声明文件要包含在编译中。 */, + // "allowSyntheticDefaultImports": true /* 允许从没有默认导出的模块中默认导入。 这不会影响代码发出,只是类型检查。 */, + "esModuleInterop": true /* 通过为所有导入创建命名空间对象,在 CommonJS 和 ES 模块之间启用发射互操作性。 暗示“allowSyntheticDefaultImports”。 */, + // "preserveSymlinks": true /* 不解析符号链接的真实路径。 */, + // "allowUmdGlobalAccess": true /* 允许从模块访问 UMD 全局变量。 */, + + /* 源映射选项 */ + // "sourceRoot": "" /* 指定调试器应该定位 TypeScript 文件而不是源位置的位置。 */, + // "mapRoot": "" /* 指定调试器应该定位映射文件而不是生成位置的位置。 */, + // "inlineSourceMap": true /* 发出带有源映射的单个文件而不是单独的文件。 */, + // "inlineSources": true /* 在单个文件中与源映射一起发出源; 需要设置“--inlineSourceMap”或“--sourceMap”。 */, + + /* 实验选项 */ + "experimentalDecorators": true /* 启用对 ES7 装饰器的实验性支持。 */, + "emitDecoratorMetadata": true /* 为装饰器的发射类型元数据启用实验性支持。 */, + + /* 高级选项 */ + "skipLibCheck": true /* 跳过声明文件的类型检查。 */, + "forceConsistentCasingInFileNames": true /* 禁止对同一文件的大小写不一致的引用。 */ + } +} diff --git a/fullstack/utils/common.ts b/fullstack/utils/common.ts new file mode 100644 index 000000000..43422e7de --- /dev/null +++ b/fullstack/utils/common.ts @@ -0,0 +1,214 @@ +/** + * 返回字符串 为n个char构成 + * @param char 重复的字符 + * @param count 次数 + * @return String + * @author adswads@gmail.com + */ +export const charString = (char: string, count: number): string => { + var str: string = ""; + while (count--) { + str += char; + } + return str; +}; + +/** + * 对日期进行格式化, 和C#大致一致 默认yyyy-MM-dd HH:mm:ss + * 可不带参数 一个日期参数 或一个格式化参数 + * @param date 要格式化的日期 + * @param format 进行格式化的模式字符串 + * 支持的模式字母有: + * y:年, + * M:年中的月份(1-12), + * d:月份中的天(1-31), + * H:小时(0-23), + * h:小时(0-11), + * m:分(0-59), + * s:秒(0-59), + * f:毫秒(0-999), + * q:季度(1-4) + * @return String + * @author adswads@gmail.com + */ +export const dateFormat = (date?: any, format?: string): string => { + //无参数 + if (date == undefined && format == undefined) { + date = new Date(); + format = "yyyy-MM-dd HH:mm:ss"; + } + //无日期 + else if (typeof date == "string") { + format = date; + date = new Date(); + } + //无格式化参数 + else if (format === undefined) { + format = "yyyy-MM-dd HH:mm:ss"; + } else { + } + //没有分隔符的特殊处理 + + let map: any = { + y: date.getFullYear() + "", //年份 + M: date.getMonth() + 1 + "", //月份 + d: date.getDate() + "", //日 + h: date.getHours(), //小时 12 + H: date.getHours(), //小时 24 + m: date.getMinutes() + "", //分 + s: date.getSeconds() + "", //秒 + q: Math.floor((date.getMonth() + 3) / 3) + "", //季度 + f: date.getMilliseconds() + "", //毫秒 + }; + //小时 12 + if (map["H"] > 12) { + map["h"] = map["H"] - 12 + ""; + } else { + map["h"] = map["H"] + ""; + } + map["H"] += ""; + + let reg = "yMdHhmsqf"; + let all = "", + str = ""; + for (let i = 0, n = 0; i < reg.length; i++) { + n = format.indexOf(reg[i]); + if (n < 0) { + continue; + } + all = ""; + for (; n < format.length; n++) { + if (format[n] != reg[i]) { + break; + } + all += reg[i]; + } + if (all.length > 0) { + if (all.length == map[reg[i]].length) { + str = map[reg[i]]; + } else if (all.length > map[reg[i]].length) { + if (reg[i] == "f") { + str = map[reg[i]] + charString("0", all.length - map[reg[i]].length); + } else { + str = charString("0", all.length - map[reg[i]].length) + map[reg[i]]; + } + } else { + switch (reg[i]) { + case "y": + str = map[reg[i]].substr(map[reg[i]].length - all.length); + break; + case "f": + str = map[reg[i]].substr(0, all.length); + break; + default: + str = map[reg[i]]; + break; + } + } + format = format.replace(all, str); + } + } + return format; +}; + +// 带_字符串转驼峰 +export function ToHump(name: string): string { + if (name.includes("_")) { + let oarr = name.split("_"); + for (let i = 1; i < oarr.length; i++) { + oarr[i] = oarr[i].charAt(0).toUpperCase() + oarr[i].slice(1); + } + return oarr.join(""); + } + return name; +} + +// 对象属性带_转驼峰 +export function HumpObj(obj: any) { + if (!obj) return obj; + + let humpObj: any = {}; + for (let key in obj) { + humpObj[ToHump(key)] = obj[key]; + + if (obj[key] && obj[key] instanceof Object) { + humpObj[ToHump(key)] = HumpObj(obj[key]); + } + } + + return humpObj; +} + +// 数组对象转驼峰 +export function HumpArray(arr: any) { + if (arr instanceof Array) { + arr.forEach((item, i) => { + arr[i] = HumpObj(item); + }); + } else { + arr = HumpObj(arr); + } + return arr; +} + +// 渲染二级菜单 +export function renderMenu(arr: any, topValue: any = 0) { + let newArray: any[] = []; + // 添加一级菜单 + for (let i = 0; i < arr.length; i++) { + if (arr[i]["parentId"] == topValue) { + arr[i].list = []; + newArray.push(arr[i]); + } + } + for (let i = 0; i < arr.length; i++) { + for (let j = 0; j < newArray.length; j++) { + if (arr[i]["parentId"] == newArray[j]["menuId"]) { + newArray[j].list.push(arr[i]); + } + } + } + return newArray; +} + +// 获得当前时间 +export function getCurrentTime(): string { + let ctime = dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss"); + return ctime; +} + +// 字符串转16进制 +export function toHex(str: string): string { + let val = ""; + for (let i = 0; i < str.length; i++) { + if (val == "") val = str.charCodeAt(i).toString(16); + else val += str.charCodeAt(i).toString(16); + } + val += "0a"; + return val; +} + +// 数字转62进制 +export function decimalTo62(num: number): string { + const base62 = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let res = ""; + while (num > 0) { + const remainder = num % 62; + res = base62.charAt(remainder) + res; + num = Math.floor(num / 62); + } + return res; +} + +/** + * 生成一个唯一的字符串作为短域名 + * @returns string 短域名 + */ +export function getShortUrl(): string { + // 使用当前时间戳+随机数作为唯一id,随机数取10位,避免id重复 + const timestamp = new Date().getTime(); + const rand = Math.floor(Math.random() * Math.pow(10, 10)); + // 转62进制返回字符串 + return decimalTo62(timestamp + rand); +} diff --git a/fullstack/utils/random.ts b/fullstack/utils/random.ts new file mode 100644 index 000000000..e5683d57a --- /dev/null +++ b/fullstack/utils/random.ts @@ -0,0 +1,11 @@ +// 生成特定长度的随机字符串 +export function randomString(len: number): string { + let str = 'abcdefg012ABCDEFGhijklmn345HIJKLMNopqrstOPQRST678uvwxyz0UVWXYZ'; + let res = ''; + let strLen = str.length - 1; + for (let i = 0; i < len; i++) { + let n = Math.round(Math.random() * strLen); + res += str.slice(n, n + 1); + } + return res; +}