Authored by htoooth

Merge branch 'feature/seoUrl' into release/5.4.1

... ... @@ -149,6 +149,7 @@ const logger = global.yoho.logger;
// dispatcher
try {
const htaccess = require('./doraemon/middleware/htaccess');
const subDomain = require('./doraemon/middleware/sub-domain');
const mobileRefer = require('./doraemon/middleware/mobile-refer');
const mobileCheck = require('./doraemon/middleware/mobile-check');
... ... @@ -162,6 +163,7 @@ try {
const devtool = require('./doraemon/middleware/devtools');
// YOHO 前置中间件
app.use(htaccess());
app.use(subDomain());
app.use(mobileRefer());
app.use(mobileCheck());
... ...
... ... @@ -75,3 +75,10 @@ exports.plusstarList = (req, res, next) => {
res.render('brands/plusstar', result);
}).catch(next);
};
/**
* 原url 301重定向到伪静态url
*/
exports.redirectNewPlusstar = (req, res) => {
res.redirect(301, `/${req.yoho.channel}`);
};
... ...
... ... @@ -18,6 +18,7 @@ const BOYS = 'boys';
const GIRLS = 'girls';
const KIDS = 'kids';
const LIFESTYLE = 'lifestyle';
const queryString = require('querystring');
/**
* 获取品牌一览资源位&channelType
... ... @@ -92,6 +93,16 @@ const seoMap = {
};
/**
* plusstar 分页链接伪静态
* @param string uri
* @param string originUrl 动态地址
*/
const staticUrl = (uri, originUrl) => {
originUrl = queryString.parse(_.split(originUrl, '?')[1]);
return `${uri}id${originUrl.id || 0}-p${originUrl.page || 1}/`;
};
/**
* 获取品牌一览list
* @param string $channel 频道名称
* @param int start 开始位置 1 开始
... ... @@ -231,10 +242,20 @@ exports.plusstarList = (channel, req) => {
brandIds = plustarList.brandsIds.slice((page - 1) * limit, page * limit);
if (plustarList.brandsIds.length > limit) {
pageList = pager(Math.ceil(plustarList.brandsIds.length / limit), {
channel: channel,
page: page,
id: id
});
// url 伪静态
_.forEach(pageList.pages, (val) => {
val.url = staticUrl(`/${channel}-brands/plusstar/`, val.url);
});
if (pageList.nextPage) {
pageList.nextPage.url = staticUrl(`/${channel}-brands/plusstar/`, pageList.nextPage.url);
}
if (pageList.prePage) {
pageList.prePage.url = staticUrl(`/${channel}-brands/plusstar/`, pageList.prePage.url);
}
}
if (brandIds.length > 0) {
... ...
... ... @@ -13,15 +13,33 @@ const router = express.Router(); // eslint-disable-line
const brandsController = require(`${cRoot}/brands`);
// 品牌一览
router.get('', brandsController.index);
router.get('/brands', brandsController.index); // 暂时保留
router.get(/\/(boys|girls|kids|lifestyle)-brands(\/)?$/,
function(req, res, next) {
req.query.channel = req.params[0];
if (req.query.channel === 'kids') {
req.query.msort = 365;
}
next();
}, brandsController.index);
// 悬浮出现品牌信息
router.get('/brandinfo', brandsController.brandInfo);
router.get('/brands/brandinfo', brandsController.brandInfo);
// 品牌没有加载完全,继续加载
router.post('/brandList', brandsController.brandList);
// brands/plusstar
router.get('/plusstar', brandsController.plusstarList);
router.post('/brands/brandList', brandsController.brandList);
// brands/plusstar 暂时保留
router.get('/brands/plusstar', brandsController.plusstarList);
// seo plusstar
router.get(/\/(boys|girls|kids|lifestyle)-brands\/plusstar\/id(\d*)-p(\d*)\/?/, function(req, res, next) {
req.query.channel = req.params[0];
req.query.id = req.params[1];
req.query.page = req.params[2];
console.log(req.params);
next();
}, brandsController.plusstarList);
module.exports = router;
... ...
... ... @@ -58,7 +58,7 @@ exports.index = (req, res, next) => {
pageSize: pageSize,
type: type,
pathNav: pathNav,
baseUrl: `?${querystring.stringify(req.query)}`,
baseUrl: helpers.urlFormat('/' + channel + `-t${type}`, null, 'guang'),
page: page,
total: (ret[2] && ret[2].total) || 0
},
... ... @@ -169,7 +169,7 @@ exports.editor = (req, res, next) => {
exRecos: ret[4],
gender: gender,
baseUrl: `?${querystring.stringify(req.query)}`,
baseUrl: helpers.urlFormat('/' + channel + `-author-i${authorId}`, null, 'guang'),
pageSize: pageSize,
pathNav: pathNav,
page: page,
... ...
/**
* guang-伪静态处理修改
* example: http://guang.yohobuy.com/boys-t4-p2/
* 其中 p2 的意思为:page=2 第二页
* @author: huangtao
* @date: 2016/09/01
*/
'use strict';
let _ = require('lodash');
let Handlebars = require('handlebars');
let ALL_TYPES = {
stand: 'p-page-n',
mini: 'p-n',
full: 'f-p-page-n-l-info-input-gobtn',
fullellipsis: 'f-p-pe-n-l-info-input-gobtn',
ellipsis: 'p-pe-n'
};
exports.spager = function() {
let options = arguments[arguments.length - 1];
let baseUrl = (arguments.length > 1 ? arguments[0] : null) || options.hash.baseUrl || '',
page = (options.hash.page || 1) * 1,
showNum = (options.hash.showNum || 7) * 1,
pageSize = options.hash.pageSize || 20,
totalPages = options.hash.totalPages,
totalRecords = options.hash.totalRecords,
theme = options.hash.theme || 'pager',
currentClass = options.hash.currentClass || 'cur',
// min full stand ellipsis or: f-首页, p-上一页, page-页码,n-下一页,l-最后一页,info-displayMsg,inout输入框,gobtn-goto Btn
type = options.hash.type || 'stand',
pageVar = options.hash.pageVar || 'page',
pageSizeVar = options.hash.pageSizeVar || 'pageSize';
// 清除原来page(page=1&) 重新定义page
let clearPageReg = new RegExp(pageVar + '=[^&]*(&|$)'),
clearSizeReg = new RegExp(pageSizeVar + '=[^&]*(&|$)'),
base = baseUrl.replace(clearPageReg, '');
if (options.hash.pageSize) {
base = base.replace(clearSizeReg, '');
}
base += '-' + (options.hash.pageSize ? (pageSizeVar + pageSize + '-') : '') + pageVar;
function getPageNums(ntype) {
var pageNums = [];
var num = showNum;
if (ntype === 'e') {
num = num - 2;
num = num > 2 ? num : 2;
}
/** 分页展示页码个数begin 规则:展示最靠近当前页的指定个数 **/
let pageShowMax = num % 2 === 0 ? page - 1 : page;
let pageShowMin = page;
for (let i = 0; i < Math.floor(num / 2); i++) {
pageShowMax++;
pageShowMin--;
if (pageShowMax > totalPages) {
pageShowMax = totalPages;
if (pageShowMin > 1) {
pageShowMin--;
}
}
if (pageShowMin < 1) {
pageShowMin = 1;
if (pageShowMax < totalPages) {
pageShowMax++;
}
}
}
for (let n = pageShowMin; n <= pageShowMax; n++) {
pageNums.push(n);
}
if (ntype === 'e') {
if (pageShowMin > 3) {
pageNums.unshift(1, '.');
} else if (pageShowMin === 2) {
pageNums.unshift(1);
} else if (pageShowMin === 3) {
pageNums.unshift(1, 2);
}
if (pageShowMax < totalPages - 2) {
pageNums.push('.', totalPages);
} else {
for (let x = pageShowMax + 1; x <= totalPages; x++) {
pageNums.push(x);
}
}
}
return pageNums;
}
function renderItem(arr) {
/** 分页展示页码个数end **/
let ret = '';
if (_.isArray(arr) && arr.length) {
arr.forEach((val) => {
if (val === '.') {
ret += '<a>...</a>';
} else {
ret += `<a href="${base}${val}/"` +
(page === val ? `class="${currentClass}"` : '') +
` title="第${val}页">${val}</a>`;
}
});
}
return ret;
}
function createStandItems() {
return renderItem(getPageNums());
}
function createEllipsisItems() {
return renderItem(getPageNums('e'));
}
if (!totalPages) {
if (!totalRecords || !pageSize) {
return new Handlebars.SafeString('');
} else {
totalPages = Math.ceil(totalRecords / pageSize);
}
}
let items = ALL_TYPES[type] ? ALL_TYPES[type] : type;
items = _.isArray(items) ? items : (items || ALL_TYPES.stand).split('-');
let hasPage = false; // 配置中如果配置了多次 page/pe 则将忽略,只第一次有效
let ret = `<div class="pager ${theme}">`;
items.forEach(function(val) {
switch (val) {
case 'f' :
if (page > 1) {
ret += `<a href="${base}1/" title="首页">首页</a>`;
}
break;
case 'p' :
if (page > 1) {
ret += `<a href="${base}` + (page - 1) + '/' +
'" title="上一页"><span class="iconfont">&#xe60e;</span>上一页</a>';
}
break;
case 'n' :
if (page < totalPages) {
ret += `<a href="${base}` + (page + 1) + '/' +
'" title="下一页">下一页<span class="iconfont">&#xe60c;</span></a>';
}
break;
case 'l' :
if (page < totalPages) {
ret += `<a href="${base}${totalPages}" title="尾页">尾页</a>`;
}
break;
case 'info' :
ret += '共{totalRecords}条/{totalPages}页';
break;
case 'input' :
// input
// ret += '<span class="{cls}"><input value="{value}" type="text"></span>';
break;
case 'gobtn' :
// goto btn
break;
case 'page' :
if (!hasPage) {
ret += createStandItems();
}
hasPage = true;
break;
case 'pe' :
if (!hasPage) {
ret += createEllipsisItems();
}
hasPage = true;
break;
}
});
if (options.fn) {
ret += options.fn(options.context);
}
ret += '</div>';
return new Handlebars.SafeString(ret);
};
... ...
... ... @@ -24,7 +24,7 @@
layoutsDir: doraemon,
partialsDir: [path.join(__dirname, 'views/partial')],
views: path.join(__dirname, 'views/action'),
helpers: Object.assign(require('./helpers/pager'), global.yoho.helpers)
helpers: Object.assign(require('./helpers/pager'), require('./helpers/pager-seo'), global.yoho.helpers)
}));
// require('./helpers/json');
... ...
... ... @@ -8,6 +8,7 @@
const _ = require('lodash');
const moment = require('moment');
const ghelper = require('./guang-helper');
const urlHelper = require('./urlHelper');
const helpers = global.yoho.helpers;
const serviceApi = global.yoho.ServiceAPI;
... ... @@ -166,15 +167,7 @@ const _formatArticle = (articleData, showTag, showAuthor, channel) => {
let authorId = articleData.author.author_id;
if (authorId) {
let aparam = {
author_id: authorId
};
if (channel) {
aparam.channel = channel;
}
result.editorUrl = helpers.urlFormat('/Index/editor', aparam, 'guang');
result.editorUrl = urlHelper.editorUrl(channel, authorId);
}
let tags = [];
... ... @@ -439,7 +432,7 @@ const getCategory = (currentSortId, channel) => {
typeId: cat.id,
type: cat.name,
isActive: String(cat.id) === String(currentSortId),
navUrl: helpers.urlFormat('/index/index', param, 'guang')
navUrl: urlHelper.listUrl(channel, cat.id)
});
}
}
... ...
/**
* Created by TaoHuang on 2017/2/21.
*/
'use strict';
const helpers = global.yoho.helpers;
module.exports.editorUrl = function(channel, authorId) {
return helpers.urlFormat(`/${channel}-author-i${authorId}/`, null, 'guang');
};
module.exports.listUrl = function(channel, type) {
return helpers.urlFormat(`/${channel}-t${type}/`, null, 'guang');
};
... ...
... ... @@ -20,7 +20,7 @@
{{/ msgs}}
</div>
<div class="msg-pager pager">
{{gpager baseUrl totalRecords=total page=page type="ellipsis" theme="msg-pager"}}
{{spager baseUrl totalRecords=total page=page pageVar="p" type="ellipsis" theme="msg-pager"}}
</div>
</div>
<div class="right-side">
... ...
... ... @@ -30,7 +30,7 @@
{{> msg}}
{{/ msgs}}
</div>
{{gpager baseUrl totalRecords=total page=page type="ellipsis" theme="msg-pager"}}
{{spager baseUrl totalRecords=total page=page pageVar="p" type="ellipsis" theme="msg-pager"}}
</div>
</div>
<div class="right-side">
... ...
... ... @@ -64,8 +64,7 @@ const homeNav = (req) => {
{name: '我的信息', href: '/home/message', count: 0},
{
name: '在线客服',
href: _.get(req.app.locals.pc, 'clientService.new', false) ?
'/service/client' : 'http://chat8.live800.com/live800/chatClient/chatbox.jsp?companyID=620092&amp;configID=149091&amp;jid=8732423409',
href: _.get(req.app.locals.pc, 'clientService.new', false) ? 'http://chat8.live800.com/live800/chatClient/chatbox.jsp?companyID=620092&amp;configID=149091&amp;jid=8732423409' : '/service/client',
isBlank: true
}
]
... ...
... ... @@ -62,7 +62,6 @@ router.get('/outlets/:channel', outlets.channel); // 奥莱频道页
// 商品分类列表页
router.get('/list', outletsList.index);
router.get(/\/pro_([\d]+)_([\d]+)\/(.*).html(.*)/, detail.redirectNewRouter); // 老的商品详情routers
router.get(/\/p([\d]+)(.*)/, detail.showMain); // 新的商品详情routers
router.get('/detail/comment', detail.indexComment); // 商品评论
router.get('/detail/consult', detail.indexConsult); // 商品咨询
... ...
... ... @@ -15,7 +15,7 @@ module.exports = app => {
app.use('/product', require('./apps/product')); // 商品相关页面
app.use(require('./apps/passport')); // 登录注册
app.use('/home', require('./apps/home')); // 会员中心
app.use('/brands', require('./apps/brands')); // 品牌一览
app.use(require('./apps/brands')); // 品牌一览
app.use('/guang', require('./apps/guang')); // 逛
app.use('/cart', require('./apps/cart'));// 购物车
app.use('/help', require('./apps/help'));// 帮助中心
... ...
/**
* 后端:改写和跳转:中间件。
*
* 说明:
* 该中间件使用:使用文件夹下 rule 的所有的文件,以文件名的形式,载入该模块
* 文件的名字建议以网站子域名的形式存在,如:guang, item
* 每个文件夹是一个模块。
*
* 模块的导出形式:见 guang 模块的使用。
* Created by TaoHuang on 2017/2/21.
*/
'use strict';
const fs = require('fs');
const _ = require('lodash');
const path = require('path');
const helpers = global.yoho.helpers;
const logger = global.yoho.logger;
const TYPE = require('./type');
const DIR = './rules/';
const curDir = path.resolve(__dirname, DIR);
const files = fs.readdirSync(curDir);
let domainRules = {};
// 启动时就载入模块
files.forEach((file) => {
let info = fs.statSync(path.resolve(curDir, file));
if (info.isFile()) {
let module = path.basename(file, '.js');
let loadPath = DIR + module;
try {
domainRules[module] = require(loadPath);
} catch (e) {
logger.error('load rules wrong', e);
}
}
});
// 选择模块
const loadRule = (domain) => {
return domainRules[domain] || domain.default;
};
// 已处理完
class Done {
constructor(val) {
this.__val = val;
}
map(fn) {
return new Done(fn(this.__val));
}
val() {
return this.__val;
}
static of(val) {
return new Done(val);
}
}
const stepX = (fn) => {
return (maybe) => {
if (maybe instanceof Done) {
return maybe;
}
return fn(maybe);
};
};
// 判断是否需要处理
const isNeedHandle = (req, rule) => {
return (_.isRegExp(rule.origin) && rule.origin.test(req.url)) ||
(_.isFunction(rule.origin) && rule.origin(req) ||
(_.isString(rule.origin) && _.isEqual(req.url, rule.origin)));
};
// 正则
const step1 = (req, rule, url) => {
if (_.isRegExp(rule.origin)) {
if (_.isFunction(rule.target)) {
return Done.of(url.replace(rule.origin, _.partial(rule.target, req)));
} else if (_.isString(rule.target)) {
return Done.of(url.replace(rule.origin, rule.target));
}
}
return url;
};
// 函数
const step2 = (req, rule, url) => {
if (_.isFunction(rule.origin)) {
if (_.isFunction(rule.target)) {
return Done.of(rule.target(req));
} else if (_.isString(rule.target)) {
return Done.of(rule.target);
}
}
return url;
};
// 字符
const step3 = (req, rule, url) => {
if (_.isString(rule.origin)) {
if (_.isFunction(rule.target)) {
return Done.of(rule.target(req));
} else if (_.isString(rule.target)) {
return Done.of(rule.target);
}
}
return url;
};
const getResultStatus = (req, rule, newUrl) => {
if (newUrl instanceof Done) {
if (newUrl.val() === req.url) {
return {
needNext: true,
needRedirect: false,
process: false
};
} else if (rule.type === TYPE.redirect) {
return {
needNext: false,
needRedirect: true,
process: true,
url: newUrl.val()
};
} else if (rule.type === TYPE.rewrite) {
return {
needNext: true,
needRedirect: false,
process: true,
url: newUrl.val()
};
}
}
return {
needNext: true,
needRedirect: false,
process: false
};
};
const isJsonp = (req) => {
return _.includes(req.url, 'callback');
};
/**
* 1. origin 可接受是 正则 , 函数, 纯字符串
* 2. target 可接受是 匹配字符串 , 函数, 纯字符串
* 3. 301: 跳转
* rewrite: 改写 url
* @returns {Function}
*/
module.exports = () => {
return (req, res, next) => {
if (req.subdomains.length > 1 && req.subdomains[1] === 'www') {
return res.redirect(301, helpers.urlFormat(req.path, req.query || '', req.subdomains[0]));
}
req.isMobile = /(nokia|iphone|android|ipad|motorola|^mot\-|softbank|foma|docomo|kddi|up\.browser|up\.link|htc|dopod|blazer|netfront|helio|hosin|huawei|novarra|CoolPad|webos|techfaith|palmsource|blackberry|alcatel|amoi|ktouch|nexian|samsung|^sam\-|s[cg]h|^lge|ericsson|philips|sagem|wellcom|bunjalloo|maui|symbian|smartphone|midp|wap|phone|windows ce|iemobile|^spice|^bird|^zte\-|longcos|pantech|gionee|^sie\-|portalmmm|jig\s browser|hiptop|^ucweb|^benq|haier|^lct|opera\s*mobi|opera\*mini|320x320|240x320|176x220)/i.test(req.get('user-agent')); // eslint-disable-line
if (req.xhr || isJsonp(req)) {
return next();
}
let rules = loadRule(req.subdomains[0]);
let useRule = _.find(rules, rule => isNeedHandle(req, rule));
if (!useRule) {
return next();
}
let step1x = stepX(_.partial(step1, req, useRule, _));
let step2x = stepX(_.partial(step2, req, useRule, _));
let step3x = stepX(_.partial(step3, req, useRule, _));
let processAfter = _.partial(getResultStatus, req, useRule, _);
let processing = _.flow(step1x, step2x, step3x);
let process = _.flow(processing, processAfter);
let result = process(req.url);
if (result.process) {
if (result.needRedirect) {
return res.redirect(result.url);
}
if (result.needNext) {
req.url = result.url;
return next();
}
}
return next();
};
};
... ...
/**
* Created by TaoHuang on 2017/2/22.
*/
'use strict';
module.exports = [
];
... ...
/**
* Created by TaoHuang on 2017/2/21.
*/
'use strict';
const helpers = global.yoho.helpers;
const TYPE = require('../type');
const MOBILE_DOMAIN = '//guang.m.yohobuy.com';
module.exports = [
// 老的首页
{
type: TYPE.redirect,
origin: (req) => {
return req.path === '/';
},
target: (req) => {
return helpers.urlFormat(`/${req.query.channel || 'boys'}/`, null, 'guang');
}
},
// 首页
{
type: TYPE.rewrite,
origin: /^\/(boys|girls|kids|lifestyle)(\/*)$/,
target: (req, match, p1) => {
req.query.channel = p1;
return `/guang/?chanel=${p1}`;
}
},
// 首页 + 类型
{
type: TYPE.rewrite,
origin: /^\/(boys|girls|kids|lifestyle)-t([\d]+)(\/*)$/,
target: (req, match, p1, p2) => {
req.query.channel = p1;
req.query.type = p2;
return `/guang/?chanel=${p1}&type=${p2}`;
}
},
// 列表页 + 类型 + 翻页
{
type: TYPE.rewrite,
origin: /^\/(boys|girls|kids|lifestyle)-t([\d]+)-p([\d]+)(\/*)$/,
target: (req, match, p1, p2, p3) => {
req.query.channel = p1;
req.query.type = p2;
req.query.page = p3;
return `/guang/index/index/?type=${p2}&channel=${p3}`;
}
},
// 编缉首页
{
type: TYPE.rewrite,
origin: /^\/(boys|girls|kids|lifestyle)-author-i([\d]+)(\/*)$/,
target: (req, match, p1, p2) => {
req.query.channel = p1;
req.query.author_id = p2;
req.mobileUrl = `${MOBILE_DOMAIN}/author/${p2}/`;
return `/guang/index/editor?channel=${p1}&author_id=${p2}`;
}
},
// 编缉首页 + 翻页
{
type: TYPE.rewrite,
origin: /^\/(boys|girls|kids|lifestyle)-author-i([\d]+)-p([\d]+)(\/*)$/,
target: (req, match, p1, p2, p3) => {
req.query.channel = p1;
req.query.author_id = p2;
req.query.page = p3;
req.mobileUrl = `${MOBILE_DOMAIN}/author/${p2}`;
return `/guang/index/editor?channel=${p1}&author_id=${p2}&page=${p3}`;
}
}
];
... ...
/**
* Created by TaoHuang on 2017/2/21.
*/
'use strict';
const helpers = global.yoho.helpers;
const TYPE = require('../type');
const MOBILE_DOMAIN = '//item.m.yohobuy.com';
module.exports = [
// 商品详情页老链接,形式一
{
type: TYPE.redirect,
origin: /^\/product\/pro_([\d]+)_([\d]+)\/(.*).html(.*)/,
target: (req, match, p1, p2, p3, p4) => {
req.mobileUrl = `${MOBILE_DOMAIN}/product/pro_${p1}_1/1.html${p4 ? p4 : ''}`;
return helpers.urlFormat(`/p${p1}.html${p4}`, null, 'item');
}
},
// 商品详情页老链接,形式二
{
type: TYPE.redirect,
origin: /^\/product\/pro_([\d]+)(.*)/,
target: (req, match, p1) => helpers.urlFormat(`/p${p1}.html`, null, 'item')
},
// 商品详情页新链接
{
type: TYPE.rewrite,
origin: /^\/p([\d]+).html(.*)/,
target: (req, match, p1, p2) => {
req.mobileUrl = `${MOBILE_DOMAIN}/product/pro_${p1}_1/1.html${p2 ? p2 : ''}`;
return `/product${req.url}`;
}
}
];
... ...
/**
* Created by TaoHuang on 2017/2/21.
*/
'use strict';
const helpers = global.yoho.helpers;
const TYPE = require('../type');
module.exports = [
{
type: TYPE.redirect,
origin: /.*/,
target: req => helpers.urlFormat(req.url, null, 'www')
}
];
... ...
/**
* Created by TaoHuang on 2017/2/22.
*/
'use strict';
// const _ = require('lodash');
const helpers = global.yoho.helpers;
const TYPE = require('../type');
module.exports = [
// 商品详情页老链接,形试一
{
type: TYPE.redirect,
origin: /^\/product\/pro_([\d]+)_([\d]+)\/(.*).html(.*)/,
target: (req, match, p1, p2, p3, p4) => helpers.urlFormat(`/p${p1}.html${p4}`, null, 'item')
},
// 商品详情页老链接,形式二
{
type: TYPE.redirect,
origin: /^\/product\/pro_([\d]+)(.*)/,
target: (req, match, p1) => helpers.urlFormat(`/p${p1}.html`, null, 'item')
},
// 商品详情页新链接
{
type: TYPE.redirect,
origin: /\/p([\d]+)(.*)/,
target: req => helpers.urlFormat(req.url, null, 'item')
},
// 品牌一览
{
type: TYPE.redirect,
origin: /\/brands\?.*channel=(boys|girls|kids|lifestyle)/,
target: (req) => {
return helpers.urlFormat(
`/${req.query.channel}-brands/`,
null,
'www'
);
}
},
// 品牌一览专题
{
type: TYPE.redirect,
origin: (req) => req.path === '/brands/plusstar',
target: (req) => {
return helpers.urlFormat(
`/${req.query.channel}-brands/plusstar/id${req.query.id || 0}-p${req.query.page || 1}/`,
null,
'www'
);
}
}
];
... ...
/**
* Created by TaoHuang on 2017/2/22.
*/
'use strict';
module.exports = {
redirect: '301',
rewrite: 'rewrite'
};
... ...
'use strict';
const cache = global.yoho.cache.master;
const _ = require('lodash');
const logger = global.yoho.logger;
const config = global.yoho.config;
const helpers = global.yoho.helpers;
let ONE_DAY = 60 * 60 * 24;
const MAX_QPS = config.maxQps;
const PAGES = {
'/product/\\/pro_([\\d]+)_([\\d]+)\\/(.*)/': 5,
'/product/list/index': 5
};
const IP_WHITE_LIST = [
'106.38.38.146',
'218.94.75.58'
];
function urlJoin(a, b) {
if (_.endsWith(a, '/') && _.startsWith(b, '/')) {
return a + b.substring(1, b.length);
} else if (!_.endsWith(a, '/') && !_.startsWith(b, '/')) {
return a + '/' + b;
} else {
return a + b;
}
}
const limiter = require('../middleware/limiter/index');
module.exports = (req, res, next) => {
let remoteIp = req.get('X-Forwarded-For') || '';
if (remoteIp.indexOf(',') > 0) {
let arr = remoteIp.split(',');
remoteIp = arr[0];
}
if (remoteIp &&
!_.get(req.app.locals, 'pc.sys.noLimiter') &&
!_.includes(IP_WHITE_LIST, remoteIp)) { // 判断获取remoteIp成功,并且开关未关闭
let key = `pc:limiter:${remoteIp}`;
res.on('render', function() {
let route = req.route ? req.route.path : '';
let appPath = req.app.mountpath;
if (_.isArray(route) && route.length > 0) {
route = route[0];
}
let pageKey = urlJoin(appPath, route.toString()); // route may be a regexp
let pageIncr = PAGES[pageKey] || 0;
if (pageIncr > 0) {
cache.incrAsync(key, pageIncr);
}
});
const limiterPage = () => {
let refer = req.method === 'GET' ? req.get('Referer') : '';
let limitAPI = helpers.urlFormat('/3party/check', {refer: refer});
let limitPage = helpers.urlFormat('/3party/check', {refer: req.protocol + '://' + req.get('host') + req.originalUrl});
if (_.indexOf(['/3party/check', '/passport/imagesNode', '/passport/cert/headerTip'], req.path) >= 0) {
return next();
}
if (req.xhr) {
return res.json({
code: 400,
data: {refer: limitAPI}
});
}
return res.redirect(limitPage);
};
cache.getAsync(key).then((result) => {
if (result && _.isNumber(result)) {
if (result > MAX_QPS) { // 判断 qps
cache.touch(key, ONE_DAY);
logger.info('req limit', key);
return limiterPage();
} else {
cache.incrAsync(key, 1); // qps + 1
return next();
}
} else {
cache.setAsync(key, 1, 60); // 设置key,1m失效
return next();
}
}).catch((err) => {
logger.error(`request limiter get key[${key}] from cache error.`, err);
return next();
});
} else {
return next();
}
return limiter(req, res, next);
};
... ...
'use strict';
const _ = require('lodash');
const logger = global.yoho.logger;
const ip = require('./rules/ip-list');
const userAgent = require('./rules/useragent');
const qpsLimiter = require('./rules/qps-limit');
const fakerLimiter = require('./rules/faker-limit');
const captchaPolicy = require('./policies/captcha');
const reporterPolicy = require('./policies/reporter');
const IP_WHITE_LIST = [
// '106.38.38.146',
// '218.94.75.58'
];
const limiter = (rule, policy, context) => {
return rule(context, policy);
};
module.exports = (req, res, next) => {
let remoteIp = req.get('X-Forwarded-For') || req.connection.remoteAddress;
logger.debug('request remote ip: ', remoteIp);
if (remoteIp.indexOf(',') > 0) {
let arr = remoteIp.split(',');
remoteIp = arr[0];
}
const excluded = _.includes(IP_WHITE_LIST, remoteIp);
const enabled = !_.get(req.app.locals, 'pc.sys.noLimiter');
// 判断获取remoteIp成功,并且开关未关闭
if (enabled && remoteIp && !excluded) {
const context = {
req: req,
res: res,
next: next,
remoteIp: remoteIp
};
Promise.all([
limiter(userAgent, captchaPolicy, context),
limiter(ip, captchaPolicy, context),
limiter(qpsLimiter, captchaPolicy, context),
//limiter(fakerLimiter, reporterPolicy, context)
]).then((results) => {
let allPass = true, exclusion = false, policy = null;
logger.debug('limiter result: ' + JSON.stringify(results));
_.forEach(results, (result) => {
if (typeof result === 'object' && !exclusion) {
exclusion = result.exclusion;
}
if (!excluded && typeof result === 'function') {
allPass = false;
}
if (typeof result === 'function') {
policy = result;
}
});
if (exclusion) {
return next();
} else if (!allPass && policy) {
policy(req, res, next);
} else {
return next();
}
}).catch((err) => {
logger.error(err);
return next();
});
} else {
return next();
}
};
... ...
'use strict';
const helpers = global.yoho.helpers;
const _ = require('lodash');
const WHITE_LIST = [
'/3party/check',
'/passport/imagesNode',
'/passport/cert/headerTip'
];
module.exports = (req, res, next) => {
let refer = req.method === 'GET' ? req.get('Referer') : '';
let limitAPI = helpers.urlFormat('/3party/check', {refer: refer});
let limitPage = helpers.urlFormat('/3party/check', {refer: req.protocol + '://' + req.get('host') + req.originalUrl});
if (_.indexOf(WHITE_LIST, req.path) >= 0) {
return next();
}
if (req.xhr) {
return res.json({
code: 400,
data: {refer: limitAPI}
});
}
return res.redirect(limitPage);
};
... ...
'use strict';
module.exports = (req, res, next) => {
return next();
};
... ...
'use strict';
const logger = global.yoho.logger;
const cache = global.yoho.cache.master;
const ONE_DAY = 60 * 60 * 24;
module.exports = (limiter, policy) => {
const req = limiter.req,
res = limiter.res,
next = limiter.next;
const key = `pc:limiter:faker:${limiter.remoteIp}`;
if (req.header('X-Requested-With') === 'XMLHttpRequest') {
cache.decrAsync(key, 1);
}
res.on('render', function() {
cache.incrAsync(key, 1);
});
return cache.getAsync(key).then((result) => {
if (result) {
if (result > 100) {
return Promise.resolve(policy);//policy(req, res, next);
} else {
return Promise.resolve(true);
}
} else {
cache.setAsync(key, 1, ONE_DAY); // 设置key,1m失效
return Promise.resolve(true);
}
});
};
... ...
'use strict';
const cache = global.yoho.cache.master;
const _ = require('lodash');
module.exports = (limiter) => {
const key = `pc:limiter:${limiter.remoteIp}`;
return cache.getAsync(key).then((result) => {
if (result && _.isNumber(result)) {
return Promise.resolve({
exclusion: result === -1
});
} else {
return Promise.resolve(true);
}
});
};
... ...
'use strict';
const logger = global.yoho.logger;
const cache = global.yoho.cache.master;
const config = global.yoho.config;
const ONE_DAY = 60 * 60 * 24;
const MAX_QPS = config.maxQps;
const _ = require('lodash');
const PAGES = {
'/product/\\/pro_([\\d]+)_([\\d]+)\\/(.*)/': 5,
'/product/list/index': 5
};
function urlJoin(a, b) {
if (_.endsWith(a, '/') && _.startsWith(b, '/')) {
return a + b.substring(1, b.length);
} else if (!_.endsWith(a, '/') && !_.startsWith(b, '/')) {
return a + '/' + b;
} else {
return a + b;
}
}
module.exports = (limiter, policy) => {
const req = limiter.req,
res = limiter.res,
next = limiter.next;
const key = `pc:limiter:${limiter.remoteIp}`;
res.on('render', function() {
let route = req.route ? req.route.path : '';
let appPath = req.app.mountpath;
if (_.isArray(route) && route.length > 0) {
route = route[0];
}
let pageKey = urlJoin(appPath, route.toString()); // route may be a regexp
let pageIncr = PAGES[pageKey] || 0;
if (/^\/p([\d]+)/.test(req.path)) {
pageIncr = 5;
}
if (pageIncr > 0) {
cache.incrAsync(key, pageIncr);
}
});
return cache.getAsync(key).then((result) => {
logger.debug('qps limiter: ' + key + '@' + result + ' max: ' + MAX_QPS);
if (result && _.isNumber(result)) {
if (result === -1) {
return Promise.resolve(true);
}
if (result > MAX_QPS) { // 判断 qps
cache.touch(key, ONE_DAY);
logger.debug('req limit', key);
return Promise.resolve(policy);
} else {
cache.incrAsync(key, 1); // qps + 1
return Promise.resolve(true);
}
} else {
cache.setAsync(key, 1, 60); // 设置key,1m失效
return Promise.resolve(true);
}
});
};
... ...
'use strict';
const cache = global.yoho.cache.master;
const _ = require('lodash');
const logger = global.yoho.logger;
module.exports = (limiter, policy) => {
const req = limiter.req,
res = limiter.res,
next = limiter.next;
const blackKey = 'pc:limiter:ua:black',
whiteKey = 'pc:limiter:ua:white';
const ua = limiter.req.header('User-Agent');
return Promise.all([
cache.getAsync(blackKey),
cache.getAsync(whiteKey)
]).then((args) => {
const blacklist = args[0] || [], whitelist = args[1] || [];
if (blacklist.length === 0 && whitelist.length === 0) {
return Promise.resolve(true);
}
const test = (list) => {
let result = false;
_.each(list, (item) => {
let regexp;
try {
regexp = new RegExp(item);
} catch (e) {
logger.error(e);
}
if (regexp.test(ua)) {
result = true;
}
});
return result;
};
if (test(blacklist)) {
return Promise.resolve(policy);
} else if (test(whitelist)) {
return Promise.resolve({
exclusion: true
});
} else {
return Promise.resolve(true);
}
});
};
... ...
... ... @@ -4,13 +4,10 @@
'use strict';
module.exports = () => {
return (req, res, next) => {
let isMobile = /(nokia|iphone|android|ipad|motorola|^mot\-|softbank|foma|docomo|kddi|up\.browser|up\.link|htc|dopod|blazer|netfront|helio|hosin|huawei|novarra|CoolPad|webos|techfaith|palmsource|blackberry|alcatel|amoi|ktouch|nexian|samsung|^sam\-|s[cg]h|^lge|ericsson|philips|sagem|wellcom|bunjalloo|maui|symbian|smartphone|midp|wap|phone|windows ce|iemobile|^spice|^bird|^zte\-|longcos|pantech|gionee|^sie\-|portalmmm|jig\s browser|hiptop|^ucweb|^benq|haier|^lct|opera\s*mobi|opera\*mini|320x320|240x320|176x220)/i.test(req.get('user-agent')); // eslint-disable-line
if (isMobile) {
req.url = req.url.replace(/^\/product\/p([\d]+).html(.*)/, '/product/pro_$1_1/1.html$2');
return res.redirect(`//m.yohobuy.com${req.url}`);
if (req.isMobile) {
return res.redirect(req.mobileUrl || `//m.yohobuy.com${req.url}`);
}
next();
return next();
};
};
... ...
... ... @@ -54,8 +54,8 @@ module.exports = () => {
if (guangDetailReg.test(url)) {
data.mobileRefer += url.replace(/\/guang\/info\/index/, '/info/index');
} else if (url === '/guang/Index/editor') {
data.mobileRefer += `/author/index?id=${req.query.author_id}`;
} else if (url === '/guang/index/editor') {
data.mobileRefer += `/author/${req.query.author_id}/`;
}
} else if (proRegNew.test(url)) {
data.mobileRefer = url.replace(proRegNew, `//${domain}/product/pro_$1_1/1.html$2`);
... ...
'use strict';
const seoMap = {
/* eslint-disable */
'gender=1,3&msort=1,3': {
... ...
'use strict';
const seoMap = {
/* eslint-disable */
'/': {
... ...
... ... @@ -4,39 +4,23 @@
* @date: 2016/6/16
*/
'use strict';
const helpers = global.yoho.helpers;
module.exports = () => {
return (req, res, next) => {
let searchReg = /^\/product\//,
guangReg = /^\/guang/,
guangDetailReg = /.html$/;
if (req.subdomains.length > 1 && req.subdomains[1] === 'www') {
return res.redirect(301, helpers.urlFormat(req.path, req.query || '', req.subdomains[0]));
}
if (req.subdomains.length) {
switch (req.subdomains[0]) {
case 'www': // 主站
case 'cdnsrcwww': // 主站的回源地址
case 'shop': // 商家入驻
break;
case 'new': // 原新版 重定向到301
return res.redirect(301, helpers.urlFormat(req.url, null, 'www'));
case 'new':
case 'item':// 商品详情页
if (/^\/p([\d]+)/.test(req.url)) { // new
req.url = `/product${req.url}`;
}
if (/^\/product\/pro_/.test(req.url)) { // old
return res.redirect(301,
req.url.replace(/^\/product\/pro_([\d]+)_([\d]+)\/(.*).html(.*)/, '/p$1.html$4')
);
}
break;
case 'guang': // 逛
case 'cdnsrcguang': // 逛CDN回源解析
{ // eslint-disable-line
let guangReg = /^\/guang/;
let guangDetailReg = /.html$/;
if (guangDetailReg.test(req.path)) {
// req.url = '/guang/detail' + req.url;
/\/([\d]*).html(\?)?(.*)/.exec(req.url);
... ... @@ -49,7 +33,11 @@ module.exports = () => {
req.url = '/guang' + req.url;
}
break;
case 'search': // 搜索
}
case 'search': // 搜索
{ // eslint-disable-line
let searchReg = /^\/product\//;
if (!searchReg.test(req.path)) {
if (req.path === '/api/suggest') {
req.url = '/product/api/suggest';
... ... @@ -58,21 +46,27 @@ module.exports = () => {
}
}
break;
}
case 'list': // 商品列表
case 'cdnsrclist': // 商品列表CDN回源解析
{ // eslint-disable-line
if (!req.path || req.path === '/') {
req.url = '/product/list/index';
} else if (req.path === '/new') {
req.url = '/product/list/new';
}
break;
}
case 'sale': // 促销
case 'cdnsrcsale': // 促销CDN回源解析
{ // eslint-disable-line
if (!req.path || req.path === '/') {
req.url = '/product/sale/special/detail';
}
break;
}
default: // 其它(识别为品牌)
{ // eslint-disable-line
if (!req.path || req.path === '/') {
req.url = `/product/index/brand?domain=${req.subdomains[0]}`;
req.query.domain = req.subdomains[0];
... ... @@ -81,6 +75,7 @@ module.exports = () => {
req.query.domain = req.subdomains[0];
}
break;
}
}
}
next();
... ...
... ... @@ -189,7 +189,7 @@
</div>
<div class="left">
<span class="iconfont rgbf">&#xe602;</span>
<a href="{{#if @root.pc.clientService.new}}/service/client{{else}}http://chat8.live800.com/live800/chatClient/chatbox.jsp?companyID=620092&amp;configID=149091&amp;jid=8732423409{{/if}}" target="_blank">
<a href="{{#if @root.pc.clientService.new}}http://chat8.live800.com/live800/chatClient/chatbox.jsp?companyID=620092&amp;configID=149091&amp;jid=8732423409{{else}}/service/client{{/if}}" target="_blank">
<span class="red">便捷</span>
<span class="rgbf">在线客服</span>
</a>
... ...