博客十二:基本框架概述(上)

一、一对多、多对多分页

使用mybatis plus 进行分页的时候,是无法进行一对多、多对多的分页的。最主要的原因是因为,该框架无法清楚count的依据是什么,以哪个表算出来的行数为准,但是我们所有的分页格式已经统一好使用IPage对象了,那么该如何适配一对多、多对多分页呢?

PageAdapter

使用分页时,前端传入的数据统一格式为current当前页,size每页大小。而我们在数据库中要将这两个数据变更为从第几行到第几行,所以我们需要简单的适配一下:

java 复制代码
@Data
public class PageAdapter{

    private int begin;

    private int end;

    public PageAdapter(Page page) {
        int[] startEnd = PageUtil.transToStartEnd((int) page.getCurrent(), (int) page.getSize());
        this.begin = startEnd[0];
        this.end = startEnd[1];
    }
}

Count

在使用mybatis plus 进行分页的时候,该工具会自动为我们编写count的sql,而一对多进行分页时如:

1个订单有5个订单项,在使用mybatis plus 生成的count sql 会认为每行都是一条数据,导致最后认为会有5条订单信息,实际上应该只有1条订单信息。这个时候我们必须自己手写count sql,并区分records sql

具体例子可以查看OrderServiceImpl

java 复制代码
@Override
public IPage<Order> pageOrdersDetialByOrderParam(Page<Order> page, OrderParam orderParam) {
    page.setRecords(orderMapper.listOrdersDetialByOrderParam(new PageAdapter(page), orderParam));
    page.setTotal(orderMapper.countOrderDetial(orderParam));
    return page;
}

二、分布式锁

在小程序登陆的时候,在MiniAppAuthenticationProvider中我们看到这样一行代码

java 复制代码
yamiUserDetailsService.insertUserIfNecessary(appConnect);

这便是商城用户创建的代码,在YamiUserServiceImpl#insertUserIfNecessary()方法中,有一个这样的注解

java 复制代码
@RedisLock(lockName = "insertUser", key = "#appConnect.appId + ':' + #appConnect.bizUserId")

这里便用了分布式锁,为什么我们要在这里使用锁?分布式锁又是什么?

  • 由于用户是通过登录直接注册的,如果一个用户在不刻意之间,又或者前端写的东西有点问题,这就会导致整个系统创建了两个相同的用户,这是非常危险的事情,所以创建用户这里必须加锁。
  • 至于为什么使用分布式锁,是因为我们虽然没有用上spring cloud、dubbo之类的东西,实际上我们也是希望我们的商城可以多实例部署的,也就是可以搞分布式的。因此用了分布式锁

分布式锁,简单来说就是锁,而且还是适合分布式环境的。分布式说起来也很奇怪,要是有什么不能共享的东西,那就抽出来共享。比如本地数据缓存不能共享,那么就抽出一个如redis之类的东西,进行共享。session不能共享,那么就将session抽出来,丢到redis之类的东西,又能共享了。

锁不能共享,同样可以丢一个标记到redis,由于redis是单线程的,所以也不用担心redis的线程安全的问题。这个标记就是一个锁的标记,那样你就实现了分布式锁...

我们看回@RedisLock 该类,里面有个expire()方法

java 复制代码
    /**
     * 过期毫秒数,默认为5000毫秒
     *
     * @return 锁的时间
     */
    int expire() default 5000;

由于网络稳定、宕机等各种原因,分布式锁,必须要有过期时间,否则锁无法释放的话,会阻塞一片的实例。

实现一个简单的分布式锁注解

由于自己去实现redis的分布式锁,是比较困难的问题,还要考虑redis复制,宕机之类的问题,所以我们使用一个比较优秀的开源项目 redisson来实现我们的分布式锁

@RedisLock所注解的方法,会被 RedisLockAspect 进行切面管理,代码如下:

java 复制代码
    @Around("@annotation(redisLock)")
    public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
        String spel = redisLock.key();
        String lockName = redisLock.lockName();
        // redissonClient 也就是通过redisson 进行对锁管理
        RLock rLock = redissonClient.getLock(getRedisKey(joinPoint,lockName,spel));

        rLock.lock(redisLock.expire(),redisLock.timeUnit());

        Object result = null;
        try {
            //执行方法
            result = joinPoint.proceed();

        } finally {
            rLock.unlock();
        }
        return result;
    }

识别spel表达式

@RedisLock(lockName = "insertUser", key = "#appConnect.appId + ':' + #appConnect.bizUserId")#appConnect.appId 也仅仅是表示一串字符串而已,而能将其变成表达式,需要一定的转换SpelUtil.parse

java 复制代码
    /**
     * 支持 #p0 参数索引的表达式解析
     * @param rootObject 根对象,method 所在的对象
     * @param spel 表达式
     * @param method ,目标方法
     * @param args 方法入参
     * @return 解析后的字符串
     */
    public static String parse(Object rootObject,String spel, Method method, Object[] args) {
        if (StrUtil.isBlank(spel)) {
            return StrUtil.EMPTY;
        }
        //获取被拦截方法参数名列表(使用Spring支持类库)
        StandardReflectionParameterNameDiscoverer standardReflectionParameterNameDiscoverer = new StandardReflectionParameterNameDiscoverer();
        String[] paraNameArr = standardReflectionParameterNameDiscoverer.getParameterNames(method);
        if (ArrayUtil.isEmpty(paraNameArr)) {
            return spel;
        }
        //使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,standardReflectionParameterNameDiscoverer);
        //把方法参数放入SPEL上下文中
        for (int i = 0; i < paraNameArr.length; i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        return parser.parseExpression(spel).getValue(context, String.class);
    }

同时我们也害怕redis的key发生冲突,所以会对key加上一些统一的前缀:

redis 锁的key能够识别spel 表达式,并且不和其他方法的锁名称或缓存名称重复

java 复制代码
/**
 * 将spel表达式转换为字符串
 * @param joinPoint 切点
 * @return redisKey
 */
private String getRedisKey(ProceedingJoinPoint joinPoint,String lockName,String spel) {
   Signature signature = joinPoint.getSignature();
   MethodSignature methodSignature = (MethodSignature) signature;
   Method targetMethod = methodSignature.getMethod();
   Object target = joinPoint.getTarget();
   Object[] arguments = joinPoint.getArgs();
   return REDISSON_LOCK_PREFIX + lockName + StrUtil.COLON + SpelUtil.parse(target,spel, targetMethod, arguments);
}

三、对xss攻击的防御

身为服务器的开发者,我们是无法相信用户输入的任何东西的。比如:金额不能从前端传过来,使用会失效的token等。当然,用户除了会传入一些假数据,也会传入一些假的脚本,比较出名的就是xss攻击

网上有很多说解决xss攻击的方法,有很多都是和前端有关,而实际上,在后台这最后一个防御当中,是最为重要的。

在mall4j这个项目里面,使用了一个过滤器 XssFilter

复制代码
public class XssFilter implements Filter {
    Logger logger = LoggerFactory.getLogger(getClass().getName());

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException{
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        
        logger.info("uri:{}",req.getRequestURI());
        // xss 过滤
        chain.doFilter(new XssWrapper(req), resp);
    }
}

主要是通过 new XssWrapper(req) 这个对象进行一系列的过滤,而 XssWrapper 是通过Jsoup进行用户输入的一系列过滤。毕竟专业的事情要交给专业的人来搞定。就此,我们通过简单的设置就完成了对xss攻击的防御。

java 复制代码
public class XssWrapper extends HttpServletRequestWrapper {
    /**
     * Constructs a request object wrapping the given request.
     *
     * @param request The request to wrap
     * @throws IllegalArgumentException if the request is null
     */
    public XssWrapper(HttpServletRequest request) {
        super(request);
    }

    /**
     * 对数组参数进行特殊字符过滤
     */
    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) {
            return null;
        }
        int count = values.length;
        String[] encodedValues = new String[count];
        for (int i = 0; i < count; i++) {
            encodedValues[i] = cleanXSS(values[i]);
        }
        return encodedValues;
    }

    /**
     * 对参数中特殊字符进行过滤
     */
    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        if (StrUtil.isBlank(value)) {
            return value;
        }
        return cleanXSS(value);
    }

    /**
     * 获取attribute,特殊字符过滤
     */
    @Override
    public Object getAttribute(String name) {
        Object value = super.getAttribute(name);
        if (value instanceof String && StrUtil.isNotBlank((String) value)) {
            return cleanXSS((String) value);
        }
        return value;
    }

    /**
     * 对请求头部进行特殊字符过滤
     */
    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        if (StrUtil.isBlank(value)) {
            return value;
        }
        return cleanXSS(value);
    }

    private String cleanXSS(String value) {
        return XssUtil.clean(value);
    }
}

这里面最主要的方法就是XssUtil.clean(value) -> Jsoup.clean(content, "", WHITE_LIST, OUTPUT_SETTINGS) 这面最总要的是有个白名单列表 WHITE_LIST 来自,我们仔细观察白名单列表会发现这里面是部分携带html的部分标签进入,从而防止xss攻击

java 复制代码
new Whitelist().addTags(
                        "a", "b", "blockquote", "br", "caption", "cite", "code", "col",
                        "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
                        "i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong",
                        "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u",
                        "ul")
    
                .addAttributes("a", "href", "title")
                .addAttributes("blockquote", "cite")
                .addAttributes("col", "span", "width")
                .addAttributes("colgroup", "span", "width")
                .addAttributes("img", "align", "alt", "height", "src", "title", "width")
                .addAttributes("ol", "start", "type")
                .addAttributes("q", "cite")
                .addAttributes("table", "summary", "width")
                .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width")
                .addAttributes(
                        "th", "abbr", "axis", "colspan", "rowspan", "scope",
                        "width")
                .addAttributes("ul", "type")

                .addProtocols("a", "href", "ftp", "http", "https", "mailto")
                .addProtocols("blockquote", "cite", "http", "https")
                .addProtocols("cite", "cite", "http", "https")
                .addProtocols("img", "src", "http", "https")
                .addProtocols("q", "cite", "http", "https")

四、文件的上传和下载

上传下载

我们对文件上传进行了分别封装了多个组件:

  • 单图片上传(替换图片):src\components\pic-upload

  • 多图片上传:src\components\mul-pic-upload

  • 文件上传:src\components\file-upload

上述这些文件上传,都是基于el-upload进行封装

单图片上传

在商品分类这个模块的弹框中可以找到单图片上传的例子,对应vue代码位置:src\views\modules\category-add-or-update.vue

html:

html 复制代码
<pic-upload v-model="dataForm.pic"></pic-upload>

js:

javascript 复制代码
import PicUpload from '@/components/pic-upload'
export default {
  data () {
    return {
      dataForm: {
        pic: ''
      }
  },
  components: {
    PicUpload
  }
}

这里的文件上传使用起来非常简单,只需要将最终文件上传完成后的路径进行双向绑定即可

多图片上传

在商品发布这个模块的中可以找到多图片上传的例子,对应vue代码位置:src\views\modules\category-add-or-update.vue

html:

html 复制代码
<mul-pic-upload v-model="dataForm.imgs" />

js:

javascript 复制代码
import MulPicUpload from '@/components/mul-pic-upload'
export default {
  data () {
    return {
      dataForm: {
        imgs: ''
      }
  },
  components: {
    MulPicUpload
  }
}

这里的文件上传使用起来也非常简单,最后返回的数据,为以逗号分隔的图片路径连接的字符串

服务端代码

直接的文件上传的例子与多图片上传的例子类似,这里便不一一举例了。

我们可以查看三个文件上传的源码,都有那么两句话:action="$http.adornUrl('/admin/file/upload/element')" :headers="{Authorization: $cookie.get('Authorization')}",其中由于规定后台所有请求都需要通过 spring security的授权,所以需要携带通用请求头headers,而action则是对应后台服务器的路径

我们查看后台FileController 这里对文件上传的接口进行了统一的管理:

java 复制代码
@RestController
@RequestMapping("/admin/file")
public class FileController {
   
   @Autowired
   private AttachFileService attachFileService;
   
   @PostMapping("/upload/element")
   public ServerResponseEntity<String> uploadElementFile(@RequestParam("file") MultipartFile file) throws IOException{
      if(file.isEmpty()){
            return ServerResponseEntity.success();
        }
      String fileName = attachFileService.uploadFile(file.getBytes(),file.getOriginalFilename());
        return ServerResponseEntity.success(fileName);
   }

   
}

同时我们查看attachFileService 的实现类,可以知道该文件上传是通过七牛云进行实现的

java 复制代码
@Service
public class AttachFileServiceImpl extends ServiceImpl<AttachFileMapper, AttachFile> implements AttachFileService {

    @Autowired
    private AttachFileMapper attachFileMapper;

    @Autowired
    private UploadManager uploadManager;

    @Autowired
    private BucketManager bucketManager;
    @Autowired
    private Qiniu qiniu;

    @Autowired
    private Auth auth;

    public final static String NORM_MONTH_PATTERN = "yyyy/MM/";

   @Override
   public String uploadFile(byte[] bytes,String originalName) throws QiniuException {
      String extName = FileUtil.extName(originalName);
      String fileName =DateUtil.format(new Date(), NORM_MONTH_PATTERN)+ IdUtil.simpleUUID() + "." + extName;


      AttachFile attachFile = new AttachFile();
      attachFile.setFilePath(fileName);
      attachFile.setFileSize(bytes.length);
      attachFile.setFileType(extName);
      attachFile.setUploadTime(new Date());
      attachFileMapper.insert(attachFile);

      String upToken = auth.uploadToken(qiniu.getBucket(),fileName);
       Response response = uploadManager.put(bytes, fileName, upToken);
       Json.parseObject(response.bodyString(),  DefaultPutRet.class);
      return fileName;
   }
}

在这里面注入了非常多的七牛云的配置,而配置文件的来源,来自

java 复制代码
@Configuration
public class FileUploadConfig {

   
   @Autowired
   private Qiniu qiniu;
   
    /**
     * 华南机房
     */
    @Bean
    public com.qiniu.storage.Configuration qiniuConfig() {
        return new com.qiniu.storage.Configuration(Zone.zone2());
    }

    /**
     * 构建一个七牛上传工具实例
     */
    @Bean
    public UploadManager uploadManager() {
        return new UploadManager(qiniuConfig());
    }

    /**
     * 认证信息实例
     * @return
     */
    @Bean
    public Auth auth() {
        return Auth.create(qiniu.getAccessKey(), qiniu.getSecretKey());
    }
    
    /**
     * 构建七牛空间管理实例
     */
    @Bean
    public BucketManager bucketManager() {
        return new BucketManager(auth(), qiniuConfig());
    }
}

注册七牛云账号

现在很少上传文件到本地了,一般都是上传到oss,我们这里选择七牛云存储

修改后台配置

平台端(vue)修改文件.env.production(生产环境)/ .env.development(开发环境)

里面的VUE_APP_BASE_API为api接口请求地址, VUE_APP_RESOURCES_URL为静态资源文件url

复制代码
// api接口请求地址
VUE_APP_BASE_API = 'http://127.0.0.1:8085'
// 静态资源文件url
VUE_APP_RESOURCES_URL = 'https://img.mall4j.com/'
相关推荐
小白学大数据12 分钟前
爬取汽车之家评论并利用NLP进行关键词提取
人工智能·自然语言处理·汽车
biubiubiu070618 分钟前
AI中的Prompt
人工智能·prompt
AIGC_ZY19 分钟前
RAG 技术详解:结合检索与生成的智能问答新范式
人工智能
1 小时前
前端工程师必备:5个改变开发效率的 MCP Server
人工智能
Ai尚研修-贾莲1 小时前
最新Transformer模型及深度学习前沿技术应用
人工智能·深度学习·transformer·生成式模型·图神经网络·注意力机制·目标检测算法
weixin_453253651 小时前
机器学习----模型评价与优化
人工智能·机器学习
DeepSeek忠实粉丝1 小时前
Deepseek篇--阿里QwQ-325b性能比肩Deepseek满血版
人工智能·程序员·llm
jndingxin1 小时前
OpenCV CUDA模块图像变形------对图像进行 尺寸缩放(Resize)操作函数resize()
人工智能·opencv·计算机视觉
清醒的兰1 小时前
OpenCV 多边形绘制与填充
图像处理·人工智能·opencv·计算机视觉
luozhonghua20001 小时前
opencv opencv_contrib vs2020 源码安装
人工智能·opencv·计算机视觉