该项目使用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
短网址模块
一般短网址实现有两个思路

  • tinyUrl=md5(longUrl).substr(0,16)
    这种方法,需要解决tinyUrl重复的问题。
  • 将自增id转为62进制对应的字符串。tinyUrl=genTinyUrl(id)
    这种方法,自定义tinyUrl会使自增id突然跳到一个比较大的值,中间的id被浪费,而且不可预估浪费多少。

我们的实现结合上面的两个方案,通过记录自定义tinyUrl对自增id的占用关系,解决了tinyUrl重复的问题和id浪费的问题。
保存自定义tinyUrl和longUrl的关联的时候,我们也使用自增id,而不是设置id为parseId(tinyUrl)。

提供两个接口

  • longUrl关联到自动生成的tinyUrl
    如果longUrl已关联过tinyUrl,则直接返回。
    否则获取下一个自增id,转成tinyUrl。
    如果tinyUrl已被id’占用,假设tinyUrl’=genTinyUrl(id’),则将longUrl关联到tinyUrl’。
    否则关联longUrl到tinyUrl。
  • longUrl关联到自定义的tinyUrl
    如果tinyUrl已关联到一个longUrl’,并且longUrl’!=longUrl则提示用户该tinyUrl已被使用。
    如果tinyUrl已关联到一个longUrl’,并且longUrl’==longUrl则返回关联成功。
    否则,获取下一个自增id,假设为id’,保存tinyUrl和longUrl的关联。并将tinyUrl对id’的占用,记录到另一张表。

假设
tinyUrl1=genTinyUrl(id1)
tinyUrl2=genTinyUrl(id2)
tinyUrl3=genTinyUrl(id3)

如果出现tinyUrl1占用id2,tinyUrl2占用id3的情况,则将tinyUrl1占用的id更新为id3,即现在tinyUrl1占用了id3,tinyUrl2一借一贷,逻辑上已经不占用别人的id,它对应的id也不被其他tinyUrl占用。

const db = require('./db');
const log = require('./log');
const logger = log.defaultLogger;
//作为短网址的字符(一共62个,我们的短网址最多6位,所以可以生成62的6次方个短网址)
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;}//获取下一个自增idlet 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}];//获取下一个自增idrows = 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"}
}

短网址生成-nodejs实现相关推荐

  1. 新浪短网址生成java_如何生成t.cn的短链接?新浪短网址怎么生成的?

    t.cn短链接.新浪短网址是什么? 短网址顾名思义就是一个很短的链接和网址,常用于将一个长连接缩短成一个短链接,方便利于推广.  t.cn短网址,可能很多朋友都已经不再陌生,特别是在微博.微信.朋友圈 ...

  2. 搏天短网址生成网站源码v3.1

    搏天短网址生成网站源码v3.1 上传服务器安装即可直接上传到空间后直接访问即可根据提示安装运行环境PHP5.4-7.0都可以-- 直接上传到空间后直接访问即可根据提示安装 运行环境PHP5.4-7.0 ...

  3. 短网址生成+域名检测+短网址还原等四合一前端源码

    介绍: 短网址生成+域名检测+短网址还原等四合一前端源码没有功能哦~!有会后端技术的可以拿去开发用!页面还是挺漂亮的,里面还有很多子页面都非常的好看! 网盘下载地址: http://kekewangL ...

  4. 防红直连php,【源码资源】20新PHP网址缩短防封防红短网址生成系统

    [源码资源]20新PHP网址缩短防封防红短网址生成系统 源码描述: 注册用户可以绑定自己域名,用来做防封.支持直连.跳转.框架.密码访问等.不用购买大量域名来 做防封.支持自定义广告. 可以设置用户等 ...

  5. wsdl接口调用请求消息xml_短网址生成 API 接口调用请求

    短网址生成 API 接口在网上已经很多且大都封装成了 API 供别人调用.支持前台跨域请求,以GET/POST方式提交即可.短网址生成 API 接口可以将长网址缩短成短网址,支持百度.新浪.suoim ...

  6. [转载] URL短网址生成算法原理

    参考链接: URL 短地址Shorteners及其Python中的API 2 短网址(Short URL),是在形式上比较短的网址,通过映射关系跳转到原有的长网址. 本文转自米扑博客:URL短网址生成 ...

  7. 最新短网址生成api接口(t.cn、url.cn短链接生成)

    简要说明 短网址生成api接口有很多种,不同的接口生成的短网址格式也不同,比如常见的t.cn.url.cn.w.url.cn等格式.总而言之短链接接口就是用来将一个冗长的链接缩短成15个字符以内的短链 ...

  8. 短网址系统php源码,Tanking个人短网址生成PHP源码

    源码介绍 在我们的项目当中,如果需要更好传播我们的活动链接,但是链接太长1来是不美观,2来是太过于"笨重",例如拼多多,他们的推广链接都是有短链接的,还有新浪微博.但是,这些始终都 ...

  9. 分享百度短网址生成工具和接口 mr.baidu.com/xxxxx

    由于过长的链接网址缺乏友好,导致用户误认为带病毒的网站,所以网址过长的时候可以通过本软件将过长的网站转换为短网址,使连接更友好,并且避免由于过度使用自己网站的域名而被屏蔽. 生成的最终效果是:http ...

  10. url短网址 java_url.cn短网址生成api接口(腾讯短链接url生成)

    分享几个最新的url.cn短网址生成api接口,快速生成url.cn超短链接,接口都可以正常调用,觉得不错可以收藏一下. 请求地址: 使用说明: 将api接口地址中"http://www.b ...

最新文章

  1. Linux C SQLite3 编程
  2. 2015-10-11 Sunday 晴 ARM学习
  3. [云炬创业学笔记]第一章创业是什么测试2
  4. formate JAVA_JAVA String.format 方法使用
  5. mkl_def.dll文件加载失败
  6. 欧式距离、曼哈顿距离、余弦相似度(python代码)
  7. 1004 字符三角形
  8. 【转载】 MySQL数据库“十宗罪”(十大经典错误案例)
  9. matlab画图实例_自定义函数
  10. s l m 尺码排序 php,尺码中LS是什么码?比M码大吗?还是说比S码还小?
  11. 《人月神话》(The Mythical Man-Month)看清问题的本质:如果我们想解决问题,就必须试图先去理解它...
  12. Chrome断点调试
  13. Python安装包时遇到There was a problem confirming the ssl certificate…的解决办法
  14. Android开发中的一些问题
  15. Node.js基础(二)-- 模块化、npm与包
  16. 花样解锁方式:后置、屏下和侧面指纹,你觉得那种最好用
  17. BDC的执行模式与更新模式
  18. 提前面试|浙江大学2023年公共管理硕士(MPA)“提前面试”通知
  19. element ui el-table 无数据时显示默认空图片(el-empty)
  20. CSS、CSS3选择器

热门文章

  1. 单独开一贴个人认为是编译器缺陷希望有人提交给微软
  2. 我心目中的编程高手(不得不转)
  3. 西瓜书之误差逆传播公式推导、源码解读及各种易混淆概念
  4. python爬取企业电话_Python爬取天眼查企业数据
  5. 企业内IT部/信息部发展阶段和趋势(第一阶段)
  6. 关系网络lbs的应用_基于智能移动端的LBS+地图应用可以 说是LBS营销的核心模式,也是LBS营销的基础...
  7. JSAPI 高德地图应用--关键字搜索、POI搜索定位,获取经纬度
  8. (华师2021年秋季课程作业以及答案3)论述东西方文化差异对建筑风格的影响。
  9. 18讲项目实战签证详细页
  10. 【股票】成交量VOL隐含的交易秘密