前端

前端使用VUE的element组件,Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库。

第一个问题:前端excel的导入导出

因业务需要,需要对商品进行批量的导入导出,信息为商品信息,供货商或者运营部门可进行excel的批量导入,而不是一个个的输入商品信息。

具体从零开始,参考了如下文章:

https://www.cnblogs.com/guwufeiyang/p/13245875.html

使用的两个js

Blob.js

Export2Excel.js

引入之后Export2Excel.js可能有点问题,相比较原版,头部的引入:

第一步:需要安装三个依赖
npm install -S file-saver xlsx (这里其实安装了2个依赖)
npm install -D script-loade

使用了require(‘script-loader!file-saver’);方式进行。

fileReader.onload 函数 要有fileReader.readAsBinaryString(files.raw);方法。

本次实现的上传excel并不是将excel传到服务器上进行解析,而是将excel的内容在js中解析,最后将内容发送到后台继续处理。

所以excel的内容映射放在了JS端进行了操作。

excel的导出则是反向操作,将excel的表头与内容规定好,用前端已有的数据进行填充即可。

未解决问题:上传的时候,有图片上传,excel将填写图片地址,但地址之后上传之后才能生成。

第二个问题:文件下载

由于mall系统的请求返回数据都为json,且原项目是没有文件下载功能,当使用json请求时,返回的数据不能解析,使用string传输数据,前端也无法正常反转成二进制,保存的文件总是提示文件已损坏。所以请求使用了http原生的接收request,返回response,在本项目中,在respone拦截器中将返会的原生http的response分出来进行结果判断。

下载商品模板excel

通过

String url = this.getClass().getResource("/").getPath() + "file/" + fileName;

得到文件在服务器上的地址。将contentType返回值设置成application/octet-stream,则前端点击下载即可。

String contentType = "application/octet-stream;charset=utf-8";

前端:

 window.URL.createObjectURL(new Blob([res]))

记得将请求的responseType: ‘arraybuffer’设置好。之后生成a元素触发点击。然后再删除a元素。

/**
 * 下载文件调用
 * @param 接口返回数据 文件名
 */

export function downloadFile(res, fileName) {
  if (!res) {
    return
  }
  /*res并不是array*/
  if (window.navigator.msSaveBlob) {  // IE以及IE内核的浏览器
    try {
      window.navigator.msSaveBlob(res, fileName)  // res为接口返回数据,这里请求的时候已经处理了,如果没处理需要在此之前自行处理var data = new Blob([res.data]) 注意这里需要是数组形式的,fileName就是下载之后的文件名
      // window.navigator.msSaveOrOpenBlob(res, fileName);  //此方法类似上面的方法,区别可自行百度
    } catch (e) {
      console.log(e)
    }
  } else {
    let url = window.URL.createObjectURL(new Blob([res]))
    let link = document.createElement('a')
    link.style.display = 'none'
    link.href = url
    link.setAttribute('download', fileName)// 文件名
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link) // 下载完成移除元素
    window.URL.revokeObjectURL(url) // 释放掉blob对象
  }

第三个问题:输入框比较小,提示信息实现

在商品规格页面,当输入:定价、销售价、成本价、规格编码、规格条码时,由于本身输入框长度不够,当输入字段过长会导致看不到前面输入的是什么了,所以设计了一个Popover 弹出框进行实时展示输入的内容:

只不过是针对input组件:

<el-popover placement="right" title="规格编码" width="200" trigger="hover" :content="scope.row.specificationCode">
                <el-input v-model="scope.row.specificationCode" slot="reference"></el-input>
 </el-popover>

但这又带来另外的问题,因为设置的是trigger=”hover” ,所以当鼠标扫过的时候就会弹出提示,针对离得很近的表格输入来说,会影响到下一个表格的输入。所以后来删除了此功能。

第四个问题:上传照片与预览图片

还是由于mall系统的请求返回数据都为json,导致上传图片与预览图片数据不正确。原系统使用的是阿里云的oss所以只需要上传充成功后返回url即可。

