Authored by 毕凯

Merge branch 'feature/hotKeywords' into 'master'

Feature/hot keywords



See merge request !35
... ... @@ -44,6 +44,7 @@ app.use(convert(body({
formLimit: '10mb',
textLimit: '10mb',
formidable: {
keepExtensions: true,
maxFieldsSize: 10 * 1024 * 1024
}
})));
... ...
/**
*
* @author: yyq<yanqing.yang@yoho.cn>
* @date: 18/5/10
*/
const _ = require('lodash');
const moment = require('moment');
const Router = require('koa-router');
const r = new Router();
const Mysql = require('../../../lib/mysql-promise');
const pager = require('../utils/pager');
const config = require('../../../config/config');
const singleBrandKeyPre = config.singleBrandKeyPre;
const singleSortKeyPre = config.singleSortKeyPre;
//通过小分类同步中分类和大分类
const getMsort = async(ctx) => {
let redis = ctx.redis;
return redis.getAsync(`global:yoho:sorts`).then((sorts) => {
let sData = {};
sorts = JSON.parse(sorts) || [];
_.forEach(_.get(sorts, 'data.sort'), (msort) => {
_.forEach(_.get(msort, 'sub'), (misort) => {
_.forEach(_.get(misort, 'sub'), (sort) => {
if (!sData[sort.sort_id]) {
sData[sort.sort_id] = {
msort: msort.sort_id,
msort_name: msort.sort_name,
misort: misort.sort_id,
misort_name: misort.sort_name,
sort_id: sort.sort_id,
sort_name: sort.sort_name,
};
}
})
})
})
return sData;
});
};
r.get('/list', async(ctx) => {
let resData = {};
let q = ctx.request.query || {};
let query = q.query || '';
let page = parseInt(`0${q.page}`, 10) || 1;
let limit = parseInt(`0${q.limit}`, 10) || 10;
let mysql = new Mysql();
let total = 0;
let typeList = [
{
type: 'keyword',
name: '关键词'
},
{
type: 'wordroot',
name: '词根ID'
},
{
type: 'goodnum',
name: '商品数'
}
];
let type = q.type || typeList[0].type;
let typeName = _.result(_.find(typeList, {'type': type}), 'name') || typeList[0].name;
let wheres = '';
let conditions = [];
if (q.hot) {
wheres += ' AND is_hot = 1';
}
switch (query && type) {
case 'keyword':
wheres += ` AND keyword like '%%${query}%'`;
break;
case 'wordroot':
wheres += ' AND root_id = ?'
conditions.push(query);
break;
case 'brand':
wheres += ' AND brand_id = ?'
conditions.push(query);
break;
case 'sort':
wheres += ' AND sort_id = ?'
conditions.push(query);
break;
case 'goodnum':
wheres += ' AND yoho_goods_num >= ?'
conditions.push(query);
break;
}
let d = await mysql.query(`SELECT COUNT(*) as total FROM seo_keywords WHERE status = 1 ${wheres}`, conditions);
resData.total = d[0] && d[0].total || 0;
conditions.push((page - 1) * limit, limit);
d = await mysql.query(`SELECT * FROM seo_keywords WHERE status = 1 ${wheres} ORDER BY id DESC limit ?, ?`, conditions);
let sortIds = [];
sortIds = await getMsort(ctx);
resData.keywords = _.map(d, (elem) => {
const _sortId = sortIds[elem.sort_id];
return Object.assign({}, elem, {
sortName: _sortId && _sortId.sort_name,
misortName: _sortId && _sortId.misort_name,
msortName: _sortId && _sortId.msort_name,
is_push: elem.is_push ? '是' : '否',
add_time: elem.add_time && moment(elem.add_time * 1000).format('YYYY-MM-DD HH:mm'),
});
});
resData.typeList = typeList;
resData.typeName = typeName;
resData.type = type;
resData.query = query;
return ctx.body = {
code: 200,
message: 'success',
data: resData
};
});
r.post('/save', async (ctx) => {
let mysql = new Mysql();
let id = ctx.request.body.id || 0;
let keyword = ctx.request.body.keywords;
let brand = ctx.request.body.brand || 0;
let msort = ctx.request.body.msort || 0;
let misort = ctx.request.body.misort || 0;
let sort = ctx.request.body.sort || 0;
let describe = ctx.request.body.describe || '';
let goods_img = ctx.request.body.goodsImg || '';
let is_hot = ctx.request.body.isHot || 0;
let addTime = Date.parse(new Date())/1000;
if (id) {
return mysql.query(`UPDATE seo_keywords SET \`describe\` = '${describe}', goods_img = '${goods_img}' WHERE id = ${id}`).then(d => {
return ctx.body = {
code: 200,
message: 'success',
data: d
};
});
}
let result = {code: 400, message: ''};
if (!keyword) {
result.message = '关键词必填';
return ctx.response.body = result;
}
let select = await mysql.query(`select id from seo_keywords where keyword='${keyword}' limit 1`);
if (_.get(select, '[0].id', 0) > 0) {
result.message = '关键词已经存在';
return ctx.response.body = result;
}
await mysql.query(`insert into seo_keywords (keyword, brand_id, msort, misort, sort_id, add_time, \`describe\`, goods_img, is_hot) values ('${keyword}', ${brand}, ${msort}, ${misort}, ${sort}, ${addTime}, '${describe}', '${goods_img}', ${is_hot})`);
result.code = 200;
return ctx.response.body = result;
})
module.exports = r;
... ...
... ... @@ -295,9 +295,13 @@ r.get('/expand', async(ctx) => {
let wheres = '';
let conditions = [];
if (query && query.hot) {
wheres += ' AND is_hot = 1';
}
switch (query && type) {
case 'keyword':
wheres = ` AND keyword like '%%${query}%'`;
wheres += ` AND keyword like '%%${query}%'`;
break;
case 'wordroot':
wheres += ' AND root_id = ?'
... ... @@ -368,6 +372,8 @@ r.post('/expand/del', async(ctx) => {
let q = ctx.request.body;
let ids = q.ids && JSON.parse(q.ids) || [];
ids = _.concat([], ids);
let len = ids.length;
let marks = [];
... ... @@ -553,17 +559,20 @@ r.get('/add', async (ctx) => {
r.post('/add', async (ctx) => {
let mysql = new Mysql();
let keyword = ctx.request.body.keywords || '';
let keyword = ctx.request.body.keywords;
let brand = ctx.request.body.brand || 0;
let msort = ctx.request.body.msort || 0;
let misort = ctx.request.body.misort || 0;
let sort = ctx.request.body.sort || 0;
let describe = ctx.request.body.describe || '';
let goods_img = ctx.request.body.goodsImg || '';
let is_hot = ctx.request.body.isHot || 0;
let addTime = Date.parse(new Date())/1000;
let result = {code: 400, message: ''};
if (_.isEmpty(keyword)) {
selectSql.message = '关键词必填';
if (!keyword) {
result.message = '关键词必填';
return ctx.response.body = result;
}
... ... @@ -574,7 +583,7 @@ r.post('/add', async (ctx) => {
return ctx.response.body = result;
}
await mysql.query(`insert into seo_keywords (keyword, brand_id, msort, misort, sort_id, add_time) values ('${keyword}', ${brand}, ${msort}, ${misort}, ${sort}, ${addTime})`);
await mysql.query(`insert into seo_keywords (keyword, brand_id, msort, misort, sort_id, add_time, \`describe\`, goods_img, is_hot) values ('${keyword}', ${brand}, ${msort}, ${misort}, ${sort}, ${addTime}, '${describe}', '${goods_img}', ${is_hot})`);
result.code = 200;
return ctx.response.body = result;
... ...
'use strict';
const _ = require('lodash');
const Router = require('koa-router');
const rp = require('request-promise');
const multiparty = require('koa2-multiparty');
const fs = require('fs');
const r = new Router();
const bucket = 'goodsimg';
const _getUploadImgAbsoluteUrl = (url, bucket) => {
if (!url) {
return null;
}
let urlArr = url.split('/'),
stag = urlArr[urlArr.length - 1].substr(0, 2),
domain = `static.yhbimg.com/${bucket}`;
url = domain + url;
if (stag === '01') {
return `//img11.${url}`;
} else if (stag === '03') {
return `//flv01.${url}`;
} else {
return `//img12.${url}`;
}
};
const upload = {
async image(ctx) {
let files = _.get(ctx, 'request.body._files.file');
let errTip;
if (!_.isArray(files)) {
files = [files];
}
const renderFiles = [];
files.forEach(file => {
let types = file.type.split('/');
if (!types || types[0] !== 'image') {
errTip = '上传文件格式不正确!';
}
if (file.size > 10 * 1024 * 1024) {
errTip = '上传文件尺寸太大!';
}
renderFiles.push(fs.createReadStream(file.path));
renderFiles.push(file.name);
});
await rp({
method: 'post',
url: 'http://upload.static.yohobuy.com',
formData: {
fileData: renderFiles,
project: bucket
},
json: true
}).then(function(result) {
if (result && result.code === 200) {
result.data = result.data || {};
result.data.images = _.map(_.get(result, 'data.imagesList'), (it) => {
return _getUploadImgAbsoluteUrl(it, bucket);
});
}
ctx.response.body = result;
});
}
};
r.post('/image', upload.image);
module.exports = r;
... ...
... ... @@ -18,6 +18,7 @@ const apiCache = require('./actions/api_cache');
const degrade = require('./actions/degrade');
const deploy = require('./actions/deploy');
const keywords = require('./actions/keywords');
const hotKeywords = require('./actions/hot-keywords');
const api = require('./actions/api');
const abuseProtection = require('./actions/abuse_protection');
const crawler = require('./actions/crawler');
... ... @@ -30,6 +31,7 @@ const file = require('./actions/file');
const riskManagement = require('./actions/risk_management');
const logs = require('./actions/logs');
const spa = require('./actions/spa');
const upload = require('./actions/upload');
module.exports = function(app) {
... ... @@ -63,6 +65,7 @@ module.exports = function(app) {
base.use('/degrade', degrade.routes(), degrade.allowedMethods());
base.use('/deploys', deploy.routes(), deploy.allowedMethods());
base.use('/keywords', keywords.routes(), keywords.allowedMethods());
base.use('/hot-keywords', hotKeywords.routes(), hotKeywords.allowedMethods());
base.use('/profile', profile.routes(), profile.allowedMethods());
const white = crawler('/crawler/ip_whitelists', '/crawler/ua_whitelists', '白名单', false);
... ... @@ -78,6 +81,7 @@ module.exports = function(app) {
// base.use('', index.routes(), index.allowedMethods());
base.use('/risk_management', riskManagement.routes(), riskManagement.allowedMethods());
base.use('/logs', logs.routes(), logs.allowedMethods());
base.use('/upload', upload.routes(), upload.allowedMethods());
base.use('', spa.routes(), spa.allowedMethods());
app.use(base.routes(), base.allowedMethods());
... ...
... ... @@ -6,7 +6,7 @@
"scripts": {
"test": "./node_modules/.bin/ava",
"dev": "nodemon -e js --ignore public/ --ignore packages/ --harmony app.js",
"static": "ada --port 9001 --cwd public --hotVue --noRem",
"static": "ada --port 9001 --cwd public --hotReact --noRem",
"build": "ada build --port 9001 --cwd public --noRem",
"start": "node --harmony app.js"
},
... ...
No preview for this file type
import React from 'react'
import { Table, Divider, Modal, Button, Input, Cascader, Upload, Icon, message } from 'antd'
function HotKeywords() {
return <div>
HotKeywords
import HotApi from '../../services/seo/hot-keywords'
const { TextArea } = Input;
const titleObj = {
id: '序号',
keyword: '关键词',
msortName: '大品类',
sortName: '小品类',
yoho_goods_num: '商品数',
is_push: '是否推送',
add_time: '添加时间'
};
const columns = [];
Object.keys(titleObj).forEach(key => {
columns.push({
title: titleObj[key],
dataIndex: key,
key: key,
});
});
let globalSort = [];
function ActionButton(props) {
function editClick() {
props.keyword.callbackFn('edit', props.keyword);
}
function deleteClick() {
props.keyword.callbackFn('delete', props.keyword);
}
return (
<span>
<a href="javascript:;" onClick={editClick}>编辑</a>
<Divider type="vertical" />
<a href="javascript:;" onClick={deleteClick}>删除</a>
</span>
);
}
class OptionModal extends React.Component {
constructor(props) {
super(props);
this.props = props;
this.state = {
sorts: globalSort || []
};
this.renderData = Object.assign({}, props.record || {});
this.hotApi = new HotApi();
this.loadHotList = props.loadHotList;
this.hideOptionModal = props.hideOptionModal;
this.handleKeywordChange = this.handleKeywordChange.bind(this);
this.handleDescChange = this.handleDescChange.bind(this);
this.handleSortChange = this.handleSortChange.bind(this);
this.handleImageChange = this.handleImageChange.bind(this);
this.loadSortData = this.loadSortData.bind(this);
switch (this.renderData.type) {
case 'edit':
this.modalTitle = '编辑关键词';
this.handleOk = this.handleSave.bind(this);
break;
case 'delete':
this.modalTitle = '删除关键词';
this.handleOk = this.handleDelete.bind(this);
break;
default:
this.modalTitle = '新建关键词';
this.handleOk = this.handleSave.bind(this);
break;
}
if (!globalSort.length) {
this.loadSortDataAsync().then(result => {
this.setState({sorts: result});
globalSort = result;
});
}
}
handleDelete() {
return this.hotApi.delHotKeywords([this.renderData.id]).then(result => {
if (result.code === 200) {
this.loadHotList && this.loadHotList();
this.hideOptionModal && this.hideOptionModal();
} else {
message.error(result.message);
}
});
}
handleSave() {
return this.hotApi.saveHotKeywords(this.renderData).then(result => {
if (result.code === 200) {
this.loadHotList && this.loadHotList();
this.hideOptionModal && this.hideOptionModal();
} else {
message.error(result.message);
}
});
}
handleKeywordChange(e) {
this.renderData.keyword = e.target.value;
}
handleDescChange(e) {
this.renderData.describe = e.target.value;
}
handleSortChange(select) {
this.renderData.msort = select[0];
this.renderData.misort = select[1];
this.renderData.sort_id = select[2];
}
handleImageChange(info) {
const status = info.file.status;
if (status === 'uploading') {
this.setState({ loading: true });
return;
} else if (status === 'done') {
const result = info.file.response;
let state = { loading: false };
if (result.code === 200) {
this.renderData.goods_img = result.data.images[0];
}
this.setState(state);
}
}
beforeUpload(file) {
const isImage = file.type.indexOf('image') > -1;
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isImage) {
message.error('请上传图片!');
} else if (!isLt2M) {
message.error('图片大小需小于 2MB!');
}
return isImage && isLt2M;
}
loadSortData(selectedOptions) {
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
this.loadSortDataAsync(targetOption.value).then(result => {
targetOption.loading = false;
targetOption.children = result;
globalSort = this.state.sorts;
this.setState({sorts: this.state.sorts});
});
}
loadSortDataAsync(id) {
return this.hotApi.getSubSorts(id || 0).then(result => {
result.map(val => {
if (id) {
val.value = val.sort_id;
val.label = val.sort_name;
val.isLeaf = false;
val.children = (val.sub || []).map(sval => {
sval.value = sval.sort_id;
sval.label = sval.sort_name;
sval.isLeaf = true;
return sval;
})
} else {
val.value = val.id;
val.label = val.sortName;
val.isLeaf = false;
}
return val;
});
return result;
});
}
uploadButton(loading) {
return (
<div>
<Icon type={loading ? 'loading' : 'plus'} />
<div className="ant-upload-text">Upload</div>
</div>);
}
render() {
const record = this.renderData;
let title = '';
if (record.sort_id || record.msort || record.misort) {
let arr = [];
record.msortName && arr.push(record.msortName);
record.misortName && arr.push(record.misortName);
record.sortName && arr.push(record.sortName);
record.sortShowName = arr.join('/');
}
if (record && record.type === 'delete') {
return (
<Modal title={this.modalTitle} visible={true} onOk={this.handleOk} onCancel={this.hideOptionModal}>
<p>确认删除该关键词?</p>
</Modal>
)
} else {
const imageUrl = '';
return (
<Modal title={this.modalTitle} visible={true} onOk={this.handleOk} onCancel={this.hideOptionModal}>
<div style={{ paddingBottom: 10 }}>
<span>关键词:</span>
<Input onChange={this.handleKeywordChange} defaultValue={record.keyword} readOnly={record.id > 0}/>
</div>
<div style={{ paddingBottom: 10 }}>
<span>描述:</span>
<TextArea rows={4} onChange={this.handleDescChange} defaultValue={record.describe}/>
</div>
<div style={{ paddingBottom: 10 }}>
<span>封面图:</span>
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
action="/upload/image"
name="file"
beforeUpload={this.beforeUpload}
onChange={this.handleImageChange}>
{record.goods_img ? <img src={record.goods_img} style={{maxWidth: 200}} /> : this.uploadButton(this.state.loading)}
</Upload>
</div>
<div style={{ paddingBottom: 10 }}>
<span>品类:</span>
{record.sortShowName ? <Input defaultValue={record.sortShowName} readOnly={record.id > 0}/> : <Cascader
options={this.state.sorts}
loadData={this.loadSortData}
onChange={this.handleSortChange}
style={{ width: '100%' }}
placeholder="" />}
</div>
</Modal>
)
}
}
}
columns.push({
title: '操作',
key: 'action',
render(keyword) {
return <ActionButton keyword={keyword}/>
}
});
class HotKeywords extends React.Component {
constructor(props) {
super(props);
this.state = {
showModal: false,
dataSource: [],
columns: columns,
selectedRowKeys: [],
pagination: {
current: 1,
total: 1,
pageSize: 10
}
};
this.hotApi = new HotApi();
this.loadHotList = this.loadHotList.bind(this);
this.handleTableChange = this.handleTableChange.bind(this);
this.deleteTableRow = this.deleteTableRow.bind(this);
this.hideOptionModal = this.hideOptionModal.bind(this);
this.addTableRow = this.addTableRow.bind(this);
this.loadHotList();
}
loadHotList(params = {}) {
const { current, pageSize } = this.state.pagination;
params.page = params.page || current || 1;
params.limit = params.limit || pageSize || 10;
this.hotApi.getHotList(params).then(result => {
if (result.code === 200 && result.data) {
const pagination = this.state.pagination;
const { keywords, total } = result.data;
pagination.total = total;
this.setState({
dataSource: keywords,
pagination: pagination
});
}
});
}
onSelectChange(selectedRowKeys) {
this.setState({selectedRowKeys});
}
handleTableChange(pagination) {
this.setState({pagination});
this.loadHotList({page: pagination.current});
}
deleteTableRow() {
this.deleteKeywordsAsync(this.state.selectedRowKeys);
}
addTableRow() {
this.showOptionModal('add');
}
sendToBaidu() {
}
deleteKeywordsAsync(ids) {
this.hotApi.delHotKeywords(ids).then(result => {
result.code === 200 && this.loadHotList();
});
}
showOptionModal(type, record) {
// const modelRecord = {...record};
const modelRecord = record || {};
modelRecord.type = type;
this.modelRecord = modelRecord;
this.setState({showModal: true});
}
hideOptionModal() {
this.modelRecord = null;
this.setState({showModal: false});
}
optionModal(showModal) {
if (showModal) {
return <OptionModal hideOptionModal={this.hideOptionModal} record={this.modelRecord} loadHotList={this.loadHotList}/>
} else {
return <div></div>;
}
}
render() {
const { showModal, dataSource, columns, selectedRowKeys, pagination, loading } = this.state;
const hasSelected = selectedRowKeys.length > 0;
const rowSelection = {
selectedRowKeys,
onChange: this.onSelectChange.bind(this)
};
const rowKey = record => {
record.callbackFn = this.showOptionModal.bind(this);
return record.id;
}
return (
<div>
<div style={{ paddingBottom: 10 }}>
<Button type="primary" onClick={this.deleteTableRow} disabled={!hasSelected} loading={loading}>删除</Button>
<Button type="primary" style={{ marginLeft: 10 }} onClick={this.addTableRow}>添加</Button>
{this.optionModal(showModal)}
</div>
<Table rowSelection={rowSelection} dataSource={dataSource} columns={columns}
rowKey={rowKey} pagination={pagination} onChange={this.handleTableChange}/>
</div>
);
}
}
export default HotKeywords
... ...
import Service from '../service';
export default class extends Service {
getHotList(params) {
params = params || {};
params.hot = 1;
return this.get('/hot-keywords/list', {params});
}
delHotKeywords(ids) {
return this.post('/keywords/expand/del', {ids});
}
saveHotKeywords(info) {
let params = {
keywords: info.keyword,
msort: info.msort,
misort: info.misort,
sort: info.sort_id,
describe: info.describe,
goodsImg: info.goods_img,
isHot: 1
};
if (info.id) {
params.id = info.id;
}
return this.post('/hot-keywords/save', params);
}
getSubSorts(sortId) {
return this.post('/seo/rootwords/getsubsorts', {sortId})
}
}
... ...