古文字识别助手与众包平台——项目博客四
背景:
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 验证 Date 和 Calendar 对象是否在当前时间之前,验证成立的话被注释的元素一定是一个过去的日期
@Future 验证 Date 和 Calendar 对象是否在当前时间之后 ,验证成立的话被注释的元素一定是一个将来的日期
@Pattern 验证 String 对象是否符合正则表达式的规则,被注释的元素符合制定的正则表达式,regexp:正则表达式 flags: 指定 Pattern.Flag 的数组,表示正则表达式的相关选项。
数值检查常用注解:
//建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为”“,Integer为null
@Min 验证 Number 和 String 对象是否大等于指定的值
@Max 验证 Number 和 String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits 验证 Number 和 String 的构成是否合法
@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即可:
这样,登录成功后的效果:
这是对于个人中心的解决办法,对于社区与画板同样的步骤,因此不再赘述。