汇通易货 开发总结

汇通易货 开发总结(Vue+ElementUI+NodeJs+小程序)

需求

系统基本可以概括为商品销售管理系统,涵盖销售数据展示商品出入库 订单管理合同创建管理门店收银会员管理管理员管理,小程序端只作为客户选购商品端,不需要线上实际交易。

前端

登陆

请添加图片描述

  • 多角色登陆:

    1. 超级管理员权限、普通管理员;

    2. 区别:超级管理员包含普通管理员权限,并有权限创建两个角色的管理员账号、创建会员账号(user要求小程序登陆账号由商家发放,涉及到合同签订)、创建合同、冻结删除管理员账号和会员账号、设置小程序、店铺管理、消息等…

    3. 多角色实现:

      • 登陆时在后端判断该账号属于哪级权限类账号,并返回标识给前端,前端通过返回的标识选择不同的菜单,从而实现简单的身份鉴别;

      • 问题:普通管理员可通过地址栏直接跳转进超级管理员权限才有的页面,从而实施操作?
        解决方案:不允许从地址栏跳转,否则强行跳转到login页面重新登陆。

  • 验证码:

    1. 后端实现 -> 验证码

    2. 使用 v-html 把后端返回的svg渲染出来,并把输入的验证码发送服务器匹配(单独接口)

  • 记住密码:

    1. 点击登陆时同账号密码一起发送给后端
  • 登陆:

    1. 后端验证码用户信息真实性,成功后返回token存在Cookies里面,方便前端后面进行一下判断

    2. 按钮防抖这些不在细说

  • 加密

    1. 因为涉及到密码和一些敏感信息,如密码还有一些状态需要传给后端或者直接存在本地,这时显然采取密文传输或存储更安全,这次使用的是 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>
    
  • 左上角信息

    1. 当前用户从后端获取的是管理员名字存在 LocalStorage 里面使用时直接获取的;
    2. 登陆时间是点击登陆按钮时使用 moment获取的时间,因为存在本地的,不会随着 刷新改变时间;
    3. 点击退出时:
    // 清除信息后退出
        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)]

    创建会员

在这里插入图片描述

  • 主要点:
    1. 创建会员前需要已有合同支撑,这里会需要输入合同号来获取合同金额,合同金额关系到这个会员能在这里买多少钱的货品;
    2. 开卡人与右上角当前用户一样,但这里会把开卡人信息同样存入数据库;

管理员

在这里插入图片描述

  • 创建
  • 修改

小程序配置

在这里插入图片描述

  • 主要设置一些小程序首页的轮播图、商家公告

后端

登陆

  1. 使用 jsonwebtoken向前端发送token,并设置失效时间

  2. 验证码 (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
    }
    
  • 其他接口或逻辑难度正常,比较常用,不再解释上传

小程序

部署:

  1. 每次进入小程序要先进入首页,不允许直接跳到登陆页面,否则升审核不通过;
  2. 只有公司主体才允许有购物车、订单信息这些可能存在交易的功能块;
  3. 域名只允许https协议。

测试时展示图片:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iLqB972o-1650768250140)(/Users/zhangychuan/Library/Application Support/typora-user-images/image-20220424102711117.png)]

部署

使用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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尤山海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值