问题解决吸取excel的经验,直接写原生的http请求,将照片数据直接传到后台,然后后台返回预览照片的url。

问题是前端请求的时候需要认证值,myHeaders: {Authorization: getToken()}, 使用 multiUpload时,可以设置请求头:

<el-upload
  :action="url"
  list-type="picture-card"
  :file-list="fileList"
  :before-upload="beforeUpload"
  :on-remove="handleRemove"
  :on-success="handleUploadSuccess"
  :on-preview="handlePreview"
  :limit="maxCount"
  :headers="myHeaders"
  :on-exceed="handleExceed"
>
  <i class="el-icon-plus"></i>
</el-upload>

则可以正常上传照片。

问题在于预览的展示问题。当上传照片成功后,保存照片到服务器上(为了对接有赞的时候对照片进行压缩)。之后前端请求照片地址进行展示。

当返回例如http://localhost:8080/product/update/getPhotoFile/1599200758115.jpg地址的时候,会被后台拦截,因为没有设置请求头的认证字段,返回的是封装好的错误提示信息,是以application/json 格式返回,导致前台无法展示照片。并且请求不会到controller这一层,mall的security会拦截请求,所以需要设置请求白名单,并且后端也需要原生http接受请求,正确设置返回类型。

增加白名单

secure:
  ignored:
    urls: #安全路径白名单
      - /swagger-ui.html
      - /swagger-resources/**
      - /swagger/**
      - /**/v2/api-docs
      - /**/*.js
      - /**/*.css
      - /**/*.png
      - /**/*.ico
      - /webjars/springfox-swagger-ui/**
      - /actuator/**
      - /druid/**
      - /admin/login
      - /admin/register
      - /admin/info
      - /admin/logout
      - /minio/upload
      - /**/*.jpg
      - /**/*.gif
      - /**/*.bmp

添加照片jpg gif bmp的过滤,注意,白名单是大小写敏感的。JPG与jpg不同。

第五个问题:tinymce去除关闭/刷新网页时弹出对话框

mall的富文本使用了插件tinymce,但在el的流程中,最后一步添加富文本后,点击提交会弹出有未保存的数据。虽然数据是已经可以保存到数据库中,但可设置取消弹窗。

autosave_ask_before_unload: false

第六个问题:数值的校验问题

针对输入的金额数据,需要进行格式化,非金额直接转换为0,金额则转换为0.00格式

参考地址:http://www.qdxw.net/xwhtml/813.html

使用进位方式,如果是0.006,则返回0.01;

export function number_format(number, decimals, dec_point, thousands_sep) {
  /*数值转换函数
      * 参数说明:
      * number:要格式化的数字
      * decimals:保留几位小数
      * dec_point:小数点符号
      * thousands_sep:千分位符号 传空为不设置千分位
      * */
  number = (number + '').replace(/[^0-9+-Ee.]/g, '');
  var n = !isFinite(+number) ? 0 : +number,
    prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
    sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
    dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
    s = '',
    toFixedFix = function (n, prec) {
      var k = Math.pow(10, prec);
      return '' + Math.ceil(n * k) / k;
    };

  s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
  var re = /(-?\d+)(\d{3})/;
  while (re.test(s[0])&&sep!="") {
    s[0] = s[0].replace(re, "$1" + sep + "$2");
  }

  if ((s[1] || '').length < prec) {
    s[1] = s[1] || '';
    s[1] += new Array(prec - s[1].length + 1).join('0');
  }
  return s.join(dec);
}

更改了添加千分位的循环地方,当不想要千分的时候,传入thousands_sep=“”;但原方法会在数值超过千位时会无限循环,除非使用千分位。我在函数中的循环部分添加参数判断。如果传入为空字符串,则不添加千分位。

第一种使用方法:

当input触发函数 onblur时,将输入框的数值进行替换为返回值。

问题:

