preface
This is the first time to write an article for personal record and sharing. Please forgive me if there is anything wrong. Because I like to eat the capital (health), called the take-away is capital in the company, and then more and more people are following me, and every time I go to statistical number, details of each order, I am the final summary to the enterprise WeChat TXT text again to call the take-away, finally confirmed with capital crews to prevent more less (it’s a sad tears, Who made me so great? . Later, I felt it was too troublesome, so I took some time to develop a PC terminal system for ordering food for the capital, mainly for the convenience of myself.
Function points involved
- Log in to register and change the account password
- Check the order list
- Order function
- Simple chat function
- The comment function
- Thumb up function
- Delete comments
- View all order details of the day
The project address
Making: github.com/FEA-Dven/du…
Online: dywsweb.com/food/login (Account :admin, password :123)
Technology stack
React + ANTD
Back-end: nodeJS + KOa2
catalogue
The outermost project directory | | - ducheng - fontend front-end project | - main project code app | -- - | API request API - assets resources management | | - libs contains common function - model redux state management Routing | | - the router front - style front end styling | - views front page components | | - chat chat page - component front-end component | - index reservation system | home page - the login login page | - App. Js | - config. Js front-end domain configuration | - main. Js project main function | - fontserver front service | -- - | config front service configuration - controller front service control layer | -- -- -- the router front-end service routing | - utils front service of public library | | - views front service rendered template - app. Js front-end service main function | - node_modules | -. Babelrc | -. Gitignore | - gulpfile. Js | - package. The json | - pm2. Prod. Json building the front end of the online service pm2 configuration | - README. Md | - webpack. Config. Js build configuration | - backend Background project | - main project code app | -- - | the controller control layer - the layer model (operation database) | | - service services layer - the route routing | - the validation parameters calibration | - config Service configuration parameter | - library defines a class library | | - logs to store log - middleware middleware | - node_modules | - SQL database SQL statement here | -- - | util public function library - app. Js Project main function | - package. The jsonCopy the code
Front-end project summary
1. Build your own services
- Instead of using scaffolding, the project built its own front-end server, which is also a KOA2 framework. The WebPack configuration is parsed through KOA2, the resources are generated through webPack packaging, and the front-end service brings the resources into XTPL for rendering.
- There are also benefits to building your own server, either to solve cross-domain problems or to request backend servers through Node as a middle tier. Well, none of those benefits were used in this project.
if(isDev) {// KoawebPack module fastlet koaWebpack = require('koa-webpack-middleware')
let devMiddleware = koaWebpack.devMiddleware
let hotMiddleware = koaWebpack.hotMiddleware
let clientCompiler = require('webpack')(webpackConfig)
app.use(devMiddleware(clientCompiler, {
stats: {
colors: true
},
publicPath: webpackConfig.output.publicPath,
}))
app.use(hotMiddleware(clientCompiler))
}
app.use(async function(CTX, next) {// Set the environment and package the resource pathif (isDev) {
let assets ={}
const publicPath = webpackConfig.output.publicPath
assets.food = { js : publicPath + `food.js` }
ctx.assets = assets
} else {
ctx.assets = require('.. /build/assets.json')
}
await next()
})
Copy the code
2. Introduce HappyPack for quick packaging
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length / 2 }); Create a thread pool based on the number of CPU threadsCopy the code
plugins: [
new HappyPack({
id: 'happyBabel',
loaders: [{
loader: 'babel-loader? cacheDirectory=true',
}],
threadPool: happyThreadPool,
verbose: true,
}),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(env), }) ].concat(isDev? [ new webpack.HotModuleReplacementPlugin(), ]:[ new AssetsPlugin({filename:'./build/assets.json'}),
new webpack.optimize.ModuleConcatenationPlugin(),
new MiniCssExtractPlugin({
filename: '[name].[hash:8].css',
chunkFilename: "[id].[hash:8].css"}),]),Copy the code
3. Encapsulate the routing component for permission verification
functionRequireAuthentication (Component) {// Component returns with logged modules (to prevent re-rendering)if (Component.AuthenticatedComponent) {
returnComponent. AuthenticatedComponent} / / create validation Component class AuthenticatedComponent extends ponent {state = {React.Com login:true,}componentWillMount() {
this.checkAuth();
}
componentWillReceiveProps(nextProps) {
this.checkAuth();
}
checkAuth() {// Redirect to login page without loginlet login = UTIL.shouldRedirectToLogin();
if (login) {
window.location.href = '/food/login';
return; } this.setState({ login: ! login }); }render() {
if (this.state.login) {
return<Component {... this.props} /> }return ' '}}return AuthenticatedComponent
}
Copy the code
When loading the page, the permission verification component will perform permission verification first. If the browser has no parameter specified by cookie, it will return to the login page directly
<Provider store={store}>
<Router history={browserHistory} >
<Switch>
<Route path="/food/login" exact component={Login}/>
<Route path="/food/index" component={requireAuthentication(Index)}/>
<Route path="/food/chat" component={requireAuthentication(Chat)}/>
<Route component={Nomatchpage}/>
</Switch>
</Router>
</Provider>
Copy the code
4. Set the theme color through Webpack
{
test: /\.less|\.css$/,
use: [
{
loader: isDev ? 'style-loader' : MiniCssExtractPlugin.loader
}, {
loader: "css-loader"
}, {
loader: "less-loader",
options: {
javascriptEnabled: true,
modifyVars: {
'primary-color': '#0089ce'.'link-color': '#0089ce'},}}]}Copy the code
5, other
- The web page saves the user ID of the cookie, and puts the header into the server when the request is made to identify which user operates
- Each page is pieced together, so the data between the components has to be handled well
Back-end project summary
The framework design
It is mainly divided into controller layer, Service layer and Model layer.
- The Controller layer receives parameters, verifies them, and passes them to the Service layer for business logic
- The Service layer does the business logic
- The Model layer calls the database
Database Details
- The database is mysql
- The query database uses the SQL query builder Knex
this.readMysql = new Knex({
client: 'mysql',
debug: dbConfig.plat_read_mysql.debug,
connection: {
host: dbConfig.plat_read_mysql.host,
user: dbConfig.plat_read_mysql.user,
password: dbConfig.plat_read_mysql.password,
database: dbConfig.plat_read_mysql.database,
timezone: dbConfig.plat_read_mysql.timezone,
},
pool: {
min: dbConfig.plat_read_mysql.minConnection,
max: dbConfig.plat_read_mysql.maxConnection
},
});
this.writeMysql = new Knex({
client: 'mysql',
debug: dbConfig.plat_write_mysql.debug,
connection: {
host: dbConfig.plat_write_mysql.host,
user: dbConfig.plat_write_mysql.user,
password: dbConfig.plat_write_mysql.password,
database: dbConfig.plat_write_mysql.database,
timezone: dbConfig.plat_write_mysql.timezone,
},
pool: {
min: dbConfig.plat_write_mysql.minConnection,
max: dbConfig.plat_write_mysql.maxConnection
},
});
Copy the code
- The code above uses two query constructors to distinguish between database write and database read actions
Write an authentication middleware
checkHeader: async function(ctx, next) {
await validator.validate(
ctx.headerInput,
userValidation.checkHeader.schema,
userValidation.checkHeader.options
)
letcacheUserInfo = await db.redis.get(foodKeyDefines.userInfoCacheKey(ctx.headerInput.fid)) cacheUserInfo = UTIL.jsonParse(cacheUserInfo); // If there is no asymmetry between redis layer user information and token information, the user needs to log in againif(! cacheUserInfo || ctx.headerInput.token ! == cacheUserInfo.token) { throw new ApiError('food.userAccessTokenForbidden');
}
await next();
}
Copy the code
Using authentication middleware, take a route as an example
// introduce const routePermission = require('.. /.. /middleware/routePermission.js'); // Router.post ('/api/user/order', routePermission.checkHeader, userMenuController.userOrder);
Copy the code
Request error code encapsulation
Define a request error class
Class ApiError extends Error {/** * constructor * @param errorName Error name * @param params Error message */ constructor(errorName,... params) { super();leterrorInfo = apiErrorDefines(errorName, params); this.name = errorName; this.code = errorInfo.code; this.status = errorInfo.status; this.message = errorInfo.message; }}Copy the code
Error code definition
const defines = {
'common.all': {code: 1000, message: '%s', status: 500},
'request.paramError': {code: 1001, message: 'Parameter error %s', status: 200},
'access.forbidden': {code: 1010, message: 'No operation permission', status: 403},
'auth.notPermission': {code: 1011, message: 'Authorization failed %s', status: 403},
'role.notExist': {code: 1012, message: 'Character does not exist', status: 403},
'auth.codeExpired': {code: 1013, message: 'Authorization code has expired', status: 403},
'auth.codeError': {code: 1014, message: 'Authorization code error', status: 403},
'auth.pargramNotExist': {code: 1015, message: 'Program does not exist', status: 403},
'auth.pargramSecretError': {code: 1016, message: 'Program secret key error', status: 403},
'auth.pargramSecretEmpty': {code: 1016, message: 'Program key is empty, please configure it in background', status: 403},
'db.queryError': { code: 1100, message: 'Database query exception', status: 500 },
'db.insertError': { code: 1101, message: 'Database write exception', status: 500 },
'db.updateError': { code: 1102, message: 'Database update exception', status: 500 },
'db.deleteError': { code: 1103, message: 'Database deletion exception', status: 500 },
'redis.setError': { code: 1104, message: 'Redis setup exception', status: 500 },
'food.illegalUser' : {code: 1201, message: 'Illegal user', status: 403},
'food.userHasExist' : {code: 1202, message: 'User already exists', status: 200},
'food.objectNotExist' : {code: 1203, message: '%s', status: 200},
'food.insertMenuError': {code: 1204, message: 'Batch insert menu failed', status: 200},
'food.userNameInvalid': {code: 1205, message: 'I don't believe that's your name.', status: 200},
'food.userOrderAlready': {code: 1206, message: 'You've already made your reservation.', status: 200},
'food.userNotOrderToday': {code: 1207, message: 'You haven't made your reservation today', status: 200},
'food.orderIsEnd': {code: 1208, message: 'Reservations are closed. Please come again.', status: 200},
'food.blackHouse': {code: 1209, message: 'Don't do too much dirty work.', status: 200},
'food.userAccessTokenForbidden': { code: 1210, message: 'token failure', status: 403 },
'food.userHasStared': { code: 1211, message: 'You've already liked this comment', status: 200 },
'food.canNotReplySelf': { code: 1212, message: 'Can't reply to your own comments', status: 200 },
'food.overReplyLimit': { code: 1213, message: 'Number of replies has exceeded %s, no more replies can be made', status: 200 }
};
module.exports = function (errorName, params) {
if(defines[errorName]) {
let result = {
code: defines[errorName].code,
message: defines[errorName].message,
status: defines[errorName].status
};
params.forEach(element => {
result.message = (result.message).replace('%s', element);
});
return result;
}
return {
code: 1000,
message: 'Server internal error',
status: 500
};
}
Copy the code
Throw the wrong mechanism
When the program determines that an error has occurred, it can throw an error to the front end, such as an incorrect token.
// If there is no asymmetry between redis layer user information and token information, the user needs to log in againif(! cacheUserInfo || ctx.headerInput.token ! == cacheUserInfo.token) { throw new ApiError('food.userAccessTokenForbidden');
}
Copy the code
Because the program has a callback processing middleware, it can catch the defined ApiError
// requestError.js
module.exports = async function (ctx, next) {
let beginTime = new Date().getTime();
try {
await next();
let req = ctx.request;
let res = ctx.response;
let input = ctx.input;
let endTime = new Date().getTime();
let ip = req.get("X-Real-IP") || req.get("X-Forwarded-For") || req.ip;
let fields = {
status: res.status,
accept: req.header['accept'],
cookie: req.header['cookie'],
ua: req.header['user-agent'],
method: req.method,
headers: ctx.headers,
url: req.url,
client_ip: ip,
cost: endTime - beginTime,
input: input
};
logger.getLogger('access').trace('requestSuccess', fields);
} catch (e) {
if (e.code === 'ECONNREFUSED') {// Database connection failed logger.getLogger('error').fatal(Mysql connection failed, e.message, e.code);
e.code = 1;
e.message = 'Database connection exception';
}
if (e.code === 'ER_DUP_ENTRY') {
logger.getLogger('error').error(Mysql > select * from 'mysql ', e.message, e.code);
e.code = 1;
e.message = 'Database operation violates unique constraint';
}
if (e.code === 'ETIMEDOUT') {
logger.getLogger('error').error(Mysql > select * from 'mysql ', e.message, e.code);
e.code = 1;
e.message = 'Database connection timeout';
}
let req = ctx.request;
let res = ctx.response;
let status = e.status || 500;
let msg = e.message || e;
let input = ctx.input;
let endTime = new Date().getTime();
let ip = req.get("X-Real-IP") || req.get("X-Forwarded-For") || req.ip;
let fields = {
status: res.status,
accept: req.header['accept'],
cookie: req.header['cookie'],
ua: req.header['user-agent'],
method: req.method,
headers: ctx.headers,
url: req.url,
client_ip: ip,
cost: endTime - beginTime,
input: input,
msg: msg
};
ctx.status = status;
if (status === 500) {
logger.getLogger('access').error('requestError', fields);
} else {
logger.getLogger('access').warn('requestException', fields);
}
let errCode = e.code || 1;
if(! (parseInt(errCode) > 0)) { errCode = 1; }returnresponse.output(ctx, {}, errCode, msg, status); }};Copy the code
Introduce middleware in app.js
/** * use(require('./middleware/requestError.js'));
Copy the code
Database create SQL (name is not standard, please forgive me)
CREATE DATABASE food_program;
USE food_program;
# the user table
CREATE TABLE t_food_user(
fid int(11) auto_increment primary key COMMENT 'user id',
user_name varchar(255) NOT NULL COMMENT 'User name',
password varchar(255) NOT NULL COMMENT 'User password',
role TINYINT(2) DEFAULT 0 COMMENT 'User roles (project relationships, no associated tables)',
create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Creation time',
update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Modification time',
status TINYINT(2) DEFAULT 1 NOT NULL COMMENT 'Status 0: Deleted, 1: Normal',
UNIQUE KEY `uidx_fid_user_name` (`fid`,`user_name`) USING BTREE
)ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'Food User Table' ;
CREATE TABLE t_food_menu(
menu_id int(11) auto_increment primary key COMMENT 'menu ids',
menu_name varchar(255) NOT NULL COMMENT 'Menu nickname'.type TINYINT(2) DEFAULT 0 NOT NULL COMMENT 'Status 0: Daily menu, 1: regular, 2: burning in open oven',
price int(11) NOT NULL COMMENT 'price',
status TINYINT(2) DEFAULT 1 NOT NULL COMMENT 'Status 0: Deleted, 1: Normal',
create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Creation time',
update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Modification time',
UNIQUE KEY `uidx_menu_id_menu_name` (`menu_id`,`menu_name`) USING BTREE,
UNIQUE KEY `uidx_menu_id_menu_name_type` (`menu_id`,`menu_name`,`type`) USING BTREE
)ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'Food Menu List' ;
CREATE TABLE t_food_user_menu_refs(
id int(11) auto_increment primary key COMMENT 'record id',
fid int(11) NOT NULL COMMENT 'user id',
menu_id int(11) NOT NULL COMMENT 'menu ids'
create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Creation time',
update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Modification time',
status TINYINT(2) DEFAULT 1 NOT NULL COMMENT 'Status 0: Deleted, 1: Normal',
KEY `idx_fid_menu_id` (`fid`,`menu_id`) USING BTREE,
KEY `idx_fid_menu_id_status` (`fid`,`menu_id`,`status`) USING BTREE
)ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'What menu does the user select?' ;
CREATE TABLE t_food_system(
id int(11) auto_increment primary key COMMENT 'system id',
order_end TINYINT(2) DEFAULT 0 NOT NULL COMMENT 'Is the order due?',
update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Modification time'
)ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'Capital Order System' ;
CREATE TABLE t_food_comment(
comment_id int(11) auto_increment primary key COMMENT 'comment id',
fid int(11) NOT NULL COMMENT 'user id',
content TEXT COMMENT 'Comment content',
star int(11) DEFAULT 0 NOT NULL COMMENT 'Likes',
create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Creation time',
update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Modification time'
)ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'Capitol Chat list' ;
CREATE TABLE t_food_reply(
reply_id int(11) auto_increment primary key COMMENT 'reply id',
reply_fid int(11) NOT NULL COMMENT 'Reply user FID',
comment_fid int(11) NOT NULL COMMENT 'Comment user fid',
content TEXT COMMENT 'Reply content',
create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Creation time',
update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Modification time'. KEY `idx_reply_fid_comment_fid` (`reply_fid`,`comment_fid`) USING BTREE )ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='Capitol Chat list' ;
CREATE TABLE t_food_comment_star_refs(
id int(11) auto_increment primary key COMMENT 'relationship id',
comment_id int(11) NOT NULL COMMENT 'comment id',
comment_fid int(11) NOT NULL COMMENT 'user id',
star_fid int(11) NOT NULL COMMENT 'Like user fid',
create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Creation time',
update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT 'Modification time'. UNIQUE KEY `idx_comment_id_fid_star_fid` (`comment_id`,`fid`,`star_fid`) USING BTREE )ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='Capital Comment Like Association Table' ;
Copy the code
Project deployment
The front-end deployment
Local development
npm run dev
Development path
http://localhost:3006/food/login
Online deployment
npm install pm2 -g
npm run build
A build folder is generated that contains resources needed online
Nginx set
// /opt/food/fontend/build/ Yes NPM run build folder path location /assets/ {alias/opt/food/fontend/build/; } location / {proxy_pass http://127.0.0.1:3006/; }Copy the code
Start the project using PM2
pm2 start pm2.prod.json
The back-end deployment
Local development
pm2 start app.js –watch
Enable — Watch mode listens for project logs
Online deployment
pm2 start app.js
Do not enable –watch, because not requesting a service refresh will cause the database and Redis to reconnect, causing an error
At the end
After developing the system, I left in three weeks in the middle of winter… Then I went to interview some companies and showed it to the interviewer, and the HR was happy, but I didn’t know if the TECH officer was happy.
Welcome everyone to exchange oh ~