Authored by 沈志敏

Merge branch 'master' of git.yoho.cn:fe/yohobuywap-node into feature/featureTemplate

... ... @@ -51,6 +51,11 @@ app.set('etag', false);
app.enable('trust proxy');
// 请求限制中间件
if (!app.locals.devEnv) {
app.use(require('./doraemon/middleware/limiter'));
}
// 指定libray目录
global.utils = path.resolve('./utils');
... ...
'use strict';
const _ = require('lodash');
const cache = global.yoho.cache.master;
exports.index = (req, res) => {
res.render('check', {
width750: true,
localCss: true
});
};
exports.submit = (req, res) => {
let captchaCode = _.get(req.session, 'captcha');
let remoteIp = req.get('X-Forwarded-For') || req.ip;
if (remoteIp.indexOf(',') > 0) {
let arr = remoteIp.split(',');
remoteIp = arr[0];
}
if (req.body.captcha === captchaCode) {
let key = `pc:limiter:${remoteIp}`;
cache.delAsync(key).then(() => {
return res.json({
code: 200
});
}).catch(() => {
return res.json({
code: 400
});
});
} else {
return res.json({
code: 400
});
}
};
... ...
... ... @@ -9,9 +9,12 @@
const router = require('express').Router(); // eslint-disable-line
const cRoot = './controllers';
const ads = require(`${cRoot}/ads`);
const check = require(`${cRoot}/check`);
// routers
router.get('/ads', ads.index);
router.get('/check', check.index);
router.post('/check/submit', check.submit);
module.exports = router;
... ...
<div class="check-page">
<div class="title">请输入正确的验证码,继续访问</div>
<div id="js-img-check"></div>
<div class="submit">
确认
</div>
</div>
... ...
... ... @@ -181,6 +181,7 @@ exports.orderSub = (req, res, next) => {
let yohoCoin = req.body.yohoCoin || 0;
let skuList = req.body.skuList || '';
let orderInfo;
let isWechat = req.yoho.isWechat;
try {
orderInfo = JSON.parse(req.cookies['order-info']);
... ... @@ -260,11 +261,11 @@ exports.orderSub = (req, res, next) => {
result = yield cartModel.orderSub(uid, addressId, 'bundle', deliveryTimeId,
deliveryId, invoices, paymentTypeId, paymentType, msg, couponCode,
yohoCoin, null, unionKey, userAgent, times, activityInfo, ip);
yohoCoin, null, unionKey, userAgent, times, activityInfo, ip, isWechat);
} else {
result = yield cartModel.orderSub(uid, addressId, cartType, deliveryTimeId,
deliveryId, invoices, paymentTypeId, paymentType, msg, couponCode,
yohoCoin, skuList, unionKey, userAgent, null, null, ip);
yohoCoin, skuList, unionKey, userAgent, null, null, ip, isWechat);
}
// 提交成功清除Cookie
... ...
... ... @@ -178,12 +178,13 @@ exports.ticketsOrderCompute = (uid, productSku, buyNumber, yohoCoin) => {
* @param string|null $userAgent 联盟过来用户下单时需要的User-Agent信息
* @param int $times
* @param null $activityInfo 套餐数据
* @param isWechat 是否是微信商城
* @return array 接口返回的数据
*/
exports.orderSub = (uid, addressId, cartType, deliveryTime,
deliveryWay, invoices, paymentId, paymentType, remark,
couponCode, yohoCoin, skuList, qhyUnio,
userAgent, times, activityInfo, ip) => {
userAgent, times, activityInfo, ip, isWechat) => {
if (!qhyUnio) {
qhyUnio = '';
}
... ... @@ -215,7 +216,7 @@ exports.orderSub = (uid, addressId, cartType, deliveryTime,
return shoppingAPI.orderSub(uid, addressId, cartType, deliveryTime,
deliveryWay, invoices, paymentId, paymentType,
remark, couponCode, yohoCoin, skuList, qhyUnio,
userAgent, times, activityInfo, ip).then(orderSubRes => {
userAgent, times, activityInfo, ip, isWechat).then(orderSubRes => {
let finalResult = {};
if (orderSubRes && orderSubRes.data && orderSubRes.data.is_hint === 'Y') {
... ...
... ... @@ -7,6 +7,7 @@
const _ = require('lodash');
const api = global.yoho.API;
const helpers = global.yoho.helpers;
const utils = require('../../../utils');
const genderMap = {
boys: '1,3',
... ... @@ -33,27 +34,27 @@ let _processCateData = (list, channel) => {
// 如果有二级菜单,二级菜单跳转,否则一级菜单跳转
if (firstItem.sub && firstItem.sub.length) {
_.map(firstItem.sub, function(secondItem) {
secondItem.url = helpers.urlFormat('/', {
secondItem.url = helpers.urlFormat('/', utils.mapSort({
sort: _.get(secondItem, 'relation_parameter.sort'),
sort_name: secondItem.category_name,
gender: genderMap[key] || ''
}, 'list');
}), 'list');
});
firstItem.sub.unshift({
category_name: `全部${firstItem.category_name}`,
url: helpers.urlFormat('/', {
url: helpers.urlFormat('/', utils.mapSort({
sort: _.get(firstItem, 'relation_parameter.sort'),
sort_name: firstItem.category_name,
gender: genderMap[key] || ''
}, 'list')
}), 'list')
});
} else {
firstItem.url = helpers.urlFormat('/', {
firstItem.url = helpers.urlFormat('/', utils.mapSort({
sort: _.get(firstItem, 'relation_parameter.sort'),
sort_name: firstItem.category_name,
gender: genderMap[key] || ''
}, 'list');
}), 'list');
}
});
});
... ...
... ... @@ -103,7 +103,7 @@ const editorRedirect = (req, res, next) => {
param = '?' + param;
}
redirectUrl += `-${id}/${param}`;
res.redirect(redirectUrl);
res.redirect(301, redirectUrl);
} else {
return next();
}
... ...
... ... @@ -469,7 +469,7 @@ const indexRedirect = (req, res, next) => {
param = '?' + param;
}
redirectUrl += `${id}.html${param}`;
res.redirect(redirectUrl);
res.redirect(301, redirectUrl);
} else {
return next();
}
... ...
... ... @@ -111,7 +111,9 @@ const local = {
res.render('login', {
width750: true,
loginIndex: true, // 模板中使用JS的标识
captchaShow: _.get(req.session, 'login.errorCount') <= 0,
// captchaShow: _.get(req.session, 'login.errorCount') <= 0,
captchaShow: true, // 170306 因为暴力破解密码问题,要求每次都展示验证码
// 返回的URL链接
backUrl: 'javascript:history.go(-1)', // eslint-disable-line
... ... @@ -162,7 +164,9 @@ const local = {
// 返回的URL链接
backUrl: 'javascript:history.go(-1)', // eslint-disable-line
loginInternational: true, // 模板中使用JS的标识
captchaShow: _.get(req.session, 'login.errorCount') <= 0,
// captchaShow: _.get(req.session, 'login.errorCount') <= 0,
captchaShow: true, // 170306 因为暴力破解密码问题,要求每次都展示验证码
isPassportPage: true, // 模板中模块标识
headerText: '登录',
areaCode: '+86', // 默认区号
... ... @@ -173,6 +177,10 @@ const local = {
});
},
login: (req, res, next) => {
// 170306 因为暴力破解密码问题,要求每次都校验验证码
_.set(req.session, 'login.errorCount', 0);
let count = _.get(req.session, 'login.errorCount');
if (count == null) { // eslint-disable-line
... ...
... ... @@ -419,16 +419,19 @@ let setPassword = (req, res, next) => {
RegService.regMobileAes(area, mobile, password, shoppingKey, smsCode, isFromMy).then((result) => {
if (!result.code || result.code !== 200) {
return Promise.reject(result);
return res.send(result);
}
if (!result.data || !result.data.uid) {
return Promise.reject(result);
return res.send(result);
}
resultCopy = result;
return AuthHelper.syncUserSession(result.data.uid, req, res);
}).then(() => {
if (!resultCopy) {
return;
}
// 返回跳转到来源页面
let refer = req.cookies.refer;
... ...
... ... @@ -86,7 +86,7 @@ const newDetail = {
reject();
}
}).then(skn => {
return res.redirect(`/product/${skn}.html${param}`);
return res.redirect(301, `/product/${skn}.html${param}`);
}, () => {
return next();
});
... ...
... ... @@ -142,8 +142,8 @@ router.get('/search/fuzzyDatas', search.fuzzyDatas);
router.get('/search/search', search.search);
// 品类
router.get('/index/index', list.category);
router.get('/list/index', list.category); // 兼容 PC 的链接
router.get('/index/index', rewrite.sortParams, list.category);
router.get('/list/index', rewrite.sortParams, list.category); // 兼容 PC 的链接
// 品牌 | 店铺
router.get('/index/shopAppCookie', list.shopAppCookie);
... ...
... ... @@ -153,11 +153,12 @@ exports.checkTickets = (uid, productSku, buyNumber, useYohoCoin, yohoCoinMode) =
* @param string|null $userAgent 联盟过来用户下单时需要的User-Agent信息
* @param $times
* @param null $activityInfo 套餐信息
* @param isWechat 是否是微信商城
* @return array 接口返回的数据
*/
exports.orderSub = (uid, addressId, cartType, deliveryTime,
deliveryWay, invoices, paymentId, paymentType, remark, couponCode,
yohoCoin, skuList, qhyUnion, userAgent, times, activityInfo, ip) => {
yohoCoin, skuList, qhyUnion, userAgent, times, activityInfo, ip, isWechat) => {
if (!activityInfo) {
activityInfo = null;
}
... ... @@ -226,6 +227,11 @@ exports.orderSub = (uid, addressId, cartType, deliveryTime,
params.qhy_union = qhyUnion;
}
// 是否是微信商城
if (isWechat) {
params.client_type = 'wechat';
}
return api.post('', params, {
headers: {
'X-Forwarded-For': ip || '',
... ...
... ... @@ -72,7 +72,6 @@ module.exports = {
udp: { // send by udp
measurement: 'yohobuy_wap_node_log',
level: 'error', // logger level
host: 'influxdblog.web.yohoops.org', // influxdb host
port: '4444' // influxdb port
},
console: {
... ... @@ -107,7 +106,8 @@ module.exports = {
key: '7e6f3307b64cc87c79c472814b88f7fb',
appSecret: 'ce21ae4a3f93852279175a167e54509b',
notifyUrl: domains.service + 'payment/weixin_notify',
}
},
maxQps: 1200
};
if (isProduction) {
... ...
'use strict';
const limiter = require('../middleware/limiter/index');
module.exports = (req, res, 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, 'wap.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',
'/passport/captcha/get',
'/3party/check/submit'
];
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, (err) => {});
}
});
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);
}
});
};
... ...
... ... @@ -7,7 +7,13 @@
const typeLib = require('../../config/type-lib');
const _ = require('lodash');
const utils = require('../../utils');
const helpers = global.yoho.helpers;
/**
* 解析url规则中的参数
*/
const resolve = (req, res, next) => {
let path,
params = {
... ... @@ -44,6 +50,9 @@ const resolve = (req, res, next) => {
next();
};
/**
* 简介channel参数
*/
const channel = (req, res, next) => {
let channelName;
... ... @@ -79,7 +88,26 @@ const channel = (req, res, next) => {
next();
};
/**
* 参数排序
*/
const sortParams = (req, res, next) => {
let sorts = utils.mapSort(req.query);
let queryKeys = _.keys(req.query);
let index = 0;
let matched = _.map(sorts, (val, key) => {
return key === queryKeys[index++];
});
if (_.every(matched, match => match)) {
return next();
} else {
return res.redirect(helpers.urlFormat('/', sorts, 'list'));
}
};
module.exports = {
resolve,
channel
channel,
sortParams
};
... ...
{
"name": "m-yohobuy-node",
"version": "5.4.25",
"version": "5.4.30",
"private": true,
"description": "A New Yohobuy Project With Express",
"repository": {
... ... @@ -48,7 +48,7 @@
"xml2js": "^0.4.17",
"yoho-express-session": "^2.0.0",
"yoho-node-lib": "^0.2.8",
"yoho-zookeeper": "^1.0.6"
"yoho-zookeeper": "^1.0.8"
},
"devDependencies": {
"autoprefixer": "^6.7.4",
... ...
require('3party/check.page.css');
require('../common');
// 图片验证码
let ImgCheck = require('plugin/img-check');
let imgCheck = new ImgCheck('#js-img-check', {
useREM: {
rootFontSize: 40,
picWidth: 150
}
});
imgCheck.init();
$(function() {
$('.submit').on('click', function() {
$.ajax({
method: 'POST',
url: '/3party/check/submit',
data: {
captcha: $.trim(imgCheck.getResults())
},
success: function(ret) {
if (ret.code === 200) {
window.location.href = decodeURIComponent(window.queryString.refer) || '//m.yohobuy.com';
} else {
imgCheck.refresh();
}
}
});
});
});
... ...
... ... @@ -18,9 +18,9 @@ require('activity/promotion/promotion.page.css');
var share = require('../common/share');
share({
title: 'YOHO!BUY有货邀请同学认证赠好礼!立享【学生专属】4大特权!',
title: '【有货】学生认证看过来!专享特价,任性分期,立即走起→',
link: location.href,
desc: '每邀请1名同学完成认证立赠300有货币!上不封顶!',
desc: '邀请同学认证更有惊喜好礼相送~',
imgUrl: 'http://img11.static.yhbimg.com/taobaocms/2017/01/17/16/0108d724a0ef1f001d3ee6010aa79d960e.png'
});
... ... @@ -303,9 +303,9 @@ $(
var shareUrl = location.href.replace('userUid=', 'oldUid=');
share({
title: 'YOHO!BUY有货邀请同学认证赠好礼!立享【学生专属】4大特权!',
title: '【有货】学生认证看过来!专享特价,任性分期,立即走起→',
link: shareUrl,
desc: '每邀请1名同学完成认证立赠300有货币!上不封顶!',
desc: '邀请同学认证更有惊喜好礼相送~',
imgUrl: 'http://img11.static.yhbimg.com/taobaocms/2017/01/17/16/0108d724a0ef1f001d3ee6010aa79d960e.png'
});
}
... ...
... ... @@ -87,7 +87,9 @@ if (canOpenApp()) {
}, 2000);
if (isiOS) {
window.location.href = appPath;
setTimeout(function() {
window.location.href = appPath;
}, 2000);
} else {
ifr = document.createElement('iframe');
ifr.src = appPath;
... ...
... ... @@ -46,7 +46,7 @@ function switchLoginBtnStatus() {
}
function resetForm() {
$pwd.val('').focus();
// $pwd.val('').focus();
$loginBtn.text('登录').addClass('disable');
}
... ... @@ -123,7 +123,7 @@ $loginBtn.on('touchstart', function() {
type: 'POST',
url: '/passport/login/auth',
data,
success: function(data) {
success: function(data) { //eslint-disable-line
var res,
LOGI_TYPE;
... ...
... ... @@ -101,8 +101,12 @@ function setPassword() {
showErrTip(data.message);
}
},
error: function() {
error: function(data) {
$btnSure.removeClass('disable');
if (data && data.responseJSON && data.responseJSON.message) {
showErrTip(data.message);
}
}
});
}
... ...
@import "layout/img-check";
.check-page {
margin: 20px auto;
width: 700px;
.submit {
width: 100%;
height: 100px;
line-height: 100px;
text-align: center;
font-size: 32px;
color: #fff;
background: #5cb85c;
border-radius: 10px;
}
}
... ...
... ... @@ -312,14 +312,14 @@
id: id
}, resultData => {
// /* 结果返回 */
// if (resultData.length < 1) {
// areaCode.val(id);
// let returnTitle = this.returnTitle();
// area.val(returnTitle);
// this.show = false;
// }
/* 结果返回 */
if (resultData.length < 1) {
areaCode.val(id);
let returnTitle = this.returnTitle();
area.val(returnTitle);
this.show = false;
}
/* 数据绑定 */
switch ((id + '').length) {
... ...
... ... @@ -30,3 +30,19 @@ exports.refererLimit = (referer, blacklist) => { // eslint-disable-line
return result;
};
/**
* 对象字段排序
*/
exports.mapSort = obj => {
if (!obj) {
return {};
}
let data = {};
Object.keys(obj).sort().forEach(k => {
data[k] = obj[k];
});
return data;
};
... ...