由于是在新增商品的商品规格中使用,在table中对每一行数据进行校验,一开始选择使用form的校验进行。但发现在写法上商品规格属于form的item的table。所以嵌套层过多,网上教程只涉及到循环form嵌套table,每一行属于item时,才可以。

最终每一行数据只能onblur才能校验以及重新赋值。

这时候遇到了vue的问题,Vue中数组元素被替换,页面没有动态展示,vue无法监测到,需要使用Vue.set实现更新。

后端

第一个问题:读取文件上传前端

将获取到的excel文件传到前端,使用原生http请求:

public void getFile(HttpServletRequest request, HttpServletResponse response, String fileName)
    {
        /* this.getClass().getResource("/").getPath() 获取程序在服务器中的位置信息 */
        LOGGER.info("下载商品的导入表格excel 入参fileName:" + fileName);
        String url = this.getClass().getResource("/").getPath() + "file/" + fileName;
        String realName = fileName; // 需要的文件名字
        String downLoadPath = url; // 这个参数表示文件在服务器的存储路径
        String contentType = "application/octet-stream;charset=utf-8";
        try
        {
            request.setCharacterEncoding("UTF-8");
            BufferedInputStream bis = null;
            BufferedOutputStream bos = null;
            long fileLength = new File(downLoadPath).length();
            response.setContentType(contentType);
            response.setHeader("Content-disposition", "attachment; filename=" + new String(realName.getBytes("utf-8"), "ISO8859-1"));
            response.setHeader("Content-Length", String.valueOf(fileLength));
            bis = new BufferedInputStream(new FileInputStream(downLoadPath));
            bos = new BufferedOutputStream(response.getOutputStream());
            byte[] buff = new byte[2048];
            int bytesRead;
            while (-1 != (bytesRead = bis.read(buff, 0, buff.length)))
            {
                bos.write(buff, 0, bytesRead);
            }
            bis.close();
            bos.close();
        }
        catch (Exception e)
        {
            LOGGER.error("读取文件失异常E" + e);
            response.setStatus(500);
        }
    }

照片文件的读取,设置response.setContentType(“image/jpg”):

/**
 * 展示照片
 *
 * @param request
 * @param response
 * @param fileName
 */
@Override
public void getPhotoFile(HttpServletRequest request, HttpServletResponse response, String fileName)
{
    FileInputStream fis = null;
    response.setContentType("image/jpg");
    try
    {
        OutputStream out = response.getOutputStream();
        String filePath = this.getClass().getResource("/").getPath() + "photos/";
        File file = new File(filePath, fileName);
        fis = new FileInputStream(file);
        byte[] b = new byte[fis.available()];
        fis.read(b);
        out.write(b);
        out.flush();
    }
    catch (Exception e)
    {
        response.setStatus(500);
        LOGGER.error("getPhotoFile异常" + e);
    }
    finally
    {
        if (fis != null)
        {
            try
            {
                fis.close();
            }
            catch (IOException e)
            {
                LOGGER.error("getPhotoFile关闭文件流异常" + e);
            }
        }
    }
}

第二个问题:在contrller之外的工具类中使用mapper

初始化static 本类。使用PostConstruct 初始化本类,之后在方法中使用该静态方法调用mapper;

public class YouzanAPI
{
    public static YouzanAPI Utils;
    
    @PostConstruct
    public void init()
    {
        Utils = this;
    }
    @Autowired
    private PmsSkuStockMapper   pmsSkuStockMapper;
    ......
        /**
     * 组装产品 将产品信息组装成有赞的格式
     *
     * @param tempP 需要组装的产品
     * @return
     */
    public YouzanRetailOpenSpuCreateResult createSpu(@NotNull PmsProduct tempP) throws Exception
    {
        ...
        List<PmsSkuStock> pmsSkuStockList = Utils.pmsSkuStockMapper.selectByExample(pmsSkuStockExample);
        ...
    }
    ......
}   
    

暂时整理这么多,end.