古文字识别助手与众包平台——项目博客四

古文字识别助手与众包平台——项目博客四

背景:

1.本项目持久层实现采用的是mybatis框架

MyBatis 是一个半自动化的 ORM 框架,所谓半自动化是指 MyBatis 只支持将数据库查出的数据映射到 POJO 实体类上,而实体到数据库的映射则需要我们自己编写 SQL 语句实现,相较于Hibernate 这种完全自动化的框架,Mybatis 更加灵活,我们可以根据自身的需求编写 sql 语句来实现复杂的数据库操作。

2.同时由于项目需要对数据进行校验,所以用到了jsr303进行便捷的验证。

JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

Hibernate 对其实现

Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

3.项目中有图片上传的需求,因此简述实现方式。

4.小程序端实现登录功能。同时优化登录相关的功能逻辑。

mybatis的使用:

1.引入maven依赖:

<!--引入 mybatis-spring-boot-starter 的依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

并在项目的properties文件内写好数据库的各项配置文件;
2.新建实体类(实体类属性与数据库中的表的字段一一对应):

package com.example.guke.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User
{

    private int id;


    // 用户的唯一标识
    @NotEmpty
    private String openid;

    private String nickname;

    private int sex;

    // 头像地址
    private String headimgurl;

    // 用户所在省份
    private String province;

    // 用户的累计积分
    private int score;

    public User(String openid)
    {
        this.openid = openid;
    }

}

3.创建Mapper映射文件,本项目采用的是注解的方式:

基于注解:

@Select
@Insert
@Update
@Delete

package com.example.guke.mapper;


import com.example.guke.entity.User;
import org.apache.ibatis.annotations.*;

import java.util.List;

@Mapper
public interface UserMapper
{

    @Insert("INSERT INTO user(openid,nickName,sex,headimgurl,province,score) " +
            "VALUES(#{openid},#{nickname},#{sex},#{headimgurl},#{province},0)")
    public int insertUser(User user);

    @Select("SELECT * FROM user WHERE openid = #{openid}")
    public User findUserByOpenid(@Param("openid") String openid);

    /**
     * @param openid
     * @name: getHeadimgurlByOpenid
     * @description: TODO  根据openid获取头像地址
     * @return: java.lang.String
     * @date: 2022/4/13 16:45
     * @author: NiuYiq
     */

    @Select("SELECT headimgurl FROM user WHERE openid = #{openid}")
    public String getHeadimgurlByOpenid(@Param("openid") String openid);

    /**
     * @param openid
     * @name: getNicknameByOpenid
     * @description: TODO  根据openid获取nickname
     * @return: java.lang.String
     * @date: 2022/4/13 16:45
     * @author: NiuYiq
     */

    @Select("SELECT nickname FROM user WHERE openid = #{openid}")
    public String getNicknameByOpenid(@Param("openid") String openid);


    /**
     * @param score
     * @param openid
     * @name: updateUserScore
     * @description: TODO  更新user表的评分
     * @return: int
     * @date: 2022/5/10 21:16
     * @author: NiuYiq
     */

    @Update("UPDATE user SET score = score + #{score} WHERE openid = #{openid}")
    int updateUserScore(@Param("score") int score, @Param("openid") String openid);


    /** 
     * @name: getRank 
     * @description: TODO  获取排行榜
     * @return: java.util.List<com.example.guke.entity.User>
     * @date: 2022/5/18 08:42
     * @author: NiuYiq
     * 
     */
     
    @Select("SELECT * FROM user ORDER BY score DESC")
    List<User> getRank();

    /** 
     * @name: getScoreByOpenid 
     * @description: TODO  通过openid获取用户积分
     * @param openid
     * @return: int
     * @date: 2022/5/18 08:43
     * @author: NiuYiq
     * 
     */
     

    @Select("SELECT score FROM user WHERE openid = #{openid}")
    int getScoreByOpenid(String openid);


}

数据校验:

空值检查常用注解:

@Null 验证对象是否为null 
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串 
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格. 
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.

boolean常用注解:

@AssertTrue 验证 Boolean 对象是否为 true 
@AssertFalse 验证 Boolean 对象是否为 false

长度检查常用注解:

@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) Validates that the annotated string is between min and max included.

日期检查常用注解:

@Past 验证 DateCalendar 对象是否在当前时间之前,验证成立的话被注释的元素一定是一个过去的日期
@Future 验证 DateCalendar 对象是否在当前时间之后 ,验证成立的话被注释的元素一定是一个将来的日期
@Pattern 验证 String 对象是否符合正则表达式的规则,被注释的元素符合制定的正则表达式,regexp:正则表达式 flags: 指定 Pattern.Flag 的数组,表示正则表达式的相关选项。

数值检查常用注解:

//建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为”“,Integer为null
@Min 验证 NumberString 对象是否大等于指定的值
@Max 验证 NumberString 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits 验证 NumberString 的构成是否合法
@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
@Range(min=, max=) 被指定的元素必须在合适的范围内
@Range(min=10000,max=50000,message=”range.bean.wage”)
@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
@CreditCardNumber信用卡验证
@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)

示例:

public class Order {
    // 必须不为 null, 大小是 10
    @NotNull
    @Size(min = 10, max = 10)
    private String orderId;
    // 必须不为空
    @NotEmpty
    private String customer;
    // 必须是一个电子信箱地址
    @Email
    private String email;
    // 必须不为空
    @NotEmpty
    private String address;
    // 必须不为 null, 必须是下面四个字符串'created', 'paid', 'shipped', 'closed'其中之一
    // @Status 是一个定制化的 contraint
    @NotNull
    @Status
    private String status;
    // 必须不为 null
    @NotNull
    private Date createDate;
    // 嵌套验证
    @Valid
    private Product product;
 
   // getter 和setter方法
}

图片上传:

流程:前端向后端上传图片,后端接受图片,并返回图片的打分。

@UserLoginToken
    @ResponseBody
    @PostMapping("/upload")
    public Result uploadPicture(@RequestParam("file") MultipartFile file,
                                int id,
                                HttpServletRequest request) throws IOException, InterruptedException
    {

        if (file == null)
            throw new CustomException("图片文件为空");

        String openid = openidUtils.getOpenidFromRequest(request);

        // 根据源图片类型创建响应的文件夹
        String path = base_path;
        path += Integer.toString(id);
        path += "/";
        //将文件保存到服务器指定位置
        try
        {
            //获取文件在服务器的储存位置
            File filePath = new File(path);
            log.debug("文件的保存路径" + path);
            if (!filePath.exists() && !filePath.isDirectory())
            {
                log.debug("目录不存在,创建目录" + filePath);
                filePath.mkdir();
            }
            Upload uploadImage = new Upload();

            //获取原始文件名称(包括格式)
            String originalFileName = file.getOriginalFilename();

            //获取文件类型,以最后一个‘.’为标识
            String type = originalFileName.substring(originalFileName.lastIndexOf(".") + 1);


            // 以用户的openid对图片进行命名
            String name = openid;

            // 设置文件新名称:用户的openid
            Date d = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
            String date = sdf.format(d);
            String fileName = date + name + "." + type;
            // 在指定路径下创建文件
            File targetFile = new File(path, fileName);
            file.transferTo(targetFile);
            System.out.println("上传成功");


            // 保存upload的信息到数据库
            uploadImage.setName(id);
            uploadImage.setAddress(path + fileName);
            uploadImage.setTime(d);
            uploadImage.setOpenid(openid);

            // TODO: 这里还有一个需要做的工作是调用python脚本获取评分 (根据图片的本地地址) 最后将评分返回给前端,也得保存至数据库
            // 由于获取到的数据为string类型,所以这边先转为double
            // 然后得分 *100
            // 然后再转化为整数
            String result = this.getScore(path + fileName, id);
            double res = Double.parseDouble(result) * 100;
            int score = (int) res;

            uploadImage.setScore(score);

            // 更新source表相应图片的描绘次数
            sourceService.updateCountById(id);

            // 更新user表的总评分
            userService.updateUserScore(uploadImage.getScore(), openid);

            // 插入upload表信息
            uploadService.insertUpload(uploadImage);
            Map<String, Object> map = new HashMap<>();
            map.put("score", score);
            return ResultFactory.success().message("上传成功").data(map);
        } catch (Exception e)
        {
//            System.out.println("上传失败");
//            result.put("code",400);
            log.error("保存图片失败");
            e.printStackTrace();
            return ResultFactory.error().message("上传失败");
        }


    }

@UserLoginToken注解已经在前面的博客中有提到,是用来验证用户token的

其中的getOpenidFromRequest方法如下:由于经常要从请求的header中抽出openid,所以剥离出来了一个工具类的方法,主要作用就是从header的token字段中提取出用户的openid。

    public String getOpenidFromRequest(HttpServletRequest request)
    {

        String token = request.getHeader("token");// 从 http 请求头中取出 token
//        if(token == null)
//            throw new CustomException(ResultCodeEnum.NO_TOKEN);
        // 获取 token 中的 user openid
        String openid;
        try
        {
            openid = JWT.decode(token).getAudience().get(0);
            log.info(openid);
        } catch (JWTDecodeException j)
        {
            throw new RuntimeException("token解析出错");
        }
        return openid;

    }

之后将获取到的图片保存到服务器的某个位置,本项目是保存到了nginx的静态资源配置目录下面,便于后面拓展需求的时候需要返回上传上来的图片给用户,便可以直接返回在线地址。
上传文件使用的是MultipartFile类。

package org.springframework.web.multipart;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.springframework.core.io.InputStreamSource;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;

public interface MultipartFile extends InputStreamSource {
//getName() 返回参数的名称
    String getName();
//获取源文件的昵称
    @Nullable
    String getOriginalFilename();
//getContentType() 返回文件的内容类型
    @Nullable
    String getContentType();
//isEmpty() 判断是否为空,或者上传的文件是否有内容
    boolean isEmpty();
//getSize() 返回文件大小 以字节为单位
    long getSize();
//getBytes() 将文件内容转化成一个byte[] 返回
    byte[] getBytes() throws IOException;
//getInputStream() 返回InputStream读取文件的内容
    InputStream getInputStream() throws IOException;

    default Resource getResource() {
        return new MultipartFileResource(this);
    }
//transferTo(File dest) 用来把 MultipartFile 转换换成 File
    void transferTo(File var1) throws IOException, IllegalStateException;

    default void transferTo(Path dest) throws IOException, IllegalStateException {
        FileCopyUtils.copy(this.getInputStream(), Files.newOutputStream(dest));
    }
}

1、“流”是一个抽象的概念,它是对输入输出设备的一种抽象理解,在java中,对数据的输入输出操作都是以“流”的方式进行的。

2、“流”具有方向性,输入流、输出流是相对的。当程序需要从数据源中读入数据的时候就会开启一个输入流,相反,写出数据到某个数据源目的地的时候也会开启一个输出流。

3、数据源可以是文件、内存或者网络等。

所以使用该工具类能够比较便捷地上传文件。

小程序端登录功能的实现:

思路与实现:
在前文搭建项目结构时说到,我们的项目将微信用户在小程序的openid作为唯一的身份标识。只有登录后,用户才可以进行画图、操作社区等功能。后端数据库中将openid作为用户的唯一标识,因此,在小程序借助微信官方API获取openid后,立即应该发送网络请求,与后端交互,后端返回登录成功的消息后,才认为登录成功。

此外,我们设计,为了保证网络请求交互的安全,后端为每个用户生成了token,在初次登录成功时返回给前端。为此,我在app.js中设置了token字段,保证全局都可以访问到token。此外,将整个项目的网络请求的base地址也存储在app.js中:
在这里插入图片描述
这样,不同组件发送网络请求时,只需要在baseUrl后面添加路径即可,并且获取token。

具体准备工作完成。接下来需要设想,什么时候向后端发送网络请求,完成登录。

在index页面,初次登录时,获取用户授权后,会获取openid,而后端要求登录的字段如下:

在这里插入图片描述
其中,除了openid是借助微信官方API得到的外,其余字段在用户授权时都已经得到。因此,我们需要在获取openid之后,立即向后端发送网络请求。因此,找到index.js中的登录:
在这里插入图片描述
wx.request是发起网络请求,向官方API指定的地址发送网络请求后获取到了openid,并将其保存在了appInstance.globalData中。因此,网络请求应该在此时发起。因此,首先需要准备字段:

在这里插入图片描述

由此可见,将用户信息和openid保存在app.js中是十分明智的。准备好数据后,我封装了发起网络请求的函数:

在这里插入图片描述

在这里插入图片描述

封装connect方法的原因是为了便于复用。其三个参数分别是:url(在baseUrl后面的地址)、msg(数据字段)、type(是get、post或是其他类型的请求)。

在方法内部,由于调用connect的是当前页面实例,先用that保存this, 便于在后面的作用域中访问到实例。例如,在that.setData处,如果使用this,这个this并不是当前实例,自然就无法调用setData了。

然后,继续使用wx.request发起网络请求,设置url、header,并在成功的回调函数中做操作。接口文档描述了返回数据:

当登录成功时,返回的code为0,否则为-1。因此,获取res.data,判断code,如果是0,则提示用户登录成功,否则登录失败。 一旦登录成功,就将message字段保存到token中。此外,我还在index.js中保存了hasToken变量:

在这里插入图片描述

设置这个变量的原因是,现在情况和之前不同。之前是在获取了userInfo之后就认为登录成功,但现在是获取token之后才认为登录成功,因此设置了这个标志位。

但这样还不够。因为,在wxml文件中,一旦获取了userInfo,页面就立刻从登录页变为信息展示页面了,这不合理,应该是在彻底登录成功后才跳转。因此,wxml文件中的判断条件不能是userInfo.avatarUrl了,而应该是hasToken:

在这里插入图片描述
至此,登录功能已经完成。

登录相关功能的逻辑优化:

说明: 此前的项目中社区,个人中心以及画板未经登录也可访问,这是不符合常识的,因此需要修改逻辑,使得用户需先登录再进行其他操作。

在my.js中也要保存token:

在这里插入图片描述

在onShow生命周期函数中,每次my页面显示,都要将appInstance.globalData中的userInfo和token保存下来,保证随时更新:

在这里插入图片描述

这样,在my.wxml中,判断条件就改为token即可:
在这里插入图片描述

这样,登录成功后的效果:

在这里插入图片描述
这是对于个人中心的解决办法,对于社区与画板同样的步骤,因此不再赘述。
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值