汇通易货
开发总结(Vue+ElementUI+NodeJs+小程序)
需求
系统基本可以概括为商品销售管理系统,涵盖
销售数据展示
、商品出入库
、订单管理
、合同创建管理
、门店收银
、会员管理
、管理员管理
,小程序端只作为客户选购商品端,不需要线上实际交易。
前端
登陆
-
多角色登陆:
-
超级管理员权限、普通管理员;
-
区别:超级管理员包含普通管理员权限,并有权限创建两个角色的管理员账号、创建会员账号(user要求小程序登陆账号由商家发放,涉及到合同签订)、创建合同、冻结删除管理员账号和会员账号、设置小程序、店铺管理、消息等…
-
多角色实现:
-
登陆时在后端判断该账号属于哪级权限类账号,并返回标识给前端,前端通过返回的标识选择不同的菜单,从而实现简单的身份鉴别;
-
问题:普通管理员可通过地址栏直接跳转进超级管理员权限才有的页面,从而实施操作?
解决方案:不允许从地址栏跳转,否则强行跳转到login页面重新登陆。
-
-
-
验证码:
-
后端实现 -> 验证码
-
使用
v-html
把后端返回的svg渲染出来,并把输入的验证码发送服务器匹配(单独接口)
-
-
记住密码:
- 点击登陆时同账号密码一起发送给后端
-
登陆:
-
后端验证码用户信息真实性,成功后返回token存在Cookies里面,方便前端后面进行一下判断
-
按钮防抖这些不在细说
-
-
加密:
- 因为涉及到密码和一些敏感信息,如密码还有一些状态需要传给后端或者直接存在本地,这时显然采取密文传输或存储更安全,这次使用的是
crypto-js
加密算法,是谷歌开发的一款纯前端的加密算法,对于我这种前端工作栈的比较友好。
// 使用方法: // 1. 引入包 npm run crypto-js // 2. 封装到公共组建里面,方便后面使用 const CryptoJS = require('crypto-js'); //引用AES源码js const key = CryptoJS.enc.Utf8.parse("123412341234ABCD"); //十六位十六进制数作为密钥 const iv = CryptoJS.enc.Utf8.parse('ABCD123412341234'); //十六位十六进制数作为密钥偏移量 //解密方法 function Decrypt(word) { let encryptedHexStr = CryptoJS.enc.Hex.parse(word); let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr); let decrypt = CryptoJS.AES.decrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8); return decryptedStr.toString(); } //加密方法 function Encrypt(word) { let srcs = CryptoJS.enc.Utf8.parse(word); let encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.ciphertext.toString().toUpperCase(); } export default { Decrypt , Encrypt } // 3. 调用 // 现在组件内引入 import secret from "@/utils/secret.js"; // 加密 secret.Encrypt('ABC') // 加密ABC 打印为 D9D17358CB5858DB5A73DB98B3D28D6A // 解密 secret.Decrypt('D9D17358CB5858DB5A73DB98B3D28D6A') // 解密 打印为 ABC // TODO: // Array、Json等需要使用 JSON.stringify转为字符串后再加密,同理,解密后使用 JSON.parse转回来
- 因为涉及到密码和一些敏感信息,如密码还有一些状态需要传给后端或者直接存在本地,这时显然采取密文传输或存储更安全,这次使用的是
首页
-
主要用于展示实时数据,使用
ECharts
进行展示;drawLine() { // 基于准备好的dom,初始化echarts实例 let myChart = this.$echarts.init(document.getElementById("myChart")); // 绘制图表 myChart.setOption({ title: { text: "库存金额", }, tooltip: { trigger: "axis", axisPointer: { type: "shadow", }, }, legend: {}, grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true, }, xAxis: { type: "value", boundaryGap: [0, 0.01], }, yAxis: { type: "category", data: ["金额"], }, series: [ { name: "库存进价", type: "bar", data: this.inventoryMoney.stockMoney, }, { name: "库存价值金额", type: "bar", data: this.inventoryMoney.sellMoney, }, ], }); },
-
Excel
导出部分: 数据由后端直接设置好返回出来的
Blod
类型数据,此处直接添加按钮导出<template> <el-button type="primary" plain icon="el-icon-download" @click="goDownloadByPost" style="width: 20%; height: 50px" > 本月订单Excel下载 </el-button> </template> <script> methods: { goDownloadByPost: () => { const derive = true; const monthOrder = true; axios .post( "/home/review", { derive, monthOrder }, { responseType: "arraybuffer", } ) .then((res) => { let data = res.data; let url = window.URL.createObjectURL( new Blob([data], { type: "application/vnd.openxmlformats-officedocment.spreadsheetml.sheet", }) ); let link = document.createElement("a"); link.style.display = "none"; link.href = url; link.setAttribute("download", "本月订单.xlsx"); document.body.appendChild(link); link.click(); document.body.removeChild(link); }); }, } </script>
-
左上角信息
- 当前用户从后端获取的是管理员名字存在
LocalStorage
里面使用时直接获取的; - 登陆时间是点击登陆按钮时使用
moment
获取的时间,因为存在本地的,不会随着 刷新改变时间; - 点击退出时:
// 清除信息后退出 quit() { this.$confirm("是否确定退出系统?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }).then(() => { this.$router.push("/login"); localStorage.clear(); }).catch(() => { this.$message({ type: 'info', message: '取消退出' }); }); },
- 当前用户从后端获取的是管理员名字存在
商品列表
- 列表使用的是分页加载,每次请求十条数据,防止出现卡顿或loading时间久;
// 首次加载 created() { let pageNum = 1; // 不传也行,后端默认pageNum为 1 axios.post("/productList", { pageNum }).then((res) => { this.loading = false; this.totalNum = res.data.total[0].count; this.tableData = res.data.val; }); }, // 加载其他页同理
录入商品
- 根据会员号查询会员信息,包括合同号码,也便于这个商品的管理、归属;
- 商品图片,直接上传给服务器,并带有预览。
收银系统
-
输入会员账号进行结账操作;
-
同时显示余额、会员状态,如果会员状态处于冻结状态不允许结账操作;
-
商品数量可以更改,这里计算使用
bigJs
,并由封装的格式化小数位方法进行格式化;[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRt38MiL-1650768250139)(/Users/zhangychuan/Desktop/收银台逻辑.png)]
创建会员
- 主要点:
- 创建会员前需要已有合同支撑,这里会需要输入合同号来获取合同金额,合同金额关系到这个会员能在这里买多少钱的货品;
- 开卡人与右上角当前用户一样,但这里会把开卡人信息同样存入数据库;
管理员
- 创建
- 修改
小程序配置
- 主要设置一些小程序首页的轮播图、商家公告
后端
登陆
-
使用
jsonwebtoken
向前端发送token,并设置失效时间 -
验证码 (svg-captcha)
const svgCaptcha = require('svg-captcha') const secret = require('../../utils/secret') const verifyCode = async (ctx, next) => { // 生成验证码 let verifyCode = ctx.request.body.verifyCode ? ctx.request.body.verifyCode : 0; console.log('verifyCode---',verifyCode); if(!verifyCode) { var captcha = svgCaptcha.create({ size: 4, fontSize: 50, width: 120, height: 35, noise: 2, ignoreChars: 'Oo01i', background: '#cc9966' }); // 保存生成的验证码结果 // 完善后台验证验证码 // ctx.session.code = captcha.text ctx.session.verifyVal = (captcha.text).toUpperCase() console.log('验证码中的session---',ctx.session.verifyVal); // const verifyCode = captcha.text; // 设置响应头 ctx.response.type = 'image/svg+xml'; const captchaData = captcha.data; const captchaText = secret.Encrypt(ctx.session.verifyVal) ctx.body = { captchaData, captchaText } }else if(verifyCode == ctx.session.verifyVal) { console.log('验证通过啦---',verifyCode,'--====',ctx.session.verifyVal); }else { console.log('验证不通过啊','verifyCode====',verifyCode,'----ctx.session.verifyVal===',ctx.session); } }
-
导出订单信息 (node-xlsx)
// 订单 const getData = require('../../models/getData'); const moment = require('moment'); const xlsx = require("node-xlsx"); const fs = require("fs"); // const stringRandom = require('string-random'); const nowTime = moment().format("YYYY-MM-DD HH:mm:ss"); console.log(nowTime); const reviewHome = async (ctx, next) => { const home = ctx.request.body.home ? true : false; // 创建订单 const derive = ctx.request.body.derive ? true : false; // 导出 // 当月新增订单数量 const nowMonthNum = await getData(`SELECT * FROM order_info WHERE DATE_FORMAT( createdTime, '%Y%m' ) = DATE_FORMAT( CURDATE( ) , '%Y%m' )`); // 本年订单 // const nowYearOrder = await getData(`select * from order_info where YEAR('${nowTime}')=YEAR(NOW())`); const nowYearOrder = await getData(`select * from order_info where QUARTER('${nowTime}')=QUARTER(now());`); if (home) { console.log('创建---'); const outPriceTotal = await getData(`SELECT sum(outPrice) as sum FROM store`); const inPriceTotal = await getData(`SELECT sum(inPrice) as sum FROM store`); // 每月销售总金额 const monthToMoney = await getData(`select orderState,DATE_FORMAT(createdTime,'%Y-%m') month ,SUM(priceTotal) total from order_info group by orderState,month`); // 每月进价统计 const monthInMoney = await getData(`select productState,DATE_FORMAT(inDB,'%Y-%m') month ,SUM(inPrice) total from store group by productState,month`); // 当月新签合同数量 const nowMonthContract = await getData(`SELECT * FROM contract WHERE DATE_FORMAT( createTime, '%Y%m' ) = DATE_FORMAT( CURDATE( ) , '%Y%m' )`) // 当月销售总金额 const saleTotal = await getData(`SELECT priceTotal FROM order_info WHERE DATE_FORMAT( createdTime, '%Y%m' ) = DATE_FORMAT( CURDATE( ) , '%Y%m' )`); console.log(saleTotal); if (outPriceTotal.errno || inPriceTotal.errno) { ctx.body = { statusCode: 502, message: '网络异常,请重新录入。' } } else if (outPriceTotal[0] && inPriceTotal[0]) { console.log('---求和成功---'); ctx.body = { statusCode: 200, message: '订单创建成功', data: { outTotal: outPriceTotal, intotal: inPriceTotal }, // yesterday: yesterday, // dataNow: dataNow, // data7: data7 monthToMoney: monthToMoney, monthInMoney: monthInMoney, nowMonthNum: nowMonthNum.length, nowMonthContract: nowMonthContract.length, saleTotal: saleTotal } } } else if (derive) { // 传monthOder就是找月订单,否则就是找年订单 let data = []; let title = ['订单ID', '客户名称', '订单商品','单价', '订单状态', '商品件数', '小计', '下单时间', '付款时间']; data.push(title); // console.log(nowYearOrder); const orderData = ctx.request.body.monthOrder ? nowMonthNum : nowYearOrder; orderData.forEach(item => { JSON.parse(item.productList).forEach(res => { let arrF = []; arrF.push(item.orderID); arrF.push(item.userName); arrF.push(res.name); arrF.push(res.price); arrF.push(item.orderState === 'S' ? '已完成' : '未完成'); arrF.push(res.num); arrF.push(res.total); arrF.push(item.createdTime); arrF.push(item.settleTime); data.push(arrF) }) }) console.log(data) let buffer = xlsx.build([ { name: 'sheet1', data } ]); ctx.body = buffer; } } module.exports = { reviewHome }
-
其他接口或逻辑难度正常,比较常用,不再解释上传
小程序
部署:
- 每次进入小程序要先进入首页,不允许直接跳到登陆页面,否则升审核不通过;
- 只有公司主体才允许有购物车、订单信息这些可能存在交易的功能块;
- 域名只允许https协议。
测试时展示图片:
部署
使用nginx代理
后端使用pm2打包运行
服务器、数据库使用阿里云,找代理拿的折扣,享受官方优惠后再打折,比较便宜实惠
使用到的npm包
-
axios
-
加密
crypto-js
-
时间
moment
:// 解析、校验、操作、显示日期和时间的 JavaScript 工具库。 // 本项目主要用户获取并直接格式化时间,非常方便简洁 moment().format('MMMM Do YYYY, h:mm:ss a'); // 四月 22日 2022, 2:38:14 下午 moment().format('dddd'); // 星期五 moment().format("MMM Do YY"); // 4月 22日 22 moment().format('YYYY [escaped] YYYY'); // 2022 escaped 2022 moment().format(); // 2022-04-22T14:38:26+08:00
-
Big,js
// 用于结局js计算不精确的问题 /* plus 加法 minus 减法 times 乘法 div 除法 */
-
echarts
-
element-ui
-
lodash
-
svg-captcha
-
node-xlsx
总结
这次项目类似电商后台,但也有很大的需求出入不同,从设计总体逻辑到每个细节点的小逻辑,从ps找小程序icon到主题色选取都是很耽误时间的,所以开发周期将近两个月。其中也走过一些弯路,比如需求中商品录入功能就是不是循规蹈矩的,在录入前需要有很多逻辑还需要处理,还有收银台等等;
总体来说还是比较简单的一个系统,感谢自己这段每晚9-11点的时间🙏。
还有很多不足,希望大家批评指正、一起交流啊! qq: 374521958