目录
软件开发整体介绍
开发流程
角色分工
软件环境
瑞吉外卖项目介绍
项目介绍
产品原型展示
技术选型
功能架构
角色
开发环境搭建
数据库环境搭建
Maven环境搭建
1.直接创建maven项目(这里是没有用springboot快捷方式创建)
2.导入pom文件
3.添加核心配置文件application.yml
4.编写项目启动类
5.添加前端资源
6.springboot项目基本结构
功能开发总结
1.功能接口开发总结
编辑 需求分析编辑
代码开发
2.通用结果集
3.请求方式及springboot相关的注解(常用)
4.Session
5.过滤器
6.异常处理
7.md5加密
8.前端后端交互流程示例
9.分页查询
需求分析及代码开发
图示示例
10.js对long型数据进行处理导致精度丢失问题
11.公共字段自动填充
需求开发
代码开发
12.文件上传及文件下载
简要介绍
编辑编辑
编辑
代码开发
13.Dto
13.短信发送
简介
操作步骤
14. 手机号实现用户登录
15.事务控制
我这里用的是Navicat图形化工具创建的,或者通过命令行创建,不过就是比较麻烦,这里就不介绍了。
然后导入我们的sql脚本,我们就可以看到下面的数据库文件了。
这里是我们用到的依赖
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
compile
com.baomidou
mybatis-plus-boot-starter
3.4.2
org.projectlombok
lombok
1.18.20
com.alibaba
fastjson
1.2.76
commons-lang
commons-lang
2.6
mysql
mysql-connector-java
runtime
com.alibaba
druid-spring-boot-starter
1.1.23
server:
port: 8080
spring:
application:
name: reggie_take_out
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
@Slf4j//日志
@SpringBootApplication//项目启动类注解
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功...");
}
}
默认情况下我们只能访问static或template下的静态资源
所以我们要写配置类
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/backend/**")
.addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**")
.addResourceLocations("classpath:/front/");
}
}
图中的img是我需要的,正常情况下应该放在resource下。大致结构是这样的。
common包基本放置一些通用的类。config是配置类包 。controller是控制类包。dto是数据传输对象的意思,他的类跟实体类类似,但又有区别,我们放到下面再去专门讲解。entry包是实体类包,一般与数据表相对应。filter包是过滤器包。mapper是映射类包,负责实体映射。service是业务类包,包括业务接口和对应实现类。utils包是工具类包。
开发一个功能接口,我们基本需要几个简单步骤,首先对功能进行需求分析,然后我们编写需要操作的实体类(对应我们的数据表),然后创建对应的mapper,然后接着创建对应的service接口和实现类,最后创建我们的controller控制类。接着我们按需求开发即可。下面是一个登录接口开发过程
下面是对应要开发的前端静态页面,我们通过F12进行调试观察。
对应的需要操作的是employee表。下面是html中的javascript相关登录代码。我们作为后端开发人员,我们只需要看懂即可。例如handleLogin()是我们的登录方法,loginApi()是我们的发送请求接口。我们进入接口,即可查看我们要访问的地址。(从网页上用f12查看也是一样的)
我们在写后端代码时应当返回上面几个信息,然后相应逻辑如下。
首先创建了mapper,service,controller,common,entity包(我们这里用的是mybatis框架)
先创建实体类
/**
* 员工实体类
*/
@Data
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
private String idNumber;//身份证号码
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
再创建Mapper接口
@Mapper
public interface EmployeeMapper extends BaseMapper {
}
接着创建业务接口和实现类
public interface EmployeeService extends IService {
}
@Service
public class EmployeeServiceImpl extends ServiceImpl implements EmployeeService {
}
然后创建controller类
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
/**
* 员工登录
* @param request
* @param employee
* @return
*/
//@RequestBody返回json数据,并封装成employee对象,
// HttpServletRequest将员工id存入session
@PostMapping("/login")
public R login(HttpServletRequest request, @RequestBody Employee employee){
//将页面提交的密码进行md5加密处理
String password = employee.getPassword();
password= DigestUtils.md5DigestAsHex(password.getBytes());
//根据页面提交用户名来查询数据库
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);//因为用户名是唯一的
//如果没有查到,则返回失败结果
if(emp==null){
return R.error("登陆失败");
}
//比对密码
if(!emp.getPassword().equals(password)){
return R.error("登陆失败");
}
//查看员工状态,是否为禁用状态
if(emp.getStatus()==0){
return R.error("账号已禁用");
}
//登陆成功,将员工id存入session
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);
}
}
我们这里封装了一个通用的结果集,我们返回前端的信息都是用它来封装的,方便且实用。
里面的code=0或1,我们在前端都有对应的处理。我们需要注意的是在不同的前端对应的处理不同。下面是登录页面的处理。1是成功,0是失败.
if (String(res.code) === '1') {
localStorage.setItem('userInfo',JSON.stringify(res.data))
window.location.href= '/backend/index.html'
} else {
this.$message.error(res.msg)
this.loading = false
}
@Data
public class R {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static R success(T object) {
R r = new R();
r.data = object;
r.code = 1;
return r;
}
public static R error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}
public R add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
@RequestMapping("/employee")//这个我们是在类上加的(必须加)。 参数代表该类的请求路径,可有可无。下面的是在方法上加的,注意区分。
查找
get:@GetMapping
post: @PostMapping
删除
delete: @DeleteMapping
插入,修改
put: @PutMapping
他们也可加参数,例如 @PostMapping("/login"),下面这三种是我们比较常用的。
这个是@RequestBody的一个例子,他表示我们返回的是Employee的json形式的数据。
@PostMapping("/login")
public R login(@RequestBody Employee employee){
}
这个是@PathVariable的例子,他是映射 URL 绑定的占位符。
通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx“) 绑定到操作方法的入参中。
这种形式也可称之为REST风格。
@GetMapping("/{id}")
public R getById(@PathVariable Long id){
}
这个是@RequestParam,主要用于将请求参数区域的数据映射到控制层方法的参数上。
首先我们需要知道@RequestParam注解主要有哪些参数
value:请求中传入参数的名称,如果不设置后台接口的value值,则会默认为该变量名。否则在后台接口中ids将接收不到对应的数据
required:该参数是否为必传项。默认是true,表示请求中一定要传入对应的参数,否则会报404错误,如果设置为false时,当请求中没有此参数,将会默认为null,而对于基本数据类型的变量,则必须有值,这时会抛出空指针异常。如果允许空值,则接口中变量需要使用包装类来声明。
defaultValue:参数的默认值,如果请求中没有同名的参数时,该变量默认为此值。注意默认值可以使用SpEL表达式,如"#{systemProperties[‘java.vm.version’]}"
@DeleteMapping
public R delete(@RequestParam List ids){
}
@PostMapping("/status/{code}")
public R stopSale(@RequestParam List ids,@PathVariable int code){
}
我们在编程中常常需要在页面间传值。这时候我们经常需要用到session。(用来存储数据)
@PostMapping("/login")
public R login(HttpServletRequest request, @RequestBody Employee employee){
//登陆成功,将员工id存入session
request.getSession().setAttribute("employee",emp.getId());
//清除session中的id
request.getSession().removeAttribute("employee");
}
由于用户需要登录才能访问内部页面等需求,我们在开发中我们经常需要用到过滤器。
下面是一个例子,如果未登录就访问其他页面,自动跳转至登录页面
/**
* 检查用户是否登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER=new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request=(HttpServletRequest) servletRequest;
HttpServletResponse response=(HttpServletResponse) servletResponse;
//获取本次请求uri
String requestURI = request.getRequestURI();
//定义不需要处理的请求路径
String[] urls=new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//放行
if (check){
filterChain.doFilter(request,response);
return;
}
//判断登录状态
if (request.getSession().getAttribute("employee")!=null){
filterChain.doFilter(request,response);
return;
}
//未登录,通过输出流方式向客户端响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if (match){
return true;
}
}
return false;
}
然后在启动类添加注解@ServletComponentScan,这样我们就开启了过滤器功能。
我们在开发中会经常遇到各种各样的异常,这时我们经常需要设置全局异常处理和自定义异常。
/**
* 全局异常处理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if (ex.getMessage().contains("Duplicate entry")){
String[] s = ex.getMessage().split(" ");
String msg=s[2]+"已存在";
return R.error(msg);
}
return R.error("未知错误");
}
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(CustomerException.class)
public R exceptionHandler(CustomerException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
}
/**
* 自定义业务异常类
*/
public class CustomerException extends RuntimeException{
public CustomerException(String message){
super(message);
}
}
我们在登录过程中处理数据时我们需要对密码进行加密,以保证账户的安全。
我们经常会用到md5加密,这个是比较常用的。下面就来简要介绍一下。
MD5加密是一种不可逆的加密算法,不可逆加密算法的特征是加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,只有重新输入明文,并再次经过同样不可逆的加密算法处理,得到相同的加密密文并被系统重新识别后,才能真正解密。
下面是设置初始密码的例子。
//设置初始密码,需要md5加密
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employeeService.save(employee);
当我们需要显示的数据量庞大的时候,我们想要简洁明了的展示我们的数据,这时候分页就是我们的首选。以下是相关流程示例。(这里配置的mybatisPlus的分页插件)
/**
* 配置MP的分页插件
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor=new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
/**
* 员工信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R page(int page,int pageSize,String name){
log.info("page={},pageSize={},name={}",page,pageSize,name);
//构造分页构造器
Page pageInfo=new Page(page,pageSize);
//构造条件构造器
LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper();
//添加过滤条件
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加排序条件
queryWrapper.orderByAsc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
在开发中,我们发现js对long型数据处理会导致精度丢失。
处理思想:在服务端给页面响应数据时进行处理,将long型数据转成String类型。
下面是具体实现步骤。
@PutMapping
//@RequestBody转换成json格式
public R update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
Long empId = (Long)request.getSession().getAttribute("employee");
employee.setUpdateUser(empId);
employee.setUpdateTime(LocalDateTime.now());
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List> converters) {
log.info("扩展消息转换器");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter=new MappingJackson2HttpMessageConverter();
//设置对象转换器
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到消息转换器容器中
converters.add(0,messageConverter);
}
由于要实现公共字段自动填充,我们就需要从session拿到一些数据,这里我们用ThreadLocal.
/**
* 自定义元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入操作自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("insert...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
/**
* 更新操作自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("update...");
log.info(metaObject.toString());
long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
//实体类中部分代码
@TableField(fill = FieldFill.INSERT)//插入时自动填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)//插入和更新时自动填充
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
/**
* 基于ThreadLocal封装工具类,用于保存和获取当前登录用户id
* 在每个线程内单独起作用
*/
public class BaseContext {
private static ThreadLocal threadLocal=new ThreadLocal<>();
/**
* 设置值
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}
/**
* 获取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}
/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")//名字file必须与前端一致
public R upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置
log.info(file.toString());
//获取原始文件名
String originalFilename = file.getOriginalFilename();
String suffix=originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName= UUID.randomUUID().toString()+suffix;
//创建目录对象
File dir=new File(basePath);
//判断当前目录是否存在
if (!dir.exists()){
dir.mkdirs();
}
try {
//将临时文件转存到指定位置
file.transferTo(new File(basePath+fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {//输入流读取文件内容
FileInputStream fileInputStream=new FileInputStream(new File(basePath+name));
//输出流,将文件写回浏览器,在浏览器展示文件内容
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("image/jpeg");
int len=0;
byte[] bytes=new byte[1024];
while ((len=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
数据传输对象(DTO)(Data Transfer Object),是一种设计模式之间传输数据的软件应用系统。数据传输目标往往是数据访问对象从数据库中检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)。
由于我们在实际开发中实体类有时候满足不了我们的需求,比如本项目开发菜品,我们在菜品中需要口味的集合。
所以这里我们既用到了口味实体,也用到了菜品实体,所以这里我们用到了dto,我们封装实体类,以得到我们想要的类。(Dish和DishFlavor分别对应一张数据表)
@Data
public class DishDto extends Dish {
private List flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
阿里云
1.添加签名
2.添加模板
3.获取Assesskey
4.查看帮助文档,用原始java sdk测试
5.用测试短信功能测试,需充值至少一块钱。(在首页我们可以找到测试接口)
这里我们通过session可以很简单的实现手机号登录。
/**如果这里页面点击没反应,我们需要及时情理浏览器缓存
* 发送手机短信验证码
* @param user
* @return
*/
@PostMapping("/sendMsg")//json提交的要加requestbody
public R sendMsg(@RequestBody User user,HttpSession httpSession){
//获取手机号
String phone=user.getPhone();
if (StringUtils.isNotEmpty(phone)){
//生成随机6位验证码
String code = ValidateCodeUtils.generateValidateCode(6).toString();
log.info("code={}",code);
//调用api
SMSUtils.sendMessage(signName,templateCode,phone,code);
//将生成的验证码保存到session中
httpSession.setAttribute(phone,code);
return R.success("验证码发送成功");
}
return R.error("短信发送失败");
}
/**
* 移动端用户登录,后期设置验证码定时器
* @param map
* @param httpSession
* @return
*/
@PostMapping("/login")//json提交的要加requestbody
public R login(@RequestBody Map map, HttpSession httpSession){
log.info(map.toString());
//获取手机号和验证码
String phone = map.get("phone").toString();
String code = map.get("code").toString();
//获取session验证码
Object codeInSession = httpSession.getAttribute(phone);
//比较
if (codeInSession!=null && codeInSession.equals(code)){
LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper();
queryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(queryWrapper);
if (user==null){
//判断当前手机号是否为新用户,若为新用户则自动完成注册
user=new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
httpSession.setAttribute("user",user.getId());
return R.success(user);
}else if(codeInSession!=null && !codeInSession.equals(code)){
return R.error("验证码错误");
}
return R.error("登录失败");
}
我们只需要在业务层的方法上加上一个注解即可。他可保证事务的一致性。
@Override
@Transactional//因为操作多张表,我们这里加入事务
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long id = dishDto.getId();//菜品id
List flavors = dishDto.getFlavors();
flavors=flavors.stream().map((item)->{
item.setDishId(id);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据
dishFlavorService.saveBatch(flavors);
}
然后在启动类上加入@EnableTransactionManagement注解即可。