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

  1. Log in to register and change the account password
  2. Check the order list
  3. Order function
  4. Simple chat function
  5. The comment function
  6. Thumb up function
  7. Delete comments
  8. 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

  1. 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.
  2. 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

  1. 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
  2. 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.

  1. The Controller layer receives parameters, verifies them, and passes them to the Service layer for business logic
  2. The Service layer does the business logic
  3. The Model layer calls the database

Database Details

  1. The database is mysql
  2. 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
  1. 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 ~