
目录
软件开发整体介绍
开发流程
角色分工
软件环境
瑞吉外卖项目介绍
项目介绍
产品原型展示
技术选型
功能架构
角色
开发环境搭建
数据库环境搭建
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-starterorg.springframework.boot spring-boot-starter-testtest org.springframework.boot spring-boot-starter-webcompile com.baomidou mybatis-plus-boot-starter3.4.2 org.projectlombok lombok1.18.20 com.alibaba fastjson1.2.76 commons-lang commons-lang2.6 mysql mysql-connector-javaruntime com.alibaba druid-spring-boot-starter1.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 {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/backend
@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;
//@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 = "
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 {
@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("未知错误");
}
@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的分页插件)
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor=new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
@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("员工信息修改成功");
}
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);
}
}
@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 {
@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());
}
@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;
public class BaseContext {
private static ThreadLocal threadLocal=new ThreadLocal<>();
public static void setCurrentId(Long id){
threadLocal.set(id);
}
public static Long getCurrentId(){
return threadLocal.get();
}
}
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath;
@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);
}
@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可以很简单的实现手机号登录。
@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("短信发送失败");
}
@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注解即可。