Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript Fullstack Engineer Assignment by Sinan Lee #1970

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions fullstack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
package-lock.json
tests
yarn.lock
dist
.env*
39 changes: 39 additions & 0 deletions fullstack/README.md
Original file line number Diff line number Diff line change
@@ -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 实现短域名服务(细节可以百度/谷歌)
Expand Down
41 changes: 41 additions & 0 deletions fullstack/app.ts
Original file line number Diff line number Diff line change
@@ -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!");
});
13 changes: 13 additions & 0 deletions fullstack/blogic/base-blogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BLogicContext } from "../common/base";

export abstract class BaseBLogic {
abstract doExecute(context: BLogicContext<any>): any;
needAuth: boolean = false;
needLogger: boolean = false;

public async execute(context: BLogicContext<any>) {
return await this.doExecute(context);
};
}


20 changes: 20 additions & 0 deletions fullstack/blogic/url/get.ts
Original file line number Diff line number Diff line change
@@ -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<any>) {
const { url } = context.input;
const baseModel: BaseModel = new BaseModel(context.proxy);
const res: string = await baseModel.get(url);
if (res) {
return {
url: res,
};
}
}
}
22 changes: 22 additions & 0 deletions fullstack/blogic/url/save.ts
Original file line number Diff line number Diff line change
@@ -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<any>) {
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,
};
}
}
}
26 changes: 26 additions & 0 deletions fullstack/common/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { DBProxy } from './db-proxy';

// 请求
export interface BLogicContext<T> {
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;
}
54 changes: 54 additions & 0 deletions fullstack/common/db-proxy.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
41 changes: 41 additions & 0 deletions fullstack/common/exception/exception-const.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
16 changes: 16 additions & 0 deletions fullstack/common/exception/permission-exception.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
16 changes: 16 additions & 0 deletions fullstack/common/exception/system-exception.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
21 changes: 21 additions & 0 deletions fullstack/common/logger.ts
Original file line number Diff line number Diff line change
@@ -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,
},
},
});
Binary file added fullstack/images/api-get.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added fullstack/images/api-save.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added fullstack/images/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added fullstack/images/mocha.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added fullstack/images/nyc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions fullstack/model/base-model.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading