短网址生成-nodejs实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhoujiaping123/article/details/81365343

该项目使用mysql+nodejs实现短网址服务;
支持自定义短网址;
完美解决了自定义短网址与自增id生产网址冲突问题。

以下所有文件都放在项目根目录下
init.txt

安装nodejs之后
在项目的根目录下执行下列命令
npm install mysql
npm install winston
npm install uuid
mysql模块,用于连接mysql数据库,执行curd操作。
winston模块,用于代替console.log打印日志。
uuid模块,用于支持配置winston模块的日志id。

db.js
封装了对数据库的curd操作,支持连接池,支持事务,解决了批量插入并且处理了占位符(?)的可读性问题。
封装的另一个目的,是将回调风格改为promise风格,调用者可以使用async-await将异步代码同步化。

const mysql = require('mysql');
const log = require('./log');
const logger = log.defaultLogger;//使用默认日志记录器
const db = {};
var pool  = mysql.createPool({  //创建连接池
  connectionLimit : 10,  
  host            : 'localhost',  
  user            : 'root',  
  password        : '',  
  database        : 'test',
  charset         : 'utf8'
});
/*  
const conn = mysql.createConnection({
    host:'localhost',
    user:'root',
    password:'',
    database:'jyd'
});
conn.connect();
*/


//获取连接  
db.conn = async function(){
    return new Promise((resolve,reject)=>{
        pool.getConnection(function(err, conn) {  
            if (err) {  
                reject(err);  
            }else{
                resolve(conn);
            }            
        });  
    });
};
//在事务中执行
db.doInTx = function(cb){
    return new Promise((resolve,reject)=>{
        db.conn().then(conn=>{
            conn.beginTransaction(async(err)=>{
                if(err){
                    conn.release();
                    await cb(err,null);
                    return;
                }
                try{
                    const res = await cb(null,conn);
                    conn.commit((err)=>{
                        logger.info('commit');
                        conn.release(); 
                        if(err){
                            logger.error(err);
                            reject(err);
                            return;
                        }
                        resolve(res);
                    });
                }catch(e){
                    conn.rollback(err=>{
                        logger.info('rollback');
                        conn.release();
                        if(err){
                            logger.error(err);
                            reject(err);
                            return;
                        }
                        reject(e);
                    });
                }
            });
        },async err=>{
            await cb(err,null);
        });
    });

};
//批量插入。这里其实还可以优化一下,可以支持设置批量数,数据太多分多次批量操作插入。
db.insert = async function({conn,tablename,columns,values}){
    let {sql,params} = genInsertSql({tablename,columns,values});
    //logger.info(sql);
    //logger.info(JSON.stringify(params,null,2));
    try{
        let rows = await query({conn,sql,params});
        return rows;
    }catch(e){
        if(e.code == 'PARSER_JS_PRECISION_RANGE_EXCEEDED'){
            logger.error(JSON.stringify(e));
        }else{
            throw e;
        }
    }
    return;
};
//生成批量插入的sql  
function genInsertSql({tablename,columns,values}){
    const columnnames = columns.join(',');
    const sql = `insert into ${tablename}(${columnnames})values `;
    const params = [];
    const placeholders = [];
    values.forEach((item,index)=>{
        let holder = [];
        for(let i=0;i<columns.length;i++){
            holder.push('?');
            params.push(item[columns[i]]);
        }
        placeholders.push('('+holder.join(',')+')');
    });
    const ret = {
        sql:sql+placeholders.join(',')+';',
        params:params
    };
    //logger.info(ret);
    return ret;
}
//查询操作
async function query({conn,sql,params}){
    return new Promise((resolve,reject)=>{
        conn.query(sql,params,(err,res)=>{
            if(err){
                reject(err);
                return;
            }
            //logger.info(res);
            //logger.info(JSON.stringify(res,null,2));
            resolve(res);
        });
    });
}
db.query = query;
db.pool = pool;
module.exports = db;
//query方法和insert方法,其实还可以优化。可以去掉conn参数,每次获取连接的时候,生成一个标识,将连接保存到一个对象,然后查询和插入的时候,根据标识从中获取它,释放连接的时候,根据标识将其从对象中删除。

log.js

//npm install winston
//npm install uuid
const uuid = require('uuid');
const winston = require('winston');

const { createLogger, format, transports } = require('winston');
const { combine, timestamp, label, printf } = format;

const myFormat = printf(info => {
  return `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`;
});

function newLogger(loglabel = uuid.v4()){
    const logger = createLogger({
      format: combine(
        label({ label: loglabel}),
        timestamp(),
        myFormat
      ),
      transports: [new transports.Console()]
    });
    return logger;
}
const defaultLogger = newLogger();//提供一个默认的日志。该日志是全局唯一的,任何地方使用它,都是相同的日志id。
const log = {
    newLogger,
    defaultLogger
};
/*
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    //
    // - Write to all logs with level `info` and below to `combined.log` 
    // - Write all logs error (and below) to `error.log`.
    //
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});
*/
//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
// 
/*
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}*/

module.exports = log;

tiny.js
短网址模块

const db = require('./db');
const log = require('./log');
const logger = log.defaultLogger;
//作为短网址的字符(一共62个,我们的短网址最多6位,所以可以生成626次方个短网址)
const chars = `0123456789abcdegfhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`.split(new RegExp('','igm'));

//模拟业务
//给定长网址,生成短网址。这里还有工作没完成,只是生成了短网址的一部分,没有处理网址协议部分
async function generateTinyUrl({longUrl}){
    if(!longUrl){
        throw new Error(`invalid longUrl => ${longUrl}`);
    }
    return await db.doInTx(async (err,conn)=>{
        if(err){
            logger.error(err);
            return;
        }
        //如果长网址已经存在,则直接返回
        let req = {
            conn,
            sql:'select * from t_tiny_url where long_url = ?',
            params:[longUrl]
        };
        let rows = await db.query(req);
        if(rows.length > 0){
            logger.info(`this longUrl is exsits => ` + JSON.stringify(rows));
            return rows;
        }
        //获取下一个自增id
        let tablename = 't_tiny_url';
        let columns = ['long_url'];
        let values = [{
            long_url:longUrl
        }];
        rows = await db.insert({conn,tablename,columns,values});
        //logger.info(rows);
        let id = rows.insertId;
        //计算短网址
        //logger.info(id);
        let tinyUrl = toTinyUrl(id);
        //判断短网址是否存在
        req.sql = 'select * from t_tiny_url where tiny_url = ?';
        req.params = [tinyUrl];
        rows = await db.query(req);
        //如果短网址已经存在,就使用占用它id的 短网址对于的id 作为生成此短网址的数值。意思是谁占了我的坑,我就要用它的坑。如果它的坑被别人占了,我就把它们作为一个整体,当成它。这样只要坑被占了,就一定能找到一个未被占用的坑。
        if(rows.length > 0){
            //寻找占用了我的坑的家伙
            req.sql = 'select * from t_tiny_id_provide where tiny_url = ?';
            req.params = [tinyUrl];
            rows = await db.query(req);
            const _tinyUrl = tinyUrl;
            const _id = rows[0].provide_id;
            tinyUrl = toTinyUrl(_id);
            logger.info(`id=>${id},tinyUrl=>${tinyUrl}`);
            //req.sql = 'update t_tiny_id_provide set provide_id = ? where tiny_url = ?';
            //req.params = [id,_tinyUrl];
            //别人占了我的坑,我用了它(它们)的坑,扯平了,这些信息不需要了。
            req.sql = 'delete from t_tiny_id_provide where provide_id = ?';
            req.params = [_id];
            rows = await db.query(req);
        }
        //logger.info(tinyUrl);
        //logger.info(db);
        //保存短网址
        req.sql = 'update t_tiny_url set tiny_url = ? where id = ?';
        req.params = [tinyUrl,id];
        rows = await db.query(req);
        return rows;
    });
}
//自定义短码
async function customTinyUrl({tinyUrl,longUrl}){
    if(!longUrl){
        throw new Error(`invalid longUrl => ${longUrl}`);
    }
    if(!tinyUrl || !/[0-9a-zA-Z]{1,6}/.test(tinyUrl)){
        throw new Error(`invalid tinyUrl => ${tinyUrl}`);
    }
    return await db.doInTx(async (err,conn)=>{
        if(err){
            logger.error(err);
            return;
        }
        //如果短网址或者长网址已经存在,就直接返回
        let req = {
            conn,
            sql:'select * from t_tiny_url where tiny_url = ? or long_url = ?',
            params:[tinyUrl,longUrl]
        };
        let rows = await db.query(req);
        if(rows.length > 0){
            logger.info(`this tinyUrl/longUrl is exsits => ` + JSON.stringify(rows));
            return rows;
        }
        let tablename = 't_tiny_url';
        let columns = ['tiny_url','long_url'];
        let values = [{
            long_url:longUrl,tiny_url:tinyUrl
        }];
        //获取下一个自增id
        rows = await db.insert({conn,tablename,columns,values});
        //logger.info(rows);
        let id = rows.insertId;
        //logger.info(id);
        //计算短网址对应的数值
        let provideId = fromTinyUrl(tinyUrl);
        //如果我的坑被别人占了,那就是声明别人占了你的坑。我不欠谁的坑,谁也不欠我的坑。
        req.sql = 'update t_tiny_id_provide set provide_id = ? where provide_id = ?';
        req.params = [id,provideId];
        logger.info(req.params);
        rows = await db.query(req);
        logger.info('=>'+JSON.stringify(rows));
        //如果我的坑没被别人占,那就声明一下,我占了某人的坑。
        if(rows.affectedRows == 0){
            tablename = 't_tiny_id_provide';
            columns = ['tiny_url','provide_id'];
            values = [{
                tiny_url:tinyUrl,
                provide_id:id
            }];
            //
            rows = await db.insert({conn,tablename,columns,values});
        }
        return rows;
    });
}
//根据数值计算短网址,当然这里并没有处理协议部分
function toTinyUrl(id){
    let n = id;
    let code = '';
    let size = chars.length;
    while(n > 0){
        code += chars[n%size];
        n = 0|(n/size);
    }
    return [...code].reverse().join('');
}
//根据短网址,计算数值
function fromTinyUrl(tinyUrl){
    const chs = [...tinyUrl];
    //logger.info(`chs=>${chs}`);
    //logger.info(Array.isArray(chs));
    let res = chs.map(ch=>chars.indexOf(ch));
    logger.info(`res=>${res}`);
    res = res.reduce((prev,curr)=>{
        return prev*chars.length+curr;
    },0);
    logger.info(`res=>${res}`);
    return res;
}
//测试
let test = async function(){
    //await generateTinyUrl('http://www.test.com/4');
    //await generateTinyUrl('http://www.test.com/1');
    //await generateTinyUrl('http://www.test.com/2');
    //await generateTinyUrl('http://www.test.com/x');

    await customTinyUrl('4','http://www.test.com/1');
    await customTinyUrl('1','http://www.test.com/2');
    await customTinyUrl('2','http://www.test.com/3');
    //await customTinyUrl('1','http://www.test.com/4');
    await generateTinyUrl('http://www.test.com/4');

}
test = async function(){
    await customTinyUrl('5','http://www.test.com/1');
    await customTinyUrl('1','http://www.test.com/2');
    await customTinyUrl('2','http://www.test.com/3');
    //await customTinyUrl('1','http://www.test.com/4');
    await generateTinyUrl('http://www.test.com/4');
    await generateTinyUrl('http://www.test.com/5');

}
test = function(){
    const id = fromTinyUrl('abc');
    logger.info(id);
}
//test();
/*
test().then(data=>{
    logger.info(data);
},err=>{
    logger.error(err);
});*/
module.exports = {
    generateTinyUrl,customTinyUrl
};

