摘要:后來本人覺得太麻煩了,便抽了點時間去開發(fā)一個專為都城點餐的端系統(tǒng),主要為了方便自己。通過解析配置,通過打包生成資源,然后前端服務(wù)將資源引入到中達(dá)到渲染效果。搭建自己的服務(wù)器也有好處,可以解決跨域問題,或者通過作為中間層請求后臺服務(wù)器。
前言
第一次寫文章,用作個人記錄和分享交流,不好之處還請諒解。因本人喜愛吃都城(健康),在公司叫的外賣都是都城,然后越來越多人跟著我點,而且每次都是我去統(tǒng)計人數(shù),每個人點餐詳情,我都是通過企業(yè)微信最后匯總到txt文本上再去打電話叫外賣,最后跟都城工作人員確認(rèn)防止多點少點(真是一把辛酸淚,誰讓我這么偉大呢?)。后來本人覺得太麻煩了,便抽了點時間去開發(fā)一個專為都城點餐的PC端系統(tǒng),主要為了方便自己。
涉及功能點登錄注冊修改賬號密碼
查看訂餐列表
點餐功能
簡單聊天功能
評論功能
點贊功能
刪除評論功能
查看當(dāng)天所有訂單詳情功能
項目圖片首頁項目地址
菜單列表頁
聊天頁
github: https://github.com/FEA-Dven/d...
線上: https://dywsweb.com/food/login (賬號:admin, 密碼:123)
技術(shù)棧前端: react + antd
后端: nodejs + koa2
目錄介紹|---ducheng 最外層項目目錄 |---fontend 前端項目 |---app 主要項目代碼 |---api 請求api |---assets 資源管理 |---libs 包含公用函數(shù) |---model redux狀態(tài)管理 |---router 前端路由 |---style 前端樣式 |---views 前端頁面組件 |---chat 聊天頁 |---component 前端組件 |---index 訂餐系統(tǒng)首頁 |---login 登錄頁 |---App.js |---config.js 前端域名配置 |---main.js 項目主函數(shù) |---fontserver 前端服務(wù) |---config 前端服務(wù)配置 |---controller 前端服務(wù)控制層 |---router 前端服務(wù)路由 |---utils 前端服務(wù)公用庫 |---views 前端服務(wù)渲染模板 |---app.js 前端服務(wù)主函數(shù) |---node_modules |---.babelrc |---.gitignore |---gulpfile.js |---package.json |---pm2.prod.json 構(gòu)建線上的前端服務(wù)pm2配置 |---README.md |---webpack.config.js 構(gòu)建配置 |---backend 后臺項目 |---app 主要項目代碼 |---controller 控制層 |---model 模型層(操作數(shù)據(jù)庫) |---service 服務(wù)層 |---route 路由 |---validation 參數(shù)校驗 |---config 服務(wù)配置參數(shù) |---library 定義類庫 |---logs 存放日志 |---middleware 中間件 |---node_modules |---sql 數(shù)據(jù)庫sql語句在這里 |---util 公共函數(shù)庫 |---app.js 項目主函數(shù) |---package.json前端項目小結(jié) 1、搭建自己的服務(wù)
項目沒有用到腳手架,而是自己搭建前端服務(wù)器,也是koa2框架。通過koa2解析webpack配置,通過webpack打包生成資源,然后前端服務(wù)將資源引入到xtpl中達(dá)到渲染效果。
搭建自己的服務(wù)器也有好處,可以解決跨域問題,或者通過node作為中間層請求后臺服務(wù)器。嗯,本項目這些好處都沒有用到。
if (isDev) { // koawebpack模快 let 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) { //設(shè)置環(huán)境和打包資源路徑 if (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() })2、引入HappyPack快速打包
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length / 2 }); //根據(jù)CPU線程數(shù)創(chuàng)建線程池
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" }), ]),3、封裝路由組件用作權(quán)限校驗
function requireAuthentication(Component) { // 組件有已登陸的模塊 直接返回 (防止從新渲染) if (Component.AuthenticatedComponent) { return Component.AuthenticatedComponent } // 創(chuàng)建驗證組件 class AuthenticatedComponent extends React.Component { state = { login: true, } componentWillMount() { this.checkAuth(); } componentWillReceiveProps(nextProps) { this.checkAuth(); } checkAuth() { // 未登陸重定向到登陸頁面 let login = UTIL.shouldRedirectToLogin(); if (login) { window.location.href = "/food/login"; return; } this.setState({ login: !login }); } render() { if (this.state.login) { return} return "" } } return AuthenticatedComponent }
思路:這個權(quán)限校驗的組件將其他組件設(shè)為參數(shù)傳入,當(dāng)加載頁面的時候,權(quán)限校驗組件會先進(jìn)行權(quán)限校驗,當(dāng)瀏覽器沒有cookie指定的參數(shù)時,直接返回登錄頁
4、通過webpack設(shè)置主題色
{ 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" }, } } ] }5、其他
網(wǎng)頁保存cookie的用戶id,請求時放入header帶去服務(wù)器,識別哪個用戶操作
每個頁面都是零散的組件拼起來,所以組件之間的數(shù)據(jù)要處理好
后端項目小結(jié) 框架設(shè)計主要分為 controller層, service層, model層。
controller層作用于接收參數(shù),然后做參數(shù)校驗,再將參數(shù)傳入到service層做業(yè)務(wù)邏輯
service層做業(yè)務(wù)邏輯
model層調(diào)用數(shù)據(jù)庫
數(shù)據(jù)庫詳情數(shù)據(jù)庫用的是mysql
查詢數(shù)據(jù)庫用的是SQL查詢構(gòu)建器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 }, });
上面代碼用了兩個查詢構(gòu)造器區(qū)分寫入數(shù)據(jù)庫動作和讀取數(shù)據(jù)庫動作
寫一個鑒權(quán)的中間件checkHeader: async function(ctx, next) { await validator.validate( ctx.headerInput, userValidation.checkHeader.schema, userValidation.checkHeader.options ) let cacheUserInfo = await db.redis.get(foodKeyDefines.userInfoCacheKey(ctx.headerInput.fid)) cacheUserInfo = UTIL.jsonParse(cacheUserInfo); // 如果沒有redis層用戶信息和token信息不對稱,需要用戶重新登錄 if (!cacheUserInfo || ctx.headerInput.token !== cacheUserInfo.token) { throw new ApiError("food.userAccessTokenForbidden"); } await next(); }
使用鑒權(quán)中間件,拿一個路由作為例子
//引入 const routePermission = require("../../middleware/routePermission.js"); // 用戶點餐 router.post("/api/user/order", routePermission.checkHeader, userMenuController.userOrder);請求錯誤碼封裝
定義一個請求錯誤類
class ApiError extends Error { /** * 構(gòu)造方法 * @param errorName 錯誤名稱 * @param params 錯誤信息參數(shù) */ constructor(errorName, ...params) { super(); let errorInfo = apiErrorDefines(errorName, params); this.name = errorName; this.code = errorInfo.code; this.status = errorInfo.status; this.message = errorInfo.message; } }
錯誤碼定義
const defines = { "common.all": {code: 1000, message: "%s", status: 500}, "request.paramError": {code: 1001, message: "參數(shù)錯誤 %s", status: 200}, "access.forbidden": {code: 1010, message: "沒有操作權(quán)限", status: 403}, "auth.notPermission": {code: 1011, message: "授權(quán)失敗 %s", status: 403}, "role.notExist": {code: 1012, message: "角色不存在", status: 403}, "auth.codeExpired": {code: 1013, message: "授權(quán)碼已失效", status: 403}, "auth.codeError": {code: 1014, message: "授權(quán)碼錯誤", status: 403}, "auth.pargramNotExist": {code: 1015, message: "程序不存在", status: 403}, "auth.pargramSecretError": {code: 1016, message: "程序秘鑰錯誤", status: 403}, "auth.pargramSecretEmpty": {code: 1016, message: "程序秘鑰為空,請后臺配置", status: 403}, "db.queryError": { code: 1100, message: "數(shù)據(jù)庫查詢異常", status: 500 }, "db.insertError": { code: 1101, message: "數(shù)據(jù)庫寫入異常", status: 500 }, "db.updateError": { code: 1102, message: "數(shù)據(jù)庫更新異常", status: 500 }, "db.deleteError": { code: 1103, message: "數(shù)據(jù)庫刪除異常", status: 500 }, "redis.setError": { code: 1104, message: "redis設(shè)置異常", status: 500 }, "food.illegalUser" : {code: 1201, message: "非法用戶", status: 403}, "food.userHasExist" : {code: 1202, message: "用戶已經(jīng)存在", status: 200}, "food.objectNotExist" : {code: 1203, message: "%s", status: 200}, "food.insertMenuError": {code: 1204, message: "批量插入菜單失敗", status: 200}, "food.userNameInvalid": {code: 1205, message: "我不信你叫這個名字", status: 200}, "food.userOrderAlready": {code: 1206, message: "您已經(jīng)定過餐了", status: 200}, "food.userNotOrderToday": {code: 1207, message: "您今天還沒有訂餐", status: 200}, "food.orderIsEnd": {code: 1208, message: "訂餐已經(jīng)截止了,歡迎下次光臨", status: 200}, "food.blackHouse": {code: 1209, message: "別搞太多騷操作", status: 200}, "food.userAccessTokenForbidden": { code: 1210, message: "token失效", status: 403 }, "food.userHasStared": { code: 1211, message: "此評論您已點過贊", status: 200 }, "food.canNotReplySelf": { code: 1212, message: "不能回復(fù)自己的評論", status: 200 }, "food.overReplyLimit": { code: 1213, message: "回復(fù)評論數(shù)已超過%s條,不能再回復(fù)", 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: "服務(wù)器內(nèi)部錯誤", status: 500 }; }拋錯機(jī)制
當(dāng)程序判斷到有錯誤產(chǎn)生時,可以拋出錯誤給到前端,例如token不正確。
// 如果沒有redis層用戶信息和token信息不對稱,需要用戶重新登錄 if (!cacheUserInfo || ctx.headerInput.token !== cacheUserInfo.token) { throw new ApiError("food.userAccessTokenForbidden"); }
因為程序有一個回調(diào)處理的中間件,所以能捕捉到定義的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") { //數(shù)據(jù)庫連接失敗 logger.getLogger("error").fatal("mysql連接失敗", e.message, e.code); e.code = 1; e.message = "數(shù)據(jù)庫連接異常"; } if (e.code === "ER_DUP_ENTRY") { logger.getLogger("error").error("mysql操作異常", e.message, e.code); e.code = 1; e.message = "數(shù)據(jù)庫操作違反唯一約束"; } if (e.code === "ETIMEDOUT") { logger.getLogger("error").error("mysql操作異常", e.message, e.code); e.code = 1; e.message = "數(shù)據(jù)庫連接超時"; } 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; } return response.output(ctx, {}, errCode, msg, status); } };
在app.js中引入中間件
/** * 請求回調(diào)處理中間件 */ app.use(require("./middleware/requestError.js"));數(shù)據(jù)庫創(chuàng)建sql(命名不規(guī)范,請見諒)
CREATE DATABASE food_program; USE food_program; # 用戶表 CREATE TABLE t_food_user( fid int(11) auto_increment primary key COMMENT "用戶id", user_name varchar(255) NOT NULL COMMENT "用戶昵稱", password varchar(255) NOT NULL COMMENT "用戶密碼", role TINYINT(2) DEFAULT 0 COMMENT "用戶角色(項目關(guān)系,沒有用關(guān)聯(lián)表)", create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "創(chuàng)建時間", update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "修改時間", status TINYINT(2) DEFAULT 1 NOT NULL COMMENT "狀態(tài) 0:刪除, 1:正常", UNIQUE KEY `uidx_fid_user_name` (`fid`,`user_name`) USING BTREE )ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = "food 用戶表" ; CREATE TABLE t_food_menu( menu_id int(11) auto_increment primary key COMMENT "菜單id", menu_name varchar(255) NOT NULL COMMENT "菜單昵稱", type TINYINT(2) DEFAULT 0 NOT NULL COMMENT "狀態(tài) 0:每日菜單, 1:常規(guī), 2:明爐燒臘", price int(11) NOT NULL COMMENT "價格", status TINYINT(2) DEFAULT 1 NOT NULL COMMENT "狀態(tài) 0:刪除, 1:正常", create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "創(chuàng)建時間", update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "修改時間", 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 菜單列表" ; CREATE TABLE t_food_user_menu_refs( id int(11) auto_increment primary key COMMENT "記錄id", fid int(11) NOT NULL COMMENT "用戶id", menu_id int(11) NOT NULL COMMENT "菜單id" create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "創(chuàng)建時間", update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "修改時間", status TINYINT(2) DEFAULT 1 NOT NULL COMMENT "狀態(tài) 0:刪除, 1:正常", 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 = "用戶選擇什么菜單" ; CREATE TABLE t_food_system( id int(11) auto_increment primary key COMMENT "系統(tǒng)id", order_end TINYINT(2) DEFAULT 0 NOT NULL COMMENT "訂單是否截止", update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "修改時間" )ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = "都城訂單系統(tǒng)" ; CREATE TABLE t_food_comment( comment_id int(11) auto_increment primary key COMMENT "評論id", fid int(11) NOT NULL COMMENT "用戶id", content TEXT COMMENT "評論內(nèi)容", star int(11) DEFAULT 0 NOT NULL COMMENT "點贊數(shù)", create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "創(chuàng)建時間", update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "修改時間" )ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = "都城聊天表" ; CREATE TABLE t_food_reply( reply_id int(11) auto_increment primary key COMMENT "回復(fù)id", reply_fid int(11) NOT NULL COMMENT "回復(fù)用戶fid", comment_fid int(11) NOT NULL COMMENT "評論用戶fid", content TEXT COMMENT "回復(fù)內(nèi)容", create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "創(chuàng)建時間", update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "修改時間", KEY `idx_reply_fid_comment_fid` (`reply_fid`,`comment_fid`) USING BTREE )ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = "都城聊天表" ; CREATE TABLE t_food_comment_star_refs( id int(11) auto_increment primary key COMMENT "關(guān)系id", comment_id int(11) NOT NULL COMMENT "評論id", comment_fid int(11) NOT NULL COMMENT "用戶id", star_fid int(11) NOT NULL COMMENT "點贊用戶fid", create_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "創(chuàng)建時間", update_time BIGINT(20) DEFAULT 0 NOT NULL COMMENT "修改時間", UNIQUE KEY `idx_comment_id_fid_star_fid` (`comment_id`,`fid`,`star_fid`) USING BTREE )ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = "都城評論點贊關(guān)聯(lián)表" ;項目部署 前端部署 本地開發(fā)
npm run dev開發(fā)路徑
http://localhost:3006/food/login線上部署
npm install pm2 -gnpm run build
會生成一個build的文件夾,里面是線上需要用到的資源
nginx設(shè)置// /opt/food/fontend/build/ 是npm run build的文件夾路徑 location /assets/ { alias /opt/food/fontend/build/; } location / { proxy_pass http://127.0.0.1:3006/; }使用pm2開啟項目
pm2 start pm2.prod.json后端部署 本地開發(fā)
pm2 start app.js --watch
開啟 --watch 模式監(jiān)聽項目日志
線上部署pm2 start app.js
千萬不要開啟 --watch,因為沒請求一次服務(wù)會刷新產(chǎn)生數(shù)據(jù)庫和redis重連,導(dǎo)致報錯
結(jié)尾開發(fā)完這個系統(tǒng)用了三個星期趕上寒冬我就離職了...然后去面試一些公司拿這個小玩意給面試官看,HR挺滿意的,就是不知道技術(shù)官滿不滿意。
歡迎大家來交流哦~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/104596.html
摘要:不覺間,已悄然離去恍然后,正慢慢襲來。已完成一期內(nèi)容,只包含買家點餐功能,二期準(zhǔn)備做賣家及支付功能。經(jīng)過考慮和評估,我決定對這兩個選擇進(jìn)行一個折中。項目部署,及代理轉(zhuǎn)發(fā)等配置。發(fā)現(xiàn)最近,已經(jīng)對非技術(shù)類書籍少了很多興趣。 不覺間,2016已悄然離去;恍然后,2017正慢慢襲來。 又到了總結(jié)過去,展望未來的時候了,那就先總結(jié)16年的收獲和經(jīng)驗教訓(xùn),再展望17年對自己及行業(yè)的一些期望吧。 1...
摘要:不覺間,已悄然離去恍然后,正慢慢襲來。已完成一期內(nèi)容,只包含買家點餐功能,二期準(zhǔn)備做賣家及支付功能。經(jīng)過考慮和評估,我決定對這兩個選擇進(jìn)行一個折中。項目部署,及代理轉(zhuǎn)發(fā)等配置。發(fā)現(xiàn)最近,已經(jīng)對非技術(shù)類書籍少了很多興趣。 不覺間,2016已悄然離去;恍然后,2017正慢慢襲來。 又到了總結(jié)過去,展望未來的時候了,那就先總結(jié)16年的收獲和經(jīng)驗教訓(xùn),再展望17年對自己及行業(yè)的一些期望吧。 1...
摘要:平日學(xué)習(xí)接觸過的網(wǎng)站積累,以每月的形式發(fā)布。年以前看這個網(wǎng)址概況在線地址前端開發(fā)群月報提交原則技術(shù)文章新的為主。 平日學(xué)習(xí)接觸過的網(wǎng)站積累,以每月的形式發(fā)布。2017年以前看這個網(wǎng)址:http://www.kancloud.cn/jsfron... 概況 在線地址:http://www.kancloud.cn/jsfront/month/82796 JS前端開發(fā)群月報 提交原則: 技...
閱讀 1574·2021-11-23 10:01
閱讀 2969·2021-11-19 09:40
閱讀 3214·2021-10-18 13:24
閱讀 3464·2019-08-29 14:20
閱讀 2980·2019-08-26 13:39
閱讀 1275·2019-08-26 11:56
閱讀 2662·2019-08-23 18:03
閱讀 373·2019-08-23 15:35