Authored by shuaiguo

init project

# Created by https://www.gitignore.io/api/node,webstorm,netbeans,sublimetext,vim
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
### WebStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### WebStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
### NetBeans ###
nbproject/private/
public/build/bundle
nbbuild/
dist/
nbdist/
nbactions.xml
.nb-gradle/
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
### Vim ###
# swap
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags
### YOHO ###
dist
.eslintcache
*.log.*
nbproject/*
.DS_Store
.devhost
.happypack/
bundle/
db/
schedule/python-jobs/files/
.stylelintcache
manifest.json
... ...
有货UFO及毒销售数据统计
... ...
const { createServer } = require('./server');
const { createSchedule } = require('./schedule');
createServer();
createSchedule();
... ...
#root {
height: 100%;
}
.yoho-layout {
height: 100%;
}
.yoho-header {
color: #fff;
font-size: 30px;
font-weight: 600;
letter-spacing: 2px;
}
.yoho-login-page {
background: #657180;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.yoho-login-page .form-block {
width: 340px;
padding: 40px 30px 20px;
position: absolute;
left: 50%;
top: 25%;
margin-left: -170px;
background-color: #fff;
}
.yoho-login-page .form-block .ant-form-item {
margin-bottom: 28px;
}
.yoho-login-page .form-block .ant-form-item:last-child {
margin-bottom: 0;
}
... ...
import React from 'react';
import { findIndex } from 'lodash';
import { Layout, Menu, Breadcrumb, Icon } from 'antd';
import { BrowserRouter, StaticRouter, Route, NavLink } from 'react-router-dom';
import LoginPage from './login';
import router from '../../router';
import 'antd/dist/antd.css'
import './layout.css'
const { SubMenu } = Menu;
const { Header, Content, Sider } = Layout;
class Page extends React.Component {
constructor() {
super();
this.menuList = [
{
name: '数据统计',
icon: 'deployment-unit',
sub: [
{
path: '/sales',
name: '销售额统计'
},
{
path: '/top300/stock',
name: 'top300库存'
}
]
},
{
name: '系统设置',
icon: 'setting',
sub: [
{
path: '/setting/account',
name: '账户管理'
}
]
}
];
}
renderSubMenu(list = [], preKey) {
return list.map((val, index) => {
return <Menu.Item key={preKey + '_' + index}><NavLink to={val.path}>{val.name}</NavLink></Menu.Item>;
});
}
renderMenu(menus, path) {
let openKey = [];
let selectKey = [];
let breadcrumb = [{path: '/', name: '首页'}];
const list = menus.map((val, index) => {
let key = 'sub' + index;
if (!selectKey.length) {
let active = findIndex(val.sub, (o) => {
return o.path === path;
});
if (active > -1) {
openKey.push(key);
selectKey.push(key + '_' + active);
breadcrumb.push(val.sub[active]);
}
}
return (
<SubMenu
key={key}
title={
<span>
<Icon type={val.icon} />
{val.name}
</span>
}
>
{this.renderSubMenu(val.sub, key)}
</SubMenu>
)
})
return {
openKey,
selectKey,
breadcrumb,
list
}
}
renderBreadcrumb(breadcrumb) {
return (
<Breadcrumb style={{ margin: '16px 0' }}>
{breadcrumb.map((val, index) => {
return <Breadcrumb.Item key={index}>{val.name}</Breadcrumb.Item>;
})}
</Breadcrumb>
)
}
render() {
let { path, client } = this.props.context || {};
const Router = client ? BrowserRouter : StaticRouter;
if (path === '/login.html') {
return <LoginPage></LoginPage>;
}
const subMenu = this.renderMenu(this.menuList, path);
return (
<Layout className="yoho-layout">
<Header className="yoho-header">
YOHO!UFO
</Header>
<Layout className="ant-layout-has-sider">
<Router>
<Sider width={200} style={{ background: '#fff' }}>
<Menu
mode="inline"
defaultSelectedKeys={subMenu.selectKey}
defaultOpenKeys={subMenu.openKey}
style={{ height: '100%', borderRight: 0 }}
>
{subMenu.list}
</Menu>
</Sider>
<Layout style={{ padding: '0 24px 24px' }}>
{this.renderBreadcrumb(subMenu.breadcrumb)}
<Content style={{
background: '#fff',
padding: 24,
margin: 0,
minHeight: 'auto',
}} >
{router.map((route, i) => (
<Route key={i} path={route.path} component={route.component}
exact={route.exact} />
))}
</Content>
</Layout>
</Router>
</Layout>
</Layout>
)
}
}
export default Page;
... ...
import React from 'react';
import { Form, Icon, Input, Button, message} from 'antd';
import axios from '../../libs/axios';
class Login extends React.Component {
getQueryVariable(key) {
const query = window.location.search.substring(1);
const vars = query.split("&");
for (let i = 0; i < vars.length; i++) {
let pair = vars[i].split("=");
if (pair[0] == key) {
return decodeURIComponent(pair[1]);
}
}
return '';
}
handleSubmit(e) {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
return axios.post('/api/passport/login', values).then(res => {
if (res.code === 200) {
if (res.data && res.data.access) {
location.href = this.getQueryVariable('refer') || '/';
} else {
message.error(res.message || '登录失败');
}
}
});
}
});
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<div className="yoho-login-page">
<div className="form-block">
<Form onSubmit={this.handleSubmit.bind(this)}>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true, message: '请输入用户名!' }],
})(
<Input
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="用户名"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('password', {
rules: [{ required: true, message: '请输入密码!' }],
})(
<Input
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="密码"
/>,
)}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="login-form-button">登录</Button>
</Form.Item>
</Form>
</div>
</div>
)
}
}
export default Form.create()(Login);
... ...
import React from 'react';
import { render } from 'react-dom';
import App from './index';
const getContext = () => {
let { pathname } = window.location;
return {
path: pathname,
client: true
};
}
render(<App context={getContext()} />, document.getElementById('root'));
... ...
import React from 'react';
import App from './index';
export default (context) => (
<App context={context} />
);
... ...
import React from 'react';
import Layout from './components/layouts/layout';
export default class App extends React.Component {
render() {
return <Layout context={this.props.context} />;
}
};
... ...
import axios from 'axios';
import { message } from 'antd';
class Api {
constructor() {
this.api = axios.create({
timeout: 5000,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
}
handelResult(res) {
let data = res.data || {};
if (data.code !== 200) {
message.error(data.message || '网络异常,请稍后重试');
}
return data;
}
get(...args) {
return this.api.get(...args).then(this.handelResult);
}
post(...args) {
return this.api.post(...args).then(this.handelResult);
}
}
export default new Api();
... ...
import React from 'react';
import { Input, Table, message } from 'antd';
import axios from '../../libs/axios';
import dayjs from 'dayjs';
const { Search } = Input;
class Account extends React.Component {
constructor() {
super();
this.state = {
userList: []
};
this.columns = [
{
title: '账号',
dataIndex: 'name',
key: 'name',
render: (name) => (
<span>{name}@yoho.cn</span>
)
},
{
title: '添加时间',
dataIndex: 'time',
key: 'time',
render: (time) => (
<span>{dayjs(time * 1000).format('YYYY-MM-DD HH:mm:ss')}</span>
)
},
{
title: '操作',
key: 'action',
render: (record) => (
<a onClick={() => this.deleteAccount(record)}>删除</a>
)
},
];
}
componentDidMount() {
return axios.get('/api/passport/account/list').then(res => {
if (res.code !== 200) {
return;
}
if (res.data && res.data.list) {
this.setState({
userList: res.data.list
});
}
});
}
addAccount(user) {
if (!user) {
return;
}
return axios.post('/api/passport/account/add', {
account: user,
needList: true
}).then(res => {
if (res.code !== 200) {
return;
}
if (res.data && res.data.list) {
this.setState({
userList: res.data.list
});
}
if (this.refs.addInput) {
this.refs.addInput.input.handleReset();
}
});
}
deleteAccount(record) {
return axios.post('/api/passport/account/delete', {
id: record._id
}).then(res => {
if (res.code !== 200) {
return;
}
let userList = [];
this.state.userList.forEach(val => {
if (val._id !== record._id) {
userList.push(val);
}
});
this.setState({ userList });
});
}
render() {
return (
<div>
<Search
ref="addInput"
placeholder="请输入OA账号名,多个以逗号分割"
enterButton="添加"
allowClear
onSearch={this.addAccount.bind(this)}
/>
<Table
columns={this.columns}
dataSource={this.state.userList}
rowKey="_id"
size="small"
pagination={false}
bordered
style={{ marginTop: '20px' }}
/>
</div>
)
}
}
export default Account;
... ...
.main-content {
position: relative;
}
.main-title {
font-size: 16px;
font-weight: bold;
}
.custom-wrap {
margin-bottom: 40px;
}
.custom-item {
min-width: 150px;
height: 50px;
position: relative;
border-left: 5px solid #bbb;
margin-right: 40px;
margin-bottom: 20px;
padding-left: 10px;
display: inline-block;
}
.custom-item-name {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
.custom-item-value {
color: #262626;
font-size: 22px;
}
.stats-chart-option {
margin-bottom: 20px;
}
.chart-option {
position: absolute;
right: 0;
margin-top: 10px;
z-index: 10;
}
.chart-title {
font-size: 14px;
line-height: 1.5;
text-align: center;
margin: 20px 0 0px;
padding-left: 10px;
}
.pad-top {
padding-top: 30px;
}
... ...
import React from 'react';
import axios from '../../libs/axios';
import DoubleAxes from './sales/double-axes';
import { Select } from 'antd';
import './sales.css'
const { Option } = Select;
const formatter = o => {
o = `${o}`.split('.');
o[0] = o[0].replace(/(\d)(?=(?:\d{3})+$)/g, '$1,');
return o.join('.');
};
function handleShowLabel(list) {
let len = list.length;
if (len) {
const endSc = len > 10 ? 0 : 2;
const space = len > 10 ? 4 : 2;
for (let i = len; i > 0; i--) {
let sc = len - i;
if (sc < endSc) {
list[i - 1].label = true;
} else {
list[i - 1].label = !(sc % space);
}
}
}
return list;
}
function Custom(props) {
return (
<div className="custom-item" style={{ 'borderLeftColor': props.color}}>
<div className="custom-item-name">{props.name}</div>
<div className="custom-item-value">{formatter(props.value)}</div>
</div>
);
}
class Sales extends React.Component {
constructor() {
super();
this._thisMounthDays = new Date().getDate() - 1;
this.state = {
salesData: [],
orderData: [],
yesterday: []
};
}
componentDidMount() {
let now = Date.parse(new Date()) / 1000;
this.getSalesFromServer();
this.getSalesFromServer({
start: now - 2 * 24 * 60 * 60,
end: now
}, res => {
let info = res.data || [];
if (!info.length) {
return;
}
let yesterdayData = info[info.length - 1];
this.setState({
yesterday: [
{
name: '毒支付金额',
value: (+yesterdayData.duSalesAmount).toFixed() || '',
color: '#2FC25B'
},
{
name: '毒支付订单数',
value: yesterdayData.duOrderCount,
color: '#2FC25B'
},
{
name: 'UFO支付金额',
value: (+yesterdayData.ufoSalesAmount).toFixed() || '',
color: '#1890FF'
},
{
name: 'UFO支付订单数',
value: yesterdayData.ufoOrderCount,
color: '#1890FF'
}
]
});
});
}
getSalesFromServer(data, callback) {
let { start, end } = data || {};
if (!start) {
end = Date.parse(new Date()) / 1000;
start = end - 4 * 24 * 60 * 60;
}
return axios.get(`/api/ufo_du/sales?startTime=${start}&endTime=${end}`).then(res => {
if (res.code !== 200) {
return;
}
if (callback) {
return callback(res);
}
let info = res.data || [];
let salesData = [];
let orderData = [];
info.forEach(val => {
salesData.push({
day: val.day,
du: (+val.duSalesAmount).toFixed() * 1,
ufo: (+val.ufoSalesAmount).toFixed() * 1
});
orderData.push({
day: val.day,
du: val.duOrderCount,
ufo: val.ufoOrderCount
});
});
handleShowLabel(salesData);
handleShowLabel(orderData);
this.setState({ salesData, orderData });
});
}
handleChange(day = 3) {
let end = Date.parse(new Date()) / 1000;
let start = end - (+day + 1) * 24 * 60 * 60;
this.getSalesFromServer({start, end});
}
renderCustom(custom) {
return (
<div className="custom-wrap">
{custom.map((val, index) => {
return <Custom key={index} {...val}></Custom>
})}
</div>
);
}
render() {
const doubleAxesOption = {
height: 400,
padding: [60, 100, 70, 100],
scale: {
ufo: {
min: 0,
ticks: [5000000, 10000000, 15000000, 20000000, 25000000]
},
du: {
min: 0,
ticks: [50000000, 100000000, 150000000, 200000000, 250000000]
}
},
axisX: 'day',
axisY: 'ufo',
axisY2: 'du'
};
const orderDoubleAxesOption = { ...doubleAxesOption};
orderDoubleAxesOption.scale = {
ufo: {
min: 0,
ticks: [4000, 8000, 12000, 16000, 20000]
},
du: {
min: 0,
ticks: [40000, 80000, 120000, 160000, 200000]
}
};
return (
<div className="main-content">
<p className="main-title">昨日销售数据</p>
{this.renderCustom(this.state.yesterday)}
<p className="main-title">销售统计图</p>
<div className="stats-chart-option">
<Select defaultValue="3" style={{ width: '50%' }} onChange={this.handleChange.bind(this)}>
<Option value="3">最近3</Option>
<Option value="7">最近1</Option>
<Option value="30">最近1个月</Option>
<Option value={this._thisMounthDays}>当月</Option>
</Select>
</div>
<DoubleAxes options={doubleAxesOption} data={this.state.salesData} title={'销售额'} />
<p className="pad-top"></p>
<DoubleAxes options={orderDoubleAxesOption} data={this.state.orderData} title={'订单数'} />
</div>
);
}
}
export default Sales;
... ...
import React from 'react';
class ChartLine extends React.Component {
constructor() {
super();
this.state = {
clientRender: false
};
}
componentDidMount() {
import('bizcharts').then(({ Chart, Geom, Axis, Tooltip, Legend }) => {
this.Chart = Chart;
this.Geom = Geom;
this.Axis = Axis;
this.Tooltip = Tooltip;
this.Legend = Legend;
this.setState({
clientRender: true
});
});
}
render() {
if (!this.state.clientRender) {
return <div></div>;
}
const { Chart, Geom, Axis, Tooltip, Legend } = this;
const { height, padding, scale, axisX, axisY, geomColor, formatter, tooltip} = this.props.options || {};
const position = `${axisX}*${axisY}`;
const formatterFn = o => o;
return (
<Chart height={height} data={this.props.data} scale={scale} padding={padding} forceFit>
<Legend />
<Axis name={axisX} />
<Axis
name={axisY}
label={{
formatter: formatter || formatterFn
}}
/>
<Tooltip crosshairs={{ type: 'y' }} />
<Geom type="line" position={position} size={2} color={geomColor} tooltip={tooltip} />
<Geom
type="point"
position={position}
size={4}
shape={'circle'}
color={geomColor}
style={{
stroke: '#fff',
lineWidth: 1,
}}
/>
</Chart>
)
}
}
export default ChartLine;
... ...
import React from 'react';
import { Button } from 'antd';
const formatter = o => {
o = `${o}`.split('.');
o[0] = o[0].replace(/(\d)(?=(?:\d{3})+$)/g, '$1,');
return o.join('.');
};
const formatLabel = (val, isShow) => {
return isShow ? formatter(val) : '';
}
class DoubleAxes extends React.Component {
constructor() {
super();
this.state = {
clientRender: false
};
}
componentDidMount() {
import('bizcharts').then(({ Chart, Geom, Guide, Axis, Tooltip, Legend, Label }) => {
this.Chart = Chart;
this.Geom = Geom;
this.Guide = Guide;
this.Axis = Axis;
this.Tooltip = Tooltip;
this.Legend = Legend;
this.Label = Label;
this.setState({
clientRender: true
});
});
}
downloadChart() {
console.log(this);
this.chartIns && this.chartIns.downloadImage();
}
render() {
if (!this.state.clientRender) {
return <div></div>;
}
const { Chart, Geom, Guide, Axis, Tooltip, Legend, Label } = this;
const { Text } = Guide;
const { height, padding, scale, axisX, axisY, axisY2 } = this.props.options || {};
const position = axisX + '*' + axisY;
const position2 = axisX + '*' + axisY2;
return (
<div>
<div className="chart-option">
<Button shape="round" icon="download" size="small" onClick={this.downloadChart.bind(this)}>下载</Button>
</div>
<Chart
height={height}
scale={scale}
forceFit
data={this.props.data}
padding={padding}
onGetG2Instance={chartIns => {
this.chartIns = chartIns;
}}
background={{
fill: '#fff'
}}
>
<Guide>
<Text
top={true}
position={['50%', '-12%']}
content={this.props.title || ''}
style={{
fill: '#444',
fontSize: '14',
textAlign: 'center'
}}
/>
</Guide>
<Legend />
<Axis
name={axisY}
label={{ formatter }}
/>
<Axis
name={axisY2}
position="right"
grid={null}
label={{ formatter }}
/>
<Tooltip />
<Geom type="path" position={position} size={2} color="#1890ff">
<Label
content={[
axisY + '*label',
formatLabel
]}
position="bottom"
/>
</Geom>
<Geom type="point" position={position} color="#1890ff" size={3} shape="circle" />
<Geom type="line" position={position2} color="#2fc25b" size={2}>
<Label
content={[
axisY2 + '*label',
formatLabel
]}
position="top"
/>
</Geom>
<Geom type="point" position={position2} color="#2fc25b" size={3} shape="circle"/>
</Chart>
</div>
);
}
}
export default DoubleAxes;
... ...
.top300-table-block {
margin-top: 30px;
position: relative;
}
.top300-stats {
border: 1px solid #e8e8e8;
padding: 5px 10px;
background-color: #fafafa;
}
.top300-stats p {
margin-bottom: 0;
}
.top300-stats .stats-col-block {
display: inline-block;
width: 90px;
}
.top300-stats .color-block {
display: inline-block;
width: 14px;
height: 14px;
vertical-align: middle;
border: 1px solid #000;
margin-right: 20px;
}
.top300-table {
width: 100%;
margin-top: -1px;
}
.top300-table th {
border: 1px solid #e8e8e8;
text-align: center;
background-color: #fafafa;
}
.top300-table td {
border: 1px solid #e8e8e8;
padding: 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top300-table .align-center {
text-align: center;
}
.top300-table .align-right {
text-align: right;
}
.top300-table-block .color-white {
background-color: #fff;
}
.top300-table-block .color-red {
background-color: #ffc7ce;
color: #9c0006;
}
.top300-table-block .color-green {
background-color: #c6efce;
color: #177218;
}
.top300-table-block .color-yellow {
background-color: #ffeb9c;
color: #9c6500;
}
.top300-table-block .color-gray {
background-color: #d0cece;
}
.top300-table-block .scroll-block {
overflow-x: scroll;
}
.top300-table .td-col-name {
max-width: 200px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top300-table .empty-tbody {
text-align: center;
line-height: 240px;
}
.top300-table .table-loading-tr td {
border: 0;
}
.top300-table .table-loading {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
padding-top: 40px;
background-color: rgba(170, 170, 170, 0.2);
}
... ...
import React from 'react';
import { DatePicker, Button, Tooltip, Spin } from 'antd';
import locale from 'antd/es/date-picker/locale/zh_CN';
import axios from '../../libs/axios';
import moment from 'moment';
import 'moment/locale/zh-cn';
import './top300-stock.css'
moment.locale('zh-cn');
class Stock extends React.Component {
constructor() {
super();
this.state = {
today: moment().endOf('day'),
onDay: '',
stats: {},
data: [],
loading: false
};
this.columns = [
{
title: 'id',
class: 'align-center',
render(record, index) {
return index + 1;
}
},
{
title: '毒id',
key: 'du_id',
},
{
title: '品牌',
key: 'brand',
},
{
title: '货号',
key: 'model',
},
{
title: '商品名称',
render(record) {
return <Tooltip className="td-col-name" placement="topLeft" title={record.name} >{record.name}</Tooltip>;
}
},
{
title: '商品id',
render(record) {
return record.product_id > 0 ? record.product_id : '';
}
},
{
title: '尺码',
key: 'size',
},
{
title: '销量',
render(record) {
return record.sold_num > -1 ? record.sold_num : '-';
}
},
{
title: '价格',
sub: [
{
title: '毒',
key: 'du_price',
class: 'align-right',
},
{
title: 'UFO',
key: 'ufo_price',
class: 'align-right',
},
]
},
{
title: '现货库存',
sub: [
{
title: 'time1',
key: 'time1',
class: 'align-right',
},
{
title: 'time2',
key: 'time2',
class: 'align-right',
},
{
title: 'time3',
key: 'time3',
class: 'align-right',
},
{
title: 'time4',
key: 'time4',
class: 'align-right',
},
]
},
{
title: '一周支付数',
sub: [
{
title: '现货',
key: 'week_spot_count',
class: 'align-right',
},
{
title: '总计',
key: 'week_count',
class: 'align-right',
}
]
}
];
let { headers, headRow } = this.formatHeader(this.columns);
this.headers = headers;
this.headRow = headRow;
}
formatHeader(columns, data = {}, deep = 0) {
data.headers = data.headers || [];
data.headRow = data.headRow || [];
let { headers, headRow } = data;
if (!headers[deep]) {
headers[deep] = [];
}
columns.map(val => {
let col = {
title: val.title,
key: val.key || ''
}
if (val.sub && val.sub.length) {
col.colspan = val.sub.length;
this.formatHeader(val.sub, data, deep + 1);
} else {
col.rowspan = '{rowspan}';
headRow.push(val);
}
headers[deep].push(col);
});
return {
headers,
headRow
};
}
componentDidMount() {
this.getStockData(this.state.today)
}
getStockData(day) {
this.setState({
loading: true,
onDay: day.format('YYYY-MM-DD')
});
axios.get(`/api/stats/top300/stock?day=${day.format('YYYY-MM-DD')}`).then(res => {
if (res.code !== 200) {
return;
}
let { list, stats } = res.data || {};
let timeTitle;
try {
timeTitle = JSON.parse(list[0].timeTitle)
} catch (e) {
console.log(e)
}
if (timeTitle) {
this.headers.map(row => {
row.map(col => {
if (timeTitle[col.key]) {
col.title = timeTitle[col.key];
}
});
});
}
setTimeout(() => {
this.setState({
loading: false,
data: list,
stats
});
}, list.length ? 300 : 1000);
});
}
dataOnChange(time) {
this.getStockData(time);
}
toPercent(numerator, denominator) {
if (!denominator) {
numerator = 0;
}
return `${numerator ? (numerator / denominator * 100).toFixed(2) : 0 }%`;
}
renderThead(headers) {
return (
<thead>
{headers.map((row, ri) => {
let rs = headers.length - ri;
return <tr key={'head-' + ri}>
{row.map((col, ci) => {
let crs = col.rowspan ? rs : 1;
let ccs = col.colspan || 1;
return <th key={ci} rowSpan={crs} colSpan={ccs} >{col.title}</th>;
})}
</tr>;
})}
</thead>
);
}
renderTbody(head, data) {
return (
<tbody>
{data.length ? data.map((row, ri) => {
let rowClass = row.class || {};
return <tr key={'body-' + ri}>
{head.map((col, ci) => {
let className = col.class || '';
let colInfo = col.key ? row[col.key] : col.render(row, ri);
className += `${className ? ' ' : ''}${rowClass[col.key] || ''}`
return <td key={ci} className={className} >{colInfo}</td>;
})}
</tr>;
}) : <tr><td className="empty-tbody" colSpan={head.length}>暂无数据</td></tr>}
<tr className="table-loading-tr">
<td colSpan={head.length}>
<Spin className="table-loading" spinning={this.state.loading} tip="加载中..."></Spin>
</td>
</tr>
</tbody>
);
}
render() {
const disabledDate = value => {
const { today } = this.state;
if (!value || !today) {
return false;
}
return value.valueOf() > today.valueOf();
};
const { gray, yellow, total } = this.state.stats || {};
const white = (total - yellow - gray) || 0;
return (
<div>
<div>
<DatePicker onChange={this.dataOnChange.bind(this)} disabledDate={disabledDate} locale={locale} defaultValue={this.state.today} />
<a href={`/api/stats/top300/stock/export?day=${this.state.onDay || this.state.today}`} target="_blank" style={{ float: 'right' }}>
<Button type="primary" shape="round" icon="download">导出</Button>
</a>
</div>
<div className="top300-table-block">
<div className="top300-stats">
<p>总计:{total || 0}</p>
<p>
颜色:白色 <em className="color-block color-white"></em>
<span className="stats-col-block">值:>0 </span>
<span className="stats-col-block">数量:{white || 0}</span>
占比:{this.toPercent(white, total)}
</p>
<p>
颜色:灰色 <em className="color-block color-gray"></em>
<span className="stats-col-block">值:0 </span>
<span className="stats-col-block">数量:{gray || 0}</span>
占比:{this.toPercent(gray, total)}
</p>
<p>
颜色:黄色 <em className="color-block color-yellow"></em>
<span className="stats-col-block">值:缺失 </span>
<span className="stats-col-block">数量:{yellow || 0}</span>
占比:{this.toPercent(yellow, total)}
</p>
</div>
<div className="scroll-block">
<table className="top300-table">
{this.renderThead(this.headers)}
{this.renderTbody(this.headRow, this.state.data)}
</table>
</div>
</div>
</div>
)
}
}
export default Stock;
... ...
import StatsSalePage from '../pages/stats/sales';
import Top300StockPage from '../pages/stats/top300-stock';
import SettingAccountPage from '../pages/setting/account';
const router = [
{
path: '/sales',
component: StatsSalePage
},
{
path: '/top300/stock',
component: Top300StockPage
},
{
path: '/setting/account',
component: SettingAccountPage
}
];
export default router;
... ...
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
sourceType: 'unambiguous',
plugins: [
'@babel/syntax-dynamic-import'
],
compact: false
};
... ...
import {subscribe} from 'webpack-hot-middleware/client?dynamicPublicPath=true&path=__webpack_hmr&noInfo=true&reload=true';
subscribe(function(event) {
if (event.action === 'reload') {
window.location.reload();
}
});
\ No newline at end of file
... ...
const path = require('path');
const config = require('../config/common');
module.exports = {
resolve: {
extensions: ['.js', '.json', '.jsx']
},
module: {
rules: [
{
test: /\.js?x?$/,
loader: 'babel-loader'
}
]
}
}
\ No newline at end of file
... ...
const merge = require('webpack-merge');
const AssetsPlugin = require('assets-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
const config = require('../config/common');
const baseWebpackConfig = require('./webpack.base.conf');
const isProd = process.env.NODE_ENV === 'production';
const clientWebpackConfig = merge(baseWebpackConfig, {
mode: isProd ? 'production' : 'development',
entry: {
app: path.join(__dirname, '../apps/entry-client.js')
},
optimization: {
runtimeChunk: true,
splitChunks: {
maxAsyncRequests: Infinity,
maxInitialRequests: Infinity,
chunks: 'all',
cacheGroups: {
vendors: {
priority: -10,
chunks: 'all',
name: 'vendors',
test: /[\\/]node_modules[\\/]/
}
}
}
},
output: {
path: path.join(__dirname, `../${config.build.outputPath}/${config.build.publicPath}`),
filename: 'js/[name].[hash].js',
chunkFilename: 'js/[id].[chunkhash].js',
publicPath: config.build.publicPath
},
module: {
rules: [
{
test: /\.js?x?$/,
loader: 'babel-loader',
},
{
test: /\.s?css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name].[hash:7].[ext]'
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'media/[name].[hash:7].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[hash:7].[ext]'
}
}
]
},
devServer: {
host: '127.0.0.1',
port: config.port + 2,
publicPath: config.build.publicPath,
contentBase: false,
hot: true,
inline: true,
compress: true,
stats: {
colors: true,
children: false,
chunks: false,
assetsSort: 'size'
},
headers: {
'Access-Control-Allow-Origin': '*'
}
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
allChunks: true
}),
new AssetsPlugin({
filename: 'manifest.json',
manifestFirst: true,
keepInMemory: true
})
]
});
module.exports = clientWebpackConfig;
... ...
const merge = require('webpack-merge');
const path = require('path');
const config = require('../config/common');
const baseWebpackConfig = require('./webpack.base.conf');
const serverWebpackConfig = merge(baseWebpackConfig, {
mode: 'development',
target: 'node',
entry: {
app: path.join(__dirname, '../apps/entry-server.js')
},
output: {
path: path.join(__dirname, '../' + config.build.outputPath),
filename: 'server-entry.js',
chunkFilename: 'js/[id].[chunkhash].js',
publicPath: '/assets/',
libraryTarget: 'commonjs2'
},
module: {
rules: [{
test: /\.s?css$/,
use: 'ignore-loader'
}]
},
// 去除依赖,不打包到生成的文件中
// 打包出来的代码是运行在node环境中的,这些类库是可以通过require()方式调用的
externals: Object.keys(require('../package.json').dependencies),
// devtool: config.dev.devtool,
});
module.exports = serverWebpackConfig;
\ No newline at end of file
... ...
module.exports = {
port: 6010,
mail: {
service: 'QQex',
auth: {
user: 'automan@yoho.cn',
pass: '8AE83e639d6b65f7'
}
},
yohops: {
name: 'yanqing.yang',
pwd: 'Y8682911q2'
},
build: {
outputPath: '/dist/',
publicPath: '/assets/'
},
database: {
connect: {
host: '192.168.102.219',
port: '3306',
user: 'yh_test',
password: 'yh_test',
charset: 'utf8mb4',
timezone: '+08:00'
},
database: 'ufo_sales_stats',
},
ldap: {
url: 'ldaps://172.16.50.12:636',
dcs: ['yoho01', 'local']
},
rootAccount: {
username: 'admin',
password: 'yohoadmin9646'
}
}
... ...
{
"name": "yoho-ufo-stats",
"version": "1.0.0",
"description": "A Yoho STATS Project With Express",
"repository": {
"type": "git",
"url": "git@git.yoho.cn:yangyanqing/yoho-ufo-stats.git"
},
"scripts": {
"dev": "nodemon -e js -i public/ -i dist/ -i build/ -i apps/ app.js",
"build:server": "NODE_ENV=production webpack --config ./build/webpack.server.conf.js",
"build:client": "NODE_ENV=production webpack --config ./build/webpack.client.conf.js"
},
"keywords": [
"ufo",
"du"
],
"author": "kingcoon@163.com",
"license": "ISC",
"dependencies": {
"antd": "^3.21.4",
"body-parser": "^1.19.0",
"client-sessions": "^0.8.0",
"cookie-parser": "^1.4.4",
"dayjs": "^1.8.16",
"express": "^4.17.1",
"ldapjs": "^1.0.2",
"lodash": "^4.17.15",
"mysql": "^2.17.1",
"nedb": "^1.8.0",
"node-lockup": "^1.0.3",
"node-schedule": "^1.3.2",
"nodemailer": "^6.3.0",
"path": "^0.12.7",
"querystring": "^0.2.0",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-helmet": "^5.2.1",
"react-router-dom": "^5.0.1",
"request": "^2.88.0",
"request-promise": "^4.2.4",
"xlsx": "^0.15.1",
"xlsx-style": "^0.8.13"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.5.5",
"assets-webpack-plugin": "^3.9.10",
"autoprefixer": "^9.6.1",
"axios": "^0.19.0",
"babel-loader": "^8.0.6",
"bizcharts": "^3.5.5",
"css-loader": "^3.2.0",
"ignore-loader": "^0.1.2",
"memory-fs": "^0.4.1",
"mini-css-extract-plugin": "^0.8.0",
"nodemon": "^1.19.1",
"postcss": "^7.0.17",
"postcss-calc": "^7.0.1",
"postcss-loader": "^3.0.0",
"react-hot-loader": "^4.12.11",
"react-router": "^5.0.1",
"style-loader": "^1.0.0",
"url-loader": "^2.1.0",
"webpack": "^4.39.2",
"webpack-cli": "^3.3.6",
"webpack-dev-middleware": "^3.7.0",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.1"
}
}
... ...
module.exports = {
sourceMap: false,
plugins: [
require('postcss-calc')(),
require('autoprefixer')({
overrideBrowserslist: ['> 0.15% in CN']
})
]
};
... ...
{
"apps": [
{
"name": "yoho-ufo-stats",
"script": "./app.js",
"instances": "1",
"exec_mode": "cluster",
"merge_logs": true,
"log_date_format": "YYYY-MM-DD HH:mm Z",
"error_file": "/Data/logs/node/yoho-ufo-stats-err.log",
"out_file": "/Data/logs/node/yoho-ufo-stats-out.log",
"env": {
"TZ": "Asia/Shanghai",
"PORT": 6010
}
}
]
}
\ No newline at end of file
... ...
const fs = require('fs');
const _ = require('lodash');
const path = require('path');
const lockup = require('node-lockup');
const schedule = require('node-schedule');
const yohops = require('./node-jobs/yohops');
const python = require('./python-jobs/job');
const { loadExcel } = require('../utils/excel');
const { insertOrderDailyStats, insertProductDailyStats, insertBrandDailyStats, insertTop300Stock } = require('./sql-util');
const lockInsertProductDailyStats = lockup(insertProductDailyStats);
const lockInsertBrandDailyStats = lockup(insertBrandDailyStats);
const lockInsertTop300Stock = lockup(insertTop300Stock);
const clearPath = path.join(__dirname, './python-jobs/files');
exports.createSchedule = async() => {
// 定时清理目录
schedule.scheduleJob('* * * * 4', function () {
if (fs.existsSync(clearPath)) {
fs.readdirSync(clearPath).forEach((file) => {
let curPath = `${path}/${file}`;
if (!fs.statSync(curPath).isDirectory()) {
fs.unlinkSync(curPath);
}
});
}
})
const salesStatsJob = new schedule.RecurrenceRule();
salesStatsJob.hour = 6
salesStatsJob.minute = 10
schedule.scheduleJob(salesStatsJob, function() {
python('task17', true).then(du => {
yohops.getUfoStats().then((ufo) => {
if (!ufo) {
return console.error('node job error on "getUfoStats"');
}
if (du.day !== ufo.day) {
return console.error('du数据日期与ufo数据日期不匹配');
}
insertOrderDailyStats(du, ufo);
});
});
});
const ProductStatsJob = new schedule.RecurrenceRule();
ProductStatsJob.hour = 6
ProductStatsJob.minute = 15
schedule.scheduleJob(ProductStatsJob, function () {
python('task-product-daily', true).then(prodRes => {
let { path, day } = prodRes;
loadExcel(path).then(res => {
res = _.take(res, 1000);
_.forEach(res, (row, index) => {
lockInsertProductDailyStats({ day, ...row }).then(() => {
if (index + 1 >= res.length) {
console.log(`[product_daily] ${day} over on ${new Date()}`);
}
});
});
});
python('task-brand-daily', true, path).then(brandRes => {
loadExcel(brandRes.path).then(res => {
_.forEach(res, (row, index) => {
lockInsertBrandDailyStats({ day, ...row }).then(() => {
if (index + 1 >= res.length) {
console.log(`[brand_daily] ${day} over on ${new Date()}`);
}
});
});
});
});
});
});
const top300StockStatsJob = new schedule.RecurrenceRule();
top300StockStatsJob.hour = 8
top300StockStatsJob.minute = 20
schedule.scheduleJob(top300StockStatsJob, function () {
python('task-top300-size-3day', true).then(sizeRes => {
let { path, day } = sizeRes;
python('task-top300-stock', true, path).then(stockRes => {
loadExcel(stockRes.path).then(res => {
_.forEach(res, (row, index) => {
lockInsertTop300Stock({ day, ...row }).then(() => {
if (index + 1 >= res.length) {
console.log(`[top300_stock] over on ${new Date()}`);
}
});
})
});
});
});
});
};
... ...
const _ = require('lodash');
const rp = require('request-promise');
const dayjs = require('dayjs');
const config = require('../../config/common');
const headers = {
Cookie: 'JSESSIONID=CC7FF14A64E94F2B9C1165FAFF1B3F489;',
Referer: 'http://www.yohops.com/user/toLogin?loginTargetUrl=/sqlOperate/toSqlOperate',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
};
const login = async() => {
try {
const result = await rp({
method: 'POST',
uri: 'http://www.yohops.com/user/login',
form: config.yohops,
headers: Object.assign({
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
'Content-Type': 'application/x-www-form-urlencoded'
}, headers)
});
return result;
} catch (error) {
console.error(JSON.stringify(error));
return Promise.resolve({});
}
};
const getUfoStats = async(needLogin) => {
if (needLogin) {
await login();
}
try {
const result = await rp({
method: 'POST',
uri: 'http://www.yohops.com//sqlOperate/query',
form: {
dataSourceInfo: '172.31.10.175:3306',
dbInfo: 'ufo_order',
queryContent: `SELECT count(1), count(DISTINCT(product_id)), sum(amount),FROM_UNIXTIME(create_time,'%Y-%m-%d') FROM ( SELECT sp.product_id, op.amount, op.create_time FROM ufo_order.buyer_order bo JOIN ufo_order.orders_pay op ON bo.order_code = op.order_code inner join ufo_order.buyer_order_goods bog on bog.order_code = bo.order_code inner join ufo_product.storage_price sp on bog.skup = sp.skup WHERE FROM_UNIXTIME(op.create_time, '%Y%m%d') > '${dayjs().subtract(2, 'day').format('YYYYMMDD')}' ) res group by FROM_UNIXTIME(create_time,'%Y-%m-%d');`,
currentPage: 0
},
headers: Object.assign({
Accept: 'application/json, text/javascript, */*; q=0.01',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}, headers)
});
let stats = _.get(JSON.parse(result), 'data.jsonArrayExcelData[0]', {});
return {
amount: stats['sum(amount)'],
count: stats['count(1)'],
prdcount: stats['count(DISTINCT(product_id))'],
day: stats['FROM_UNIXTIME(create_time,\'%Y-%m-%d\')']
}
} catch (error) {
let setCookie = _.get(error, 'response.headers[set-cookie][0]', '');
setCookie.split(';').forEach(val => {
if (val.indexOf('JSESSIONID') >= 0) {
headers.Cookie = val + ';';
}
});
if (!needLogin) {
return await getUfoStats(true);
}
return Promise.resolve(false);
}
};
module.exports = {
getUfoStats
};
\ No newline at end of file
... ...
const _ = require('lodash');
const path = require('path');
const process = require('child_process');
const jobPath = path.join(__dirname, './');
module.exports = (name, file, ...args) => {
args = args || [];
file && args.unshift(`${jobPath}files`);
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`python job time out: ${name}`);
return resolve('');
}, 1000 * 60 * 10);
console.log(`python job start: ${name}`);
process.exec(`python3 ${jobPath}${name}.py${args.length ? ' ' + args.join(' ') : ''}`, function (error, stdout, stderr) {
if (error || stderr) {
console.log(`python job error: ${name}`);
return reject(error || stderr);
}
console.log(`python job end: ${name}`);
try {
return resolve(JSON.parse(stdout));
} catch (e) {
return resolve(_.trim(stdout));
}
})
})
}
... ...
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import pandas as pd
import pymysql
import json
import datetime
# 根据id给出所有尺码
conn = pymysql.connect(host="192.168.103.250", user='root',
passwd='123456', charset="utf8")
def readBrandFromMysql():
sql_query = "SELECT PRODUCT_ID as productId, BRAND_ID FROM du_info.BRAND_PRODUCT WHERE 1"
df = pd.read_sql(sql_query, con=conn)
return df
def readBrandNameFromMysql():
sql_query = "SELECT ID as BRAND_ID, NAME FROM du_info.BRAND_7 WHERE 1"
df = pd.read_sql(sql_query, con=conn)
return df
def brandAmount(file, outPath):
df = pd.read_excel(file, 'Sheet1')
brand_df = pd.concat([readBrandFromMysql()], sort=True)
name_df = pd.concat([readBrandNameFromMysql()], sort=True)
result = pd.merge(df, brand_df, on=['productId'])
amount = result['amount'].groupby([result['BRAND_ID']]).sum()
amount.rename(columns={'BRAND_NAME': 'name'}, inplace=True)
priceData = pd.DataFrame(amount, columns=['amount'])
finalData = priceData.reset_index()
finalData2 = pd.merge(finalData, name_df, on=['BRAND_ID'])
finalData1 = finalData2.sort_values(
by='amount', ascending=False)
nowTime = datetime.datetime.now()
yesTime = nowTime + datetime.timedelta(days=-1)
filePath = outPath+'/brand-daily-'+yesTime.strftime('%m%d')+'.xlsx'
finalData1.to_excel(filePath, sheet_name='Sheet1', index=0)
print(json.dumps({
'path': filePath
}))
if(sys.argv.__len__() > 2):
brandAmount(sys.argv[2], sys.argv[1])
... ...
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pymysql
import pandas as pd
import sys
import json
import datetime
# 按productId统计销售额
conn = pymysql.connect(host="192.168.103.250", user='root',
passwd='123456', charset="utf8")
def readDataFromMysql(db, table, where):
sql_query = 'SELECT * FROM '+db+'.'+table+where
df = pd.read_sql(sql_query, con=conn)
return df
nowTime = datetime.datetime.now()
yesTime = nowTime + datetime.timedelta(days=-1)
# 获取昨天数据
wh = ' WHERE time > "'+yesTime.strftime('%Y-%m-%d')+' 00:00:00" AND time < "'+nowTime.strftime('%Y-%m-%d')+' 00:00:00"'
r1 = readDataFromMysql('appbuyer', 'buyers_' + yesTime.strftime('%Y_%m_%d'), wh)
r2 = readDataFromMysql('appbuyer', 'buyers_' + nowTime.strftime('%Y_%m_%d'), wh)
df = pd.concat([r1, r2], sort=True)
# 统计productId个数并生成表格
repeat = df['productId'].value_counts()
repeat.rename(columns={'productId': 'ProductId'}, inplace=True)
addCol = pd.DataFrame(repeat, columns=['count'])
finalData1 = addCol.reset_index()
finalData1.rename(columns={'index': 'productId'}, inplace=True)
# 统计productId销售额并生成表格
amount = df['price'].groupby([df['productId']]).sum()
# print(r)
amount.rename(columns={'productId': 'ProductId'}, inplace=True)
priceData = pd.DataFrame(amount, columns=['amount'])
finalData = priceData.reset_index()
price = df['price'].groupby([df['productId']]).mean()
price.rename(columns={'productId': 'ProductId'}, inplace=True)
priceData = pd.DataFrame(price, columns=['price'])
priceResult = priceData.reset_index()
mergePrice = pd.merge(finalData, priceResult, on=['productId'])
result = pd.merge(mergePrice, finalData1, on=['productId'])
# print(df)
dfData = df[['productId', 'model', 'productName', 'soldNum']]
dfData1 = dfData.drop_duplicates(['productId'], keep="first", inplace=False)
outputResult = pd.merge(result, dfData1, how='left', on=['productId'])
exportData = outputResult.sort_values(
by='amount', ascending=False)
if(sys.argv.__len__() > 1):
filePath = sys.argv[1]+'/product-daily-'+yesTime.strftime('%m%d')+'.xlsx'
exportData.to_excel(filePath,
sheet_name='Sheet1', index=0)
print(json.dumps({
'path': filePath,
'day': yesTime.strftime('%Y-%m-%d')
}))
... ...
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pymysql
import pandas as pd
import urllib.request
import hashlib
from urllib.parse import urlencode
import sys
import json
import datetime
nowTime = datetime.datetime.now()
yesTime = nowTime + datetime.timedelta(days=-1)
beforeYesTime = nowTime + datetime.timedelta(days=-2)
global_tables = [
'buyers_' + beforeYesTime.strftime('%Y_%m_%d'),
'buyers_' + yesTime.strftime('%Y_%m_%d'),
'buyers_' + nowTime.strftime('%Y_%m_%d')
]
conn = pymysql.connect(host="192.168.103.250", user='root',
passwd='123456', charset="utf8")
conn219 = pymysql.connect(host="192.168.102.219", user='yh_test',
passwd='yh_test', charset="utf8")
def readTopSalesFromMysql(tables, limit):
unionTables = []
for i in list(tables):
unionTables.append('SELECT * FROM appbuyer.' + i)
sql_query = 'SELECT * FROM (SELECT productId, productName, model, SUM(price) as amount, COUNT(1) as count FROM (' + ' UNION '.join(unionTables) + ') as td GROUP BY td.`productId`) as tds ORDER BY tds.amount DESC LIMIT ' + str(limit)
df = pd.read_sql(sql_query, con=conn)
return df
def readSizeSalesFromMysql(tables, pids):
unionTables = []
inPids = []
for i in list(tables):
unionTables.append('SELECT * FROM appbuyer.' + i)
for i in list(pids):
inPids.append(str(i))
sql_query = 'SELECT productId, size, SUM(price) as amount, COUNT(1) as count FROM (' + ' UNION '.join(unionTables) + ') as td WHERE productId IN (' + ', '.join(inPids) + ') GROUP BY productId, size'
df = pd.read_sql(sql_query, con=conn)
return df
# 网络获取实时数据
def getSign(params):
sign_str = ''
for key in sorted(params.keys()):
sign_str += key + str(params[key])
md5 = hashlib.md5()
md5.update((sign_str + '048a9c4943398714b356a696503d2d36').encode('utf8'))
signed_str = md5.hexdigest()
return signed_str
def processDuData(data):
if(type(data) == dict):
sizeList = []
for item in data['sizeList']:
price = 0
if(type(item['item']) == dict):
price = item['item']['price'] / 100
sizeItem = [
data['detail']['productId'],
data['detail']['title'],
data['detail']['articleNumber'],
item['size'],
price,
data['detail']['soldNum']
]
sizeList.append(sizeItem)
return sizeList
CONVER_SIZE_DIC = {
'35 1/2': '35.5',
'36 2/3': '36.5',
'37 1/3': '37',
'38 2/3': '38.5',
'39 1/3': '39',
'40 2/3': '40.5',
'41 1/3': '41',
'42 2/3': '42.5',
'43 1/3': '43',
'44 2/3': '44.5',
'45 1/3': '45',
'46 2/3': '46.5',
'47 1/3': '47',
'49 1/3': '49',
}
def processUfoData(data, id):
sizeList = []
if len(data):
for item in data:
if('price' in item):
sizeName = item['size_name']
if sizeName in CONVER_SIZE_DIC:
sizeName = CONVER_SIZE_DIC[sizeName]
sizeList.append([
int(id),
sizeName,
item['price']
])
return sizeList
def getDu(prdId):
params = {
'productId': prdId,
'productSourceName': 'shareDetail'
}
sign = getSign(params)
params.update({'sign': sign})
url = 'https://app.poizon.com/api/v1/h5/index/fire/flow/product/detail?' + urlencode(params)
try:
resu = urllib.request.urlopen(url, data=None, timeout=10)
data = json.loads(resu.read().decode()) # 解析返回对象获取
if data['status'] == 200:
return data
elif data['status'] == 6000:
return {'productId': prdId}
else:
return None
except:
return None
def toInt(str):
try:
return int(str)
except:
try:
return int(float(str))
except:
return 0
def getUfo(id):
id = toInt(id)
if id > 0:
url = 'http://ufo-auth.yohops.com/open/getProductSizePriceList?productid=' + str(id)
try:
resu = urllib.request.urlopen(url, data=None, timeout=10)
data = json.loads(resu.read().decode()) # 解析返回对象获取
if data['code'] == 200:
return data
else:
return None
except:
return None
else:
return None
def getPidByModel(models):
url = "http://ufo-auth.yohops.com/open/getProductIdByCode"
reqData = bytes(urllib.parse.urlencode({
'codes': ','.join(models)
}), 'utf-8')
resJson = []
try:
resu = urllib.request.urlopen(url=url, data=reqData, timeout=10)
resData = json.loads(resu.read().decode()) # 解析返回对象获取
if resData['code'] == 200:
resJson = resData
except:
pass
prodList = []
if ('data' in resJson):
data = resJson['data']
for item in data:
prodItem = [
str(item['product_code']),
int(item['product_id'])
]
prodList.append(prodItem)
return pd.DataFrame(prodList, columns=['model', 'yhid'])
df_top300 = pd.concat([readTopSalesFromMysql(global_tables, 300)], sort=True)
df_relation = getPidByModel(list(df_top300['model']))
df_top300_relation = pd.merge(df_top300, df_relation, on=['model'], how='left').fillna(0)
list_top300_pid = list(df_top300_relation['productId'])
list_top300_yhid = list(df_top300_relation['yhid'])
# 实时获取毒尺码/价格
list_du_size = []
for duid in list_top300_pid:
res = getDu(duid)
if(res != None):
if('data' in res):
list_size = processDuData(res['data'])
list_du_size = list_du_size + list_size
df_du_online = pd.DataFrame(list_du_size, columns=['productId', 'productName', 'model', 'size', 'duPrice', 'soldNum'])
# 实时获取UFO尺码/价格
list_ufo_size = []
for i in list(list_top300_yhid):
res = getUfo(i)
if(res != None):
if('data' in res):
list_size = processUfoData(res['data'], i)
list_ufo_size = list_ufo_size + list_size
df_ufo_online = pd.DataFrame(list_ufo_size, columns=['yhid', 'size', 'ufoPrice'])
df_du_online = pd.merge(df_du_online, df_relation, on=['model'], how='left')
df_du_online = pd.merge(df_du_online, df_ufo_online, on=['yhid', 'size'], how='left')
df_top300_size_sales = pd.concat([readSizeSalesFromMysql(global_tables, list_top300_pid)], sort=True)
df_du_online = pd.merge(df_du_online, df_top300_size_sales, on=['productId', 'size'], how='left').fillna('0')
df_du_online.rename(columns={'productId': 'id'}, inplace=True)
df_du_online['id'] = df_du_online['id'].astype('category')
df_du_online['id'].cat.set_categories(list_top300_pid, inplace=True)
df_du_online.sort_values('id', inplace=True)
if(sys.argv.__len__() > 1):
filePath = sys.argv[1]+'/top300-size-' + nowTime.strftime('%m%d') + '.xlsx'
df_du_online.to_excel(filePath, sheet_name='Sheet1', index=0)
print(json.dumps({
'path': filePath,
'day': nowTime.strftime('%Y-%m-%d')
}))
... ...
import pandas as pd
import requests
import time
import urllib
import numpy as np
import xlwt
from pymongo import MongoClient
import pymysql
import datetime
# from xlwt import *
import sys
import json
nowTime = datetime.datetime.now()
yesTime = nowTime + datetime.timedelta(days=-1)
weekAgoTime = nowTime + datetime.timedelta(days=-7)
outputObj = {}
global skns_file_path
global save_stock_path
global proccessed_stock_path
global result_path
def getProductCodes():
df = pd.read_excel(skns_file_path)
df = df.groupby(['id', 'model']).agg({'size': 'count'}).reset_index()
return df['model'].tolist()
def getHeaders():
url = 'http://run.yohops.com/data-analysis-web/user/login?username=admin&password=yohodata123456'
header = {
'Accept': 'application/json, text/plain, */*',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,ja;q=0.6',
'Connection': 'keep-alive',
'Host': 'run.yohops.com',
'Referer': 'http://run.yohops.com/login.html',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
}
r = requests.get(url, headers=header)
cookie = requests.utils.dict_from_cookiejar(r.cookies)
header['Cookie'] = 'JSESSIONID=%s; u_=%s; p_=%s' % (cookie['JSESSIONID'], cookie['u_'], cookie['p_'])
header['Referer'] = 'http://run.yohops.com/sys/search.html'
return header
def getStock(models, headers):
endstamp = int(time.time() / 3600) * 3600
timestamp = endstamp - 8 * 60 * 60
url = "http://run.yohops.com/data-analysis-web/executeSql?sql=select id, product_id, product_name, product_code, size_name, size_id, on_count, pay_count, FROM_UNIXTIME(create_time,{0}) as create_timestr, create_date from ufo_product_size_storeage_analysis where pre_sale_flag = 0 and create_time >={1} and create_time < {2} and product_code in {3} group by product_id,size_name, create_timestr;&db=run&export=Y".format(
urllib.parse.quote_plus("'%m%d%H'"), timestamp, endstamp, str(tuple(models)))
response = requests.get(url, headers=headers)
with open(save_stock_path, "wb") as code:
code.write(response.content)
def getWeekSales(models, headers, spot):
url = "http://ufo-auth.yohops.com/open/exportdata"
headers['Host'] = 'ufo-auth.yohops.com'
headers['Referer'] = 'http://ufo-auth.yohops.com/login.html'
reqData = {
'startdate': weekAgoTime.strftime('%Y%m%d'),
'enddate': yesTime.strftime('%Y%m%d'),
'productcodes': ','.join(models)
}
columnName = 'count_spot'
if int(spot) > 0:
reqData['isproductready'] = 1
columnName = 'count_all'
res = requests.post(url=url, data=reqData, headers=headers, timeout=10)
resJson = res.json()
data = []
if ('data' in resJson):
data = resJson['data']
countList = []
outputObj['week_' + columnName] = str(len(data))
for item in data:
countItem = [
str(item['product_code']),
str(item['size_name']),
item['count(1)'],
]
countList.append(countItem)
return pd.DataFrame(countList, columns=['model', 'size', columnName])
def convertSize(row, df):
CONVER_SIZE_DIC = {
'35 1/2': '35.5',
'36 2/3': '36.5',
'37 1/3': '37',
'38 2/3': '38.5',
'39 1/3': '39',
'40 2/3': '40.5',
'41 1/3': '41',
'42 2/3': '42.5',
'43 1/3': '43',
'44 2/3': '44.5',
'45 1/3': '45',
'46 2/3': '46.5',
'47 1/3': '47',
'49 1/3': '49',
}
if 'adidas' in row['product_name'] and row['size_name'] in CONVER_SIZE_DIC:
row['size_name'] = CONVER_SIZE_DIC[row['size_name']]
size_name = row['size_name']
orignal_pay_count = df[(df['size_name'] == size_name) & (df['product_id'] == row['product_id']) & (df['product_code'] == row['product_code'])]['pay_count'].max()
row['pay_count'] = orignal_pay_count + row['pay_count']
return row
def handleExcel():
df = pd.read_excel(save_stock_path,
dtype={'id': np.int32, 'product_id': np.int32, 'product_code': str, 'size_name': str,
'on_count': np.int32, 'pay_count': np.int32, 'create_timestr': str, 'create_date': str})
df = df[df['product_name'] != '作废']
# df = df[df['product_name'].str.contains("adidas", case = False)]
df = df.apply(convertSize, args=[df], axis=1)
del df['size_id']
df = df.groupby(['product_id', 'product_name', 'product_code', 'size_name', 'create_timestr', 'create_date']).agg(
{'on_count': sum, 'pay_count': sum}).reset_index()
createtimelist = df.groupby(['create_timestr']).agg({'on_count': sum}).index.values.tolist()
createtimelist.sort(reverse=True)
newdf = df.groupby(['product_id', 'product_name', 'product_code', 'size_name']).agg(
{'on_count': sum}).reset_index()
for createtime in createtimelist:
sub_df = df[df['create_timestr'] == createtime]
if createtimelist.index(createtime) == 0:
sub_df = sub_df[['pay_count', 'on_count', 'product_id', 'size_name']].rename(
columns={'on_count': createtime})
else:
sub_df = sub_df[['on_count', 'product_id', 'size_name']].rename(columns={'on_count': createtime})
newdf = newdf.merge(sub_df, how='left', on=['product_id', 'size_name']).fillna(0)
newdf.insert(loc=4, column='size_id', value=0)
newdf.to_excel(proccessed_stock_path, index=None)
def getBrandIDAndName():
db = pymysql.connect(host='172.16.6.117',
port=3306,
user='root',
password='asdf1234!',
database='duapp',
charset='utf8')
cursor = db.cursor()
query = 'SELECT * FROM duapp.BRAND;'
cursor.execute(query)
results = cursor.fetchall()
brand_id_name_dic = {}
for row_number in range(0, cursor.rowcount):
brand_id_name_dic[str(results[row_number][0])] = results[row_number][1]
return brand_id_name_dic
def getSknsExcelBrand(brand_id_name_dic):
conn = MongoClient('172.16.6.117', 27017)
mongodb = conn.du
product_db = mongodb.product
du_id_brandname_dic = {}
for product in product_db.find({}):
brandIDStr = str(product['detail']['brandId'])
productIDStr = str(product['detail']['productId'])
if brandIDStr in brand_id_name_dic:
du_id_brandname_dic[productIDStr] = brand_id_name_dic[brandIDStr]
du_id_brandname_dic['18624'] = 'Timberland'
du_id_brandname_dic['22922'] = 'Nike'
du_id_brandname_dic['31470'] = 'adidas'
du_id_brandname_dic['31471'] = 'adidas'
du_id_brandname_dic['31515'] = 'adidas'
du_id_brandname_dic['26063'] = 'Nike'
return du_id_brandname_dic
def getSkuCount(df, model, size):
week_df = df[df['model'].str.lower().isin([model.lower()])]
week_size_df = week_df[week_df['size'].str.lower().isin([str(size).lower()])]
count = '0'
if len(week_size_df) > 0:
count = week_size_df.iloc[0, 2]
return count
def combineExcels(du_id_brandname_dic, week_spot_pay_count, week_pay_count):
aligment = xlwt.Alignment()
aligment.horz = aligment.HORZ_RIGHT
style_gray = xlwt.XFStyle()
pattern = xlwt.Pattern()
pattern.pattern = xlwt.Pattern.SOLID_PATTERN
pattern.pattern_fore_colour = xlwt.Style.colour_map['gray25'] # 设置单元格背景色为黄色
style_gray.pattern = pattern
style_gray.alignment = aligment
style_red = xlwt.XFStyle()
pattern = xlwt.Pattern()
pattern.pattern = xlwt.Pattern.SOLID_PATTERN
pattern.pattern_fore_colour = xlwt.Style.colour_map['red'] # 设置单元格背景色为黄色
style_red.pattern = pattern
style_red.alignment = aligment
style_yellow = xlwt.XFStyle()
pattern = xlwt.Pattern()
pattern.pattern = xlwt.Pattern.SOLID_PATTERN
pattern.pattern_fore_colour = xlwt.Style.colour_map['yellow'] # 设置单元格背景色为黄色
style_yellow.pattern = pattern
style_yellow.alignment = aligment
style_green = xlwt.XFStyle()
pattern = xlwt.Pattern()
pattern.pattern = xlwt.Pattern.SOLID_PATTERN
pattern.pattern_fore_colour = xlwt.Style.colour_map['green'] # 设置单元格背景色为黄色
style_green.pattern = pattern
style_green.alignment = aligment
style_orange = xlwt.XFStyle()
pattern = xlwt.Pattern()
pattern.pattern = xlwt.Pattern.SOLID_PATTERN
pattern.pattern_fore_colour = xlwt.Style.colour_map['light_orange'] # 设置单元格背景色为黄色
style_orange.pattern = pattern
style_orange.alignment = aligment
style_green = xlwt.XFStyle()
pattern = xlwt.Pattern()
pattern.pattern = xlwt.Pattern.SOLID_PATTERN
pattern.pattern_fore_colour = xlwt.Style.colour_map['sea_green'] # 设置单元格背景色为黄色
style_green.pattern = pattern
borders = xlwt.Borders()
borders.left = xlwt.Borders.THIN
borders.right = xlwt.Borders.THIN
borders.top = xlwt.Borders.THIN
borders.bottom = xlwt.Borders.THIN
style_green.borders = borders
style_border = xlwt.XFStyle()
style_border.borders = borders
# 关心的
SIZE_CONCERN = ['36', '37', '37.5', '38', '41', '42', '43']
skns_df = pd.read_excel(skns_file_path, dtype={'id': np.int32, 'model': str, 'size': str})
ufo_df = pd.read_excel(proccessed_stock_path,
dtype={'id': np.int32, 'product_id': np.int32, 'product_code': str, 'size_name': str,
'on_count': np.int32, 'create_timestr': str, 'create_date': str})
workbook_write = xlwt.Workbook(encoding='UTF-8')
worksheet_write = workbook_write.add_sheet('Sheet1')
worksheet_write.write(0, 0, 'id')
worksheet_write.write(0, 1, 'duId')
worksheet_write.write(0, 2, 'brand')
worksheet_write.write(0, 3, 'model')
worksheet_write.write(0, 4, 'name')
worksheet_write.write(0, 5, 'productId')
worksheet_write.write(0, 6, 'size')
worksheet_write.write(0, 7, 'duPrice')
worksheet_write.write(0, 8, 'ufoPrice')
offset_price = 2
time_extra_name = {}
time_on_count = ufo_df.columns
for i in range(7, 11):
worksheet_write.write(0, i+offset_price, 'time' + str(i - 6))
time_extra_name['time' + str(i - 6)] = time_on_count[17 - i]
worksheet_write.write(0, 11+offset_price, 'count')
worksheet_write.write(0, 12+offset_price, 'weekSpotCount')
worksheet_write.write(0, 13+offset_price, 'weekCount')
worksheet_write.write(0, 14+offset_price, 'status')
worksheet_write.write(0, 15+offset_price, 'extra')
time_extra_name_json = json.dumps(time_extra_name)
rowNum = 1
gray = 0
yellow = 0
greater_then_zero = 0
for index, row in skns_df.iterrows():
currentSizeName = index
try:
float(row['size'])
currentSizeName = row['size']
if currentSizeName in SIZE_CONCERN:
pass
else:
currentSizeName = ''
except:
currentSizeName = ''
pass
if currentSizeName == '':
continue
brand = ''
duid = str(row['id'])
model = row['model']
yhid = row['yhid']
duPrice = row['duPrice']
ufoPrice = row['ufoPrice']
soldNum = row['count']
if duid in du_id_brandname_dic:
brand = du_id_brandname_dic[duid]
sub_df = ufo_df[ufo_df['product_code'].str.lower().isin([model.lower()])]
worksheet_write.write(rowNum, 0, rowNum)
worksheet_write.write(rowNum, 1, duid)
worksheet_write.write(rowNum, 2, brand)
worksheet_write.write(rowNum, 3, model)
productID = ''
if len(sub_df) > 0:
productID = sub_df.iloc[0, 0]
productName = sub_df.iloc[0, 1]
sub_df = sub_df[sub_df['size_name'].isin([currentSizeName])]
worksheet_write.write(rowNum, 4, productName)
if len(sub_df) > 0:
for i in range(7, 11):
currentCount = 0
try:
currentCount = int(sub_df.iloc[0, 17 - i])
except:
pass
if i > 7:
preCount = 0
try:
preCount = int(sub_df.iloc[0, 17 - i + 1])
except:
preCount = 0
if currentCount > preCount:
worksheet_write.write(rowNum, i+offset_price, currentCount, style=style_red)
elif currentCount < preCount:
worksheet_write.write(rowNum, i+offset_price, currentCount, style=style_green)
elif currentCount == 0:
worksheet_write.write(rowNum, i+offset_price, currentCount, style=style_gray)
else:
worksheet_write.write(rowNum, i+offset_price, currentCount)
else:
if currentCount == 0:
worksheet_write.write(rowNum, i+offset_price, currentCount, style=style_gray)
else:
worksheet_write.write(rowNum, i+offset_price, currentCount)
if sub_df.iat[0, 5] == 0:
gray += 1
worksheet_write.write(rowNum, 14+offset_price, '1')
else:
greater_then_zero += 1
worksheet_write.write(rowNum, 14+offset_price, '0')
else:
#有对应型号没对应尺码
for i in range(7, 11):
worksheet_write.write(rowNum, i+offset_price, '0', style=style_gray)
gray += 1
worksheet_write.write(rowNum, 14+offset_price, '1')
else:
stock_style = style_yellow
if int(yhid) > 0:
productID = yhid
stock_style = style_gray
gray += 1
worksheet_write.write(rowNum, 14+offset_price, '1')
else:
yellow += 1
worksheet_write.write(rowNum, 14+offset_price, '2')
#无对应型号
for i in range(7, 11):
worksheet_write.write(rowNum, i+offset_price, '0', style=stock_style)
worksheet_write.write(rowNum, 5, str(productID))
worksheet_write.write(rowNum, 6, currentSizeName)
worksheet_write.write(rowNum, 7, duPrice)
worksheet_write.write(rowNum, 8, ufoPrice)
worksheet_write.write(rowNum, 11+offset_price, soldNum)
sizeCount = getSkuCount(week_pay_count, model, row['size'])
sizeSpotCount = getSkuCount(week_spot_pay_count, model, row['size'])
worksheet_write.write(rowNum, 12+offset_price, str(sizeSpotCount))
worksheet_write.write(rowNum, 13+offset_price, str(sizeCount))
worksheet_write.write(rowNum, 15+offset_price, time_extra_name_json)
rowNum += 1
workbook_write.save(result_path)
if(sys.argv.__len__() > 2):
skns_file_path = sys.argv[2]
save_stock_path = sys.argv[1] + '/auto_stock%s.xls' % (nowTime.strftime("%m%d"))
proccessed_stock_path = sys.argv[1] + '/auto_stock_processed%s.xls' % (nowTime.strftime("%m%d"))
result_path = sys.argv[1] + '/top300_sales_stock%s.xls' % (nowTime.strftime("%m%d"))
modellist = getProductCodes()
headers = getHeaders()
getStock(modellist, headers)
week_spot_df = getWeekSales(modellist, headers, 0)
week_df = getWeekSales(modellist, headers, 1)
handleExcel()
brand_id_name_dic = getBrandIDAndName()
du_id_brandname_dic = getSknsExcelBrand(brand_id_name_dic)
combineExcels(du_id_brandname_dic, week_spot_df, week_df)
outputObj['path'] = result_path
outputObj['day'] = nowTime.strftime('%Y-%m-%d')
print(json.dumps(outputObj))
... ...
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 按天统计销量、销售额及skn数量
import pymysql
import pandas as pd
import json
import datetime
conn = pymysql.connect(host="192.168.103.250", user='root',
passwd='123456', charset="utf8")
def readDataFromMysql(db, table, where):
sql_query = 'SELECT * FROM '+db+'.'+table+where
df = pd.read_sql(sql_query, con=conn)
return df
nowTime = datetime.datetime.now()
yesTime = nowTime + datetime.timedelta(days=-1)
# 获取某张表数据
wh = ' WHERE time > "'+yesTime.strftime('%Y-%m-%d')+' 00:00:00" AND time < "'+nowTime.strftime('%Y-%m-%d')+' 00:00:00"'
r1 = readDataFromMysql('appbuyer', 'buyers_' + yesTime.strftime('%Y_%m_%d'), wh)
r2 = readDataFromMysql('appbuyer', 'buyers_' + nowTime.strftime('%Y_%m_%d'), wh)
df = pd.concat([r1, r2], sort=True)
# 新增day字段 以便按天计算销量及销售额
time1 = df['time'].astype('datetime64[D]')
df['day'] = time1
daysum = df['price'].groupby([df['day']]).sum()
count = df['price'].groupby([df['day']]).count()
# 每天的productId个数
prd = df['productId'].groupby([df['day'], df['productId']]).count()
prd.rename(columns={'day': 'Day'}, inplace=True)
addCol = pd.DataFrame(prd, columns=['prdcount'])
finalData = addCol.reset_index()
prdcount = finalData['day'].value_counts()
finalprd = prdcount.reset_index()
finalprd.rename(columns={'day': 'prdcount', 'index': 'day'}, inplace=True)
result = pd.merge(daysum, count, on=['day'])
result.rename(columns={'price_x': 'amount',
'price_y': 'count'}, inplace=True)
finalData1 = result.reset_index()
result1 = pd.merge(finalData1, finalprd, on=['day'])
result1.set_index(["day"], inplace=True)
result1 = result1.loc[yesTime.strftime('%Y-%m-%d')]
print(json.dumps({
'amount': result1['amount'],
'count': result1['count'],
'prdcount': result1['prdcount'],
'day': yesTime.strftime('%Y-%m-%d')
}))
... ...
const { mysqlPool } = require('../utils/mysql');
const _ = require('lodash');
const insertOrderDailyStats = async(du, ufo) => {
console.log(du.day, du.amount, +ufo.amount, du.count, ufo.count, du.prdcount, ufo.prdcount);
const insertRow = await mysqlPool.insert('INSERT INTO `order_daily_stats` (`day`, `ufo_sales_amount`, `du_sales_amount`, `ufo_order_count`,`du_order_count`, `ufo_product_count`, `du_product_count`, `update_time`) VALUES (:day, :ufoAmount, :duAmount, :ufoCount, :duCount, :ufoProductCount, :duProductCount, :updateTime)', {
day: du.day,
ufoAmount: +ufo.amount,
duAmount: du.amount,
ufoCount: ufo.count,
duCount: du.count,
ufoProductCount: ufo.prdcount,
duProductCount: du.prdcount,
updateTime: Date.parse(new Date()) / 1000
});
console.log(`[amount-daily] ${du.day} insert ${insertRow ? 'succeed' : 'failed'} `);
return true;
};
const insertProductDailyStats = async (info, retry) => {
const insertRow = await mysqlPool.insert('INSERT INTO `product_daily_stats` (`day`, `pid`, `product_name`, `model`, `amount`, `update_time`) VALUES (:day, :productId, :productName, :model, :amount, :updateTime)', {
day: info.day,
productId: info.productId,
productName: info.productName,
model: info.model,
amount: info.amount,
updateTime: Date.parse(new Date()) / 1000
});
if (!insertRow) {
if (retry) {
console.log(`[product_daily_insert_error] day:${info.day} data:${JSON.stringify(info)}`);
} else {
await insertProductDailyStats(info, true);
}
}
return true;
};
const insertBrandDailyStats = async (info, retry) => {
const insertRow = await mysqlPool.insert('INSERT INTO `brand_daily_stats` (`day`, `bid`, `name`, `amount`, `update_time`) VALUES (:day, :brandId, :brandName, :amount, :updateTime)', {
day: info.day,
brandId: info.BRAND_ID,
brandName: info.NAME,
amount: info.amount,
updateTime: Date.parse(new Date()) / 1000
});
if (!insertRow) {
if (retry) {
console.log(`[brand_daily_insert_error] day:${info.day} data:${JSON.stringify(info)}`);
} else {
await insertBrandDailyStats(info, true);
}
}
return true;
};
const insertTop300Stock = async (info, retry) => {
const numberKey = ['duId', 'productId', 'time1', 'time2', 'time3', 'time4', 'count', 'weekCount', 'weekSpotCount', 'status'];
numberKey.map(val => {
info[val] = _.toNumber(info[val]) || 0;
});
if (!info.status) {
if (!_.compact([info.time1, info.time2, info.time3, info.time4]).length) {
info.status = 1;
}
}
info.name = info.name || '';
info.brand = info.brand || '';
const insertRow = await mysqlPool.insert('INSERT INTO `top300_stock` (`du_id`, `brand`, `model`, `name`, `product_id`, `size`, `sold_num`, `du_price`, `ufo_price`, `time1`, `time2`, `time3`, `time4`, `week_spot_count`, `week_count`, `status`, `day`, `extra`)' +
' VALUES (:duId, :brand, :model, :name, :productId, :size, :count, :duPrice, :ufoPrice, :time1, :time2, :time3, :time4, :weekSpotCount, :weekCount, :status, :day, :extra)', info);
if (!insertRow) {
if (retry) {
console.log(`[top300_stock_insert_error] day:${info.day} data:${JSON.stringify(info)}`);
} else {
await insertTop300Stock(info, true);
}
}
return true;
};
module.exports = {
insertOrderDailyStats,
insertProductDailyStats,
insertBrandDailyStats,
insertTop300Stock
};
... ...
const express = require('express');
const router = express.Router();
const app = express();
const ufoCtrl = require('./ufo/controller');
const passportCtrl = require('./passport/controller');
const auth = global.auth;
router.get('/ufo_du/sales', auth, ufoCtrl.getSalesStats);
router.get('/stats/top300/stock', auth, ufoCtrl.getTop300Stock);
router.get('/stats/top300/stock/export', auth, ufoCtrl.exportTop300Stock);
router.post('/passport/login', passportCtrl.loginByPassword);
router.get('/passport/account/list', auth, passportCtrl.checkAdmin, passportCtrl.accountList);
router.post('/passport/account/add', auth, passportCtrl.checkAdmin, passportCtrl.accountAdd);
router.post('/passport/account/delete', auth, passportCtrl.checkAdmin, passportCtrl.accountDelete);
app.use(router);
app.all('*', (req, res) => {
return res.json({
code: 404,
message: 'api not found'
});
});
module.exports = app;
... ...
const _ = require('lodash');
const { login, account } = require('./model');
const config = global.config;
const loginByPassword = (req, res, next) => {
login.password(req.body).then(result => {
if (result.access) {
req.session.user = {
name: result.username
};
}
res.json({
code: 200,
data: {
access: result.access
},
message: result.message
});
}).catch(next)
};
const checkAdmin = (req, res, next) => {
let account = config.rootAccount || {};
if (account.username !== _.get(req.session, 'user.name')) {
return res.json({
code: 400,
message: '没有访问权限,请联系管理员开通'
});;
}
return next();
};
const accountList = (req, res, next) => {
account.list().then(list => {
return res.json({
code: 200,
data: { list },
message: ''
});
}).catch(next);
};
const accountAdd = (req, res, next) => {
account.add(req.body.account, _.get(req.session, 'user.name')).then(result => {
if (result.err) {
return res.json({
code: 400,
message: err
});
}
if (req.body.needList) {
accountList(req, res, next);
} else {
return res.json({
code: 200,
message: '添加成功'
});
}
}).catch(next);
};
const accountDelete = (req, res, next) => {
account.delete(req.body.id).then(() => {
return res.json({
code: 200,
message: '删除成功'
});
}).catch(next);
};
module.exports = {
loginByPassword,
checkAdmin,
accountList,
accountAdd,
accountDelete
};
... ...
const _ = require('lodash');
const { checkUser } = require(global.utilPath + '/ldap');
const fileDb = require(global.utilPath + '/file-db');
const config = global.config;
const ERROR_TIP = {
DEFAULT: '账号或密码错误,请确认后登陆',
REFUSE: '没有访问权限,请联系管理员开通'
};
const login = {
password: async(info) => {
let account = config.rootAccount || {};
let { username, password } = info || {};
let access = false;
let message = '';
if (account.username === username) {
if (account.password === password) {
access = true;
} else {
message = ERROR_TIP.DEFAULT;
}
} else {
let user = await checkUser(username, password);
if (user) {
let users = await fileDb.findOne({ name: username });
console.log(users)
if (users) {
access = true;
} else {
message = ERROR_TIP.REFUSE;
}
} else {
message = ERROR_TIP.DEFAULT;
}
}
return {
username,
access,
message
};
}
};
const account = {
async add(addUser, username) {
let users = _.trim(addUser).split(',');
if (!users.length) {
return { err: '请输入要添加的账户' };
}
let failedAccount = [];
for (let i = 0; i < users.length; i++) {
let name = _.replace(users[i], '@yoho.cn', '');
await fileDb.insert({
name,
time: Date.parse(new Date()) / 1000,
operator: username
}).catch(e => {
failedAccount.push(name)
});
}
return {
filed: failedAccount.join(',')
};
},
list() {
return fileDb.findAll();
},
delete(id) {
return fileDb.remove(id);
}
};
module.exports = {
login,
account
};
... ...
const model = require('./model');
const { exportExcel } = require(global.utilPath + '/excel');
const getSalesStats = (req, res, next) => {
model.getSalesStats(req.query).then(result => {
res.json({
code: 200,
data: result
});
}).catch(next)
};
const getTop300Stock = (req, res, next) => {
model.getTop300Stock(req.query).then(result => {
res.json({
code: 200,
data: result
});
}).catch(next)
};
const exportTop300Stock = (req, res, next) => {
model.exportTop300Stock(req.query).then(result => {
return exportExcel({
title: result.header,
data: result.body,
fileName: `top300_stock_${req.query.day}`,
sheetName: 'sheet1',
res
})
}).catch(next)
};
module.exports = {
getSalesStats,
getTop300Stock,
exportTop300Stock
};
... ...
const _ = require('lodash');
const moment = require('moment');
const { mysqlPool } = require(global.utilPath + '/mysql');
const { camelCase } = require(global.utilPath + '/helper');
function toPercent(numerator, denominator) {
if (!denominator) {
numerator = 0;
}
return `${numerator ? (numerator / denominator * 100).toFixed(2) : 0}%`;
}
const getSalesStats = async({startTime, endTime}) => {
if (!endTime || !startTime) {
endTime = Date.parse(new Date()) / 1000;
startTime = endTime - 4 * 24 * 60 * 60;
}
const salesData = await mysqlPool.query('SELECT * FROM `order_daily_stats` WHERE `day` > :startTime AND `day` < :endTime ORDER by `day` desc limit 30', {
startTime: moment(startTime * 1000).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(endTime * 1000).format('YYYY-MM-DD HH:mm:ss')
});
_.forEach(salesData, val => {
let date = new Date(val.day);
val.day = `${date.getMonth() + 1}/${date.getDate()}`;
val.timestamp = Date.parse(date) / 1000;
});
return _.sortBy(camelCase(salesData), o => o.timestamp);
};
const getTop300Stock = async({day}) => {
const arr = ['time1', 'time2', 'time3', 'time4'];
const stockData = await mysqlPool.query('SELECT * FROM `top300_stock` WHERE `day` = :day', {
day: day || moment().format('YYYY-MM-DD')
});
const stats = {
gray: 0,
yellow: 0,
total: 0
};
if (stockData && stockData.length) {
stats.total = stockData.length;
stockData.map((val, index) => {
let classObj = {};
let preVal;
for (let i = 0; i < arr.length; i++) {
let key = arr[i];
let timeVal = +val[key];
if (+val.status === 2) {
classObj[key] = 'color-yellow';
} else {
if (timeVal === 0) {
classObj[key] = 'color-gray';
}
if (timeVal > preVal) {
classObj[key] = 'color-red';
} else if (timeVal < preVal) {
classObj[key] = 'color-green';
}
preVal = timeVal;
}
}
val.class = classObj;
if (!index) {
val.timeTitle = val.extra;
}
switch (+val.status) {
case 1:
stats.gray++;
break;
case 2:
stats.yellow++;
break;
}
_.unset(val, 'id');
_.unset(val, 'day');
_.unset(val, 'extra');
});
}
return {
stats,
list: stockData
};
}
const exportTop300Stock = async({day}) => {
let data = await getTop300Stock({ day });
let { gray, yellow, total } = data.stats || {};
let white = total - gray - yellow;
const dataSource = {
count: [
{ name: '白色', val: '>0', num: white, percent: toPercent(white, total) },
{ name: '灰色', val: '0', num: gray, percent: toPercent(gray, total) },
{ name: '黄色', val: '缺失', num: yellow, percent: toPercent(yellow, total) },
{ name: '总计', num: total },
]
};
const rowReplaceKey = '__row__';
const handleStatus = (status) => {
let str = '';
switch(+status) {
case 1:
str = '0';
break;
case 2:
str = '缺失';
break;
default:
str = '>0';
break;
}
return str;
};
const headers = [
{ title: 'id', render(record, index) { return index + 1; } },
{ title: '毒id', key: 'du_id' },
{ title: '品牌', key: 'brand'},
{ title: '货号', key: 'model'},
{ title: '商品名称', key: 'name' },
{ title: '商品id', key: 'product_id' },
{ title: '尺码', key: 'size' },
{ title: '销量', key: 'sold_num' },
{ title: '毒售价', key: 'du_price' },
{ title: 'UFO售价', key: 'ufo_price' },
{ title: 'time1', key: 'time1', titleNameObj: 'timeTitle' },
{ title: 'time2', key: 'time2', titleNameObj: 'timeTitle' },
{ title: 'time3', key: 'time3', titleNameObj: 'timeTitle' },
{ title: 'time4', key: 'time4', titleNameObj: 'timeTitle' },
{ title: '一周支付数现货', key: 'week_spot_count' },
{ title: '一周支付数总计', key: 'week_count' },
{ title: '状态', render(record) { return handleStatus(record.status)} },
{ title: '' },
{ title: '颜色', dataSource: `count${rowReplaceKey}.name` },
{ title: '值', dataSource: `count${rowReplaceKey}.val` },
{ title: '数量', dataSource: `count${rowReplaceKey}.num` },
{ title: '占比', dataSource: `count${rowReplaceKey}.percent` }
];
let first = _.get(data, 'list[0]');
if (first) {
try {
first.timeTitle = JSON.parse(first.timeTitle);
} catch (e) {
console.error('first line parse error: ', e)
}
}
let headerData = [];
let bodyData = [];
headers.forEach((v, i) => {
headerData.push(_.get(first, `${v.titleNameObj}.${v.key}`, v.title) || '');
});
data.list.forEach((dv, di) => {
let row = [];
headers.forEach((v, i) => {
let val = '';
if (v.dataSource) {
val = _.get(dataSource, _.replace(v.dataSource, rowReplaceKey, `[${di}]`), '');
} else {
if (v.render) {
val = v.render(dv, di);
} else if (v.key) {
val = dv[v.key] || '0';
}
}
row.push(val || '');
});
bodyData.push(row);
});
return {
header: headerData,
body: bodyData,
}
}
module.exports = {
getSalesStats,
getTop300Stock,
exportTop300Stock
};
... ...
module.exports = app => {
app.use('/api', require('./api')); // api
app.use('/mail', require('./mail')); // 邮件
};
... ...
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const sessions = require("client-sessions");
const querystring = require('querystring');
const render = require('./render');
const config = require('../config/common');
global.config = config;
global.utilPath = path.join(__dirname, '../utils');
global.auth = (req, res, next) => {
if (!req.session.user) {
if (req.xhr) {
res.json({
code: 401,
message: '抱歉,您暂未登录!'
});
return false;
}
res.redirect(`/login.html?${querystring.stringify({ refer: req.originalUrl})}`);
return false;
}
next && next();
return true;
};
exports.createServer = async() => {
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../dist')));
app.use(sessions({
requestKey: 'session',
cookieName: 'ufo_stats_session',
secret: 'yoho_ufo_stats_9646',
duration: 24 * 60 * 60 * 1000,
activeDuration: 1000 * 60 * 5
}));
// dispatcher
try {
require('./dispatch')(app);
app.all('*', render);
// 后置中间件
app.use((err, req, res, next) => {
console.log(err)
return res.status(500).end();
});
} catch (err) {
console.error(err);
}
// listener
app.listen(config.port, function () {
console.log('Project server listening on port ' + config.port);
});
};
... ...
const express = require('express');
const router = express.Router();
const app = express();
router.get('/test', (req, res) => {
console.log(321312);
return res.send('1231231');
});
router.post('/test', (req, res) => {
console.log('post');
console.log(req.body);
return res.json('1231231');
});
app.use(router);
module.exports = app;
\ No newline at end of file
... ...
const _ = require('lodash');
const { renderToString } = require('react-dom/server');
const template = require('./template');
const { devServer } = require('./ssr');
const publicPath = ['/login.html'];
let auth;
let serverBundle, clientManifest;
let readyPromise = devServer(({bundle, manifest}) => {
serverBundle = bundle;
clientManifest = manifest;
});
const getStatics = () => {
let statics = _.get(clientManifest, 'app', {});
let js = '';
let css = '';
_.concat(_.get(clientManifest, '["runtime~app"].js'), _.get(clientManifest, 'vendors.js'), statics.js).forEach(val => {
if (val) {
js += `<script src="${val}" defer></script>`;
}
});
_.concat(_.get(clientManifest, 'vendors.css'), statics.css).forEach(val => {
if (val) {
css += `<link rel="stylesheet" href="${val}">`
}
});
return { js, css };
}
const pageAuth = (req, res) => {
return new Promise(resolve => {
if (publicPath.indexOf(req.path) > -1) {
return resolve(true);
}
if (!auth) {
auth = global.auth;
}
if (auth(req, res)) {
return resolve(true);
} else {
return resolve(false);
}
});
}
module.exports = (req, res, next) => {
pageAuth(req, res).then(pass => {
if (!pass) {
return;
}
readyPromise.then(() => {
if (!serverBundle || !clientManifest) {
return next({code: 500});
}
const createApp = serverBundle.default;
const body = renderToString(createApp({path: req.path}));
res.send(template({
body,
title: 'YOHO!UFO',
statics: getStatics()
}));
});
});
};
... ...
const express = require('express');
const vm = require('vm');
const fs = require('fs');
const path = require('path');
const MemoryFs = require('memory-fs');
const nativeModule = require('module');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const clientConfig = require('../build/webpack.client.conf');
const serverConfig = require('../build/webpack.server.conf');
const bundlePath = path.join(serverConfig.output.path, serverConfig.output.filename);
const manifestPath = path.join(clientConfig.output.path, '../../manifest.json');
const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
let manifestProd;
if (!isDev) {
manifestProd = require(manifestPath);
}
const getModuleFromString = (bundle, filename) => {
const m = { exports: {} };
const wrapper = nativeModule.wrap(bundle);
const script = new vm.Script(wrapper, {
filename,
displayErrors: true,
});
const result = script.runInThisContext();
result.call(m.exports, m.exports, require, m);
return m;
}
exports.devServer = (cb) => {
const app = express();
let bundle, manifest;
let resolve;
let realyPromise = new Promise(r => {
resolve = r;
});
let ready = (...args) => {
resolve();
cb(...args);
};
if (isDev) {
const webpack = require('webpack');
clientConfig.entry.app = ['./build/client-hot.js', clientConfig.entry.app];
clientConfig.output.publicPath = '//127.0.0.1:6012/';
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
);
const clientCompiler = webpack(clientConfig);
devMiddleware = webpackDevMiddleware(clientCompiler, {
publicPath: clientConfig.output.publicPath,
quiet: true,
headers: {
'Access-Control-Allow-Origin': '*'
}
});
app.use(devMiddleware);
clientCompiler.plugin('done', stats => {
stats = stats.toJson();
stats.errors.forEach(error => console.log(error));
stats.warnings.forEach(warning => console.log(warning));
if (stats.errors.length) {
return;
}
try {
manifest = JSON.parse(devMiddleware.fileSystem.readFileSync(manifestPath, 'utf-8'));
} catch (e) {
console.error('manifest load failure!');
}
if (bundle) {
ready({bundle, manifest});
}
});
hotMiddleware = webpackHotMiddleware(clientCompiler, { heartbeat: 5000 });
app.use(hotMiddleware);
const mfs = new MemoryFs();
const serverCompiler = webpack(serverConfig);
serverCompiler.outputFileSystem = mfs;
serverCompiler.watch({}, (err, stats) => {
if (err) throw err;
const info = stats.toJson();
if (stats.hasErrors()) {
console.log('错误');
console.error(info.errors);
}
if (stats.hasWarnings()) {
console.log('警告');
console.warn(info.warnings);
}
const bundleSource = mfs.readFileSync(bundlePath, 'utf8');
const m = getModuleFromString(bundleSource, 'server-entry.js');
bundle = m.exports;
if (manifest) {
ready({bundle, manifest});
}
});
} else {
const bundleSource = fs.readFileSync(bundlePath, 'utf8');
const m = getModuleFromString(bundleSource, 'server-entry.js');
ready({ bundle: m.exports, manifest: manifestProd});
}
app.listen(6012, () => {
console.log('static server is started');
});
return realyPromise;
}
... ...
module.exports = ({body, title, statics}) => {
return `
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
${statics.css}
</head>
<body>
<div id="root">${body}</div>
</body>
${statics.js}
</html>
`;
};
\ No newline at end of file
... ...
const xlsx = require('xlsx');
const XLSX = require('xlsx-style');
const fs = require('fs');
const os = require('os');
var exec = require('child_process').exec;
function datenum(v, date1904) {
if (date1904) {
v += 1462;
}
let epoch = Date.parse(v);
return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
}
let Workbook = function () {
this.SheetNames = [];
this.Sheets = {};
};
const sheet_arr = (data) => {
let ws = {};
let wscols = [];
let range = { s: { c: 10000000, r: 10000000 }, e: { c: 0, r: 0 } };
for (let R = 0; R !== data.length; ++R) {
for (let C = 0; C !== data[R].length; ++C) {
wscols.push({});// 单元格宽度
if (range.s.r > R) {
range.s.r = R;
}
if (range.s.c > C) {
range.s.c = C;
}
if (range.e.r < R) {
range.e.r = R;
}
if (range.e.c < C) {
range.e.c = C;
}
let cell = { v: data[R][C] };
if (cell.v === null) {
continue;
}
let cell_ref = XLSX.utils.encode_cell({ c: C, r: R });
/* TEST: proper cell types and value handling */
if (typeof cell.v === 'number') {
cell.t = 'n';
} else if (typeof cell.v === 'boolean') {
cell.t = 'b';
} else if (cell.v instanceof Date) {
cell.t = 'n';
cell.z = XLSX.SSF._table[14];
cell.v = datenum(cell.v);
} else {
cell.t = 's';
}
cell.s = {
border: {
left: {
style: 'thin',
color: {
auto: 1
}
},
right: {
style: 'thin',
color: {
auto: 1
}
},
top: {
style: 'thin',
color: {
auto: 1
}
},
bottom: {
style: 'thin',
color: {
auto: 1
}
}
}
};
ws[cell_ref] = cell;
}
}
if (range.s.c < 10000000) {
ws['!ref'] = XLSX.utils.encode_range(range);
}
ws['!cols'] = wscols;
return ws;
};
const exportExcel = (obj) => {
let arr_name = obj.title;
let arr = obj.data;
let ws_name = obj.sheetName;
let file = os.tmpdir() + '/' + obj.fileName + '.xlsx';
let wb = new Workbook();
arr.unshift(arr_name);
wb.SheetNames.push(ws_name);
wb.Sheets[ws_name] = sheet_arr(arr);
XLSX.writeFile(wb, file);
let filestream = fs.createReadStream(file);
obj.res.setHeader('Content-Type', 'application/octet-stream');
obj.res.setHeader('Content-Disposition', 'attachment; filename=' + obj.fileName + '.xlsx');
filestream.pipe(obj.res);
exec(['rm', '-rf', file].join(' '));
};
const loadExcel = function(path) {
try {
let wb = xlsx.readFile(path);
const sheetNames = wb.SheetNames;
const worksheet = wb.Sheets[sheetNames[0]];
return Promise.resolve(xlsx.utils.sheet_to_json(worksheet));
} catch (error) {
console.error(error);
return Promise.resolve([]);
}
};
module.exports = {
loadExcel,
exportExcel
};
... ...
const nedb = require('nedb');
const path = require('path');
const db = new nedb({
filename: path.join(__dirname, '../db/user.db'),
autoload: true
})
const fileDb = {
findAll() {
return new Promise((resolve, reject) => {
db.find({}, function (err, doc) {
if (err) {
return reject(err);
}
return resolve(doc);
});
})
},
findOne(info) {
return new Promise((resolve, reject) => {
db.findOne(info, function (err, doc) {
if (err) {
return reject(err);
}
resolve(doc);
})
})
},
insert(info) {
return new Promise((resolve, reject) => {
db.insert(info, function (err, doc) {
if (err) {
return reject(err)
}
resolve(doc);
})
})
},
update(id, doc) {
return new Promise((resolve, reject) => {
db.update({
_id: id
}, {
$set: doc
}, {}, function (err, numAffected) {
if (err) {
return reject(err);
}
resolve(numAffected);
});
})
},
remove(id) {
return new Promise((resolve, reject) => {
db.remove({
_id: id
}, {
returnUpdatedDocs: true
}, function (err, numAffected, affectedDocuments) {
if (err) {
return reject(err);
}
resolve(affectedDocuments);
});
})
}
}
module.exports = fileDb;
... ...
const _ = require('lodash');
const camelCase = (info) => {
let res;
if (_.isArray(info)) {
res = [];
_.forEach(info, val => {
res.push(camelCase(val));
});
} else if (_.isObject(info)) {
res = {};
Object.keys(info).forEach(val => {
res[_.camelCase(val)] = info[val];
});
} else {
return info;
}
return res;
}
module.exports = {
camelCase
};
... ...
const ldap = require('ldapjs');
const config = global.config;
let client;
const createClient = () => {
client = ldap.createClient({
url: config.ldap.url,
tlsOptions: {
rejectUnauthorized: false
},
reconnect: true
});
return client;
};
const searchUser = (userName) => {
return new Promise((resolve, reject) => {
const searchPath = config.ldap.dcs.map(dc => {
return `dc=${dc}`;
}).join(',');
client.search(searchPath, {
scope: 'sub',
filter: `(&(objectclass=person)(sAMAccountName=${userName}))`
}, (error, res) => {
res.on('searchEntry', function (entry) {
resolve(entry.object);
});
res.on('error', function (e) {
console.log(e);
reject(e.message);
});
});
});
};
const checkUser = (userName, password) => {
if (!config.ldap) {
throw new Error('缺少ldap配置');
}
return new Promise((resolve, reject) => {
try {
if (!client) {
client = createClient();
}
const bindPath = config.ldap.dcs.join('.');
client.bind(`${userName}@${bindPath}`, password, async (err) => {
if (err) {
return resolve(void 0);
}
try {
const user = await searchUser(userName);
resolve(user);
client.unbind();
} catch (error) {
reject(error);
}
});
} catch (error) {
reject(error);
}
});
};
module.exports = {
checkUser
};
... ...
const _ = require('lodash');
const nodemailer = require('nodemailer');
const config = require('../config/common');
const transporter = nodemailer.createTransport(config.mail);
const formatTable = (data) => {
if (!_.isArray(data)) {
return '';
}
let thead = '';
let tbody = '';
_.forEach(data[0], val => {
thead += `<td>${val}</td>`;
});
_.forEach(data, (row, index) => {
index && _.forEach(row, val => {
tbody += `<td>${val}</td>`;
});
});
return `<table border=="1">
<thead>${thead}</thead>
<tbody>${tbody}</tbody>
</table>`;
}
module.exports = {
send: (data, cb) => {
transporter.sendMail(data, cb);
},
formatTable
}
\ No newline at end of file
... ...
const mysql = require('mysql');
const _ = require('lodash');
const config = require('../config/common');
class MysqlAdapter {
constructor(connect, database) {
this.connect = connect;
this.database = database;
this.createPool();
}
createPool() {
this.pool = mysql.createPool(Object.assign(this.connect, {
database: this.database,
queryFormat: function(query, values) {
let exeQuery;
if (!values) {
exeQuery = query;
} else {
exeQuery = query.replace(/:(\w+)/g, function(txt, key) {
if (values.hasOwnProperty(key)) {
return this.escape(values[key]);
}
return txt;
}.bind(this));
}
return exeQuery;
},
connectTimeout: 2000
}));
}
getConnection() {
return new Promise((resolve, reject) => {
this.pool.getConnection((connErr, connection) => {
if (connErr) {
reject(connErr);
} else {
resolve(connection);
}
});
});
}
query(sql, params, options) {
return this.execute(sql, params, options);
}
delete(sql, params) {
return this.execute(sql, params).then(result => {
return result.affectedRows;
});
}
update(sql, params) {
return this.execute(sql, params).then(result => {
return result.affectedRows;
});
}
insert(sql, params) {
return this.execute(sql, params).then(result => {
return result.insertId;
});
}
execute(sql, params) {
return new Promise((resolve, reject) => {
this.getConnection().then(connection => {
connection.query(sql, params, (queryErr, result) => {
connection.release();
if (queryErr) {
reject(queryErr);
} else {
resolve(result);
}
});
}).catch(e => {
reject(e);
});
});
}
transaction(sqls, cb) {
return new Promise((resolve, reject) => {
this.getConnection().then(connection => {
let promises = _.map(sqls, sql => {
return new Promise((res, rej) => {
connection.query(sql, (queryErr, result) => {
if (queryErr) {
connection.rollback();
rej(queryErr);
} else {
cb && cb(sql); // eslint-disable-line
res(result);
}
});
});
});
Promise.all(promises).then(results => {
connection.commit(err => {
if (err) {
connection.rollback(() => {
connection.release();
});
reject();
} else {
connection.release();
resolve(results);
}
});
}, () => {
reject();
});
});
});
}
changeDatabase(database) {
return new Promise(resolve => {
this.pool.end(() => {
this.createPool(database);
resolve();
});
});
}
close() {
this.pool.end();
}
}
module.exports = MysqlAdapter;
module.exports.mysqlPool = new MysqlAdapter(config.database.connect, config.database.database);
... ...
This diff could not be displayed because it is too large.