init.js
初始化模块
用于创建表

const db = require('./db');
const log = require('./log');
const logger = log.defaultLogger;

async function creatTables(err,conn){
    if(err){
        logger.error(err);
        return;
    }
    //清除数据
    let sqls = [
        'drop table if exists t_tiny_url;',
        'drop table if exists t_tiny_id_provide;',

        `create table if not exists t_tiny_url(
            id bigint primary key auto_increment,
            tiny_url varchar(32) unique,
            long_url varchar(767) unique,
            key t_tiny_url_idx_tiny_url(tiny_url),
            key t_tiny_url_idx_long_url(long_url)
        );`,
        `create table if not exists t_tiny_id_provide(
            tiny_url varchar(32) unique,
            provide_id bigint unique,
            key t_tiny_id_provide_idx_tiny_url(tiny_url),
            key t_tiny_id_provide_idx_provide_id(provide_id)
        );`
    ];
    //logger.info(sqls);
    let params = [];
    let defers = [];
    sqls.forEach(sql=>{
        defers.push(db.query({conn,sql,params}));
    });
    await Promise.all(defers);
}
let test = async function(){
    await db.doInTx(creatTables);
}
test().then(data=>{
    logger.info(data);
    db.pool.end();
},err=>{
    logger.error(err);
    db.pool.end();
});

server.js
http服务器模块

const http = require('http');
const url = require('url');
const querystring = require('querystring');

const tiny = require('./tiny');
// 创建一个 HTTP 服务器
const server = http.createServer( async(req, res) => {
    try{
        const reqUrl = url.parse(req.url);
        //这里没有用到框架,因为我们的需求非常简单
        const handler = {
            '/gen-tiny-url':'generateTinyUrl',
            '/cust-tiny-url':'customTinyUrl'
        };
        const pathname = reqUrl.pathname;
        //TODO 添加一个处理,访问短网址的时候,重定向到长网址
        //TODO 添加一个处理,查询短网址对应的长网址
        //TODO 添加一个处理,查询长网址对应的短网址,如果不存在,返回空
        if(!handler[pathname]){
            res.writeHead(404, { 'Content-Type': 'text/plain' });
            res.end('404');
            return;
        };
        const arg = querystring.parse(reqUrl.query);
        console.info(arg);
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        const rows = await tiny[handler[pathname]](arg);
        //console.info(rows);
        res.write(JSON.stringify(rows));
        res.end();
    }catch(e){
        console.error(e);
        res.writeHead(500, { 'Content-Type': 'text/plain' });
        res.end('500');
    }

});
server.on('clientError', (err, socket) => {
    socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
// 服务器正在运行
server.listen(1337);

package.json

{
  "name": "tinyurl",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "mysql": "^2.16.0",
    "uuid": "^3.3.2",
    "winston": "^3.0.0"
  }
}

猜你喜欢

转载自blog.csdn.net/zhoujiaping123/article/details/81365343