项目整体介绍




开发环境搭建
1.前端环境搭建
2.后端环境搭建
nginx反向代理

管理端
员工管理
禁用员工
PathVariable路径参数的使用
builder注解的使用
员工分页查询
-
Controller层 返回值为 Result<PageResult> return Result.success(PageResult);
-
Service层 返回值为 PageResult return new PageResult(.getTotal(),list)
@Override
public PageResult pageQuey(EmployeePageQueryDTO employeePageQueryDTO) {
//重点:使用pagehelper:本质是拦截器,用Treadlocal取出page参数在xml文件里加入limit关键字
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
//重点:这里的page是pagehelper里的,范性里填属性,用mapper返回
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
//重点:从mapper获取到page(pagehelper里的)列表后,取出page里的数量和列表记录
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total,records);
}时间日期格式化
(Spring MVC消息转换器)
(JsonFormat)

新增员工
分为根据id查询员工与更新员工(传入上个步骤的对象json格式) 这两个步骤之间是自动完成的(前端路径自动匹配)
@RequestBody→当传入对象为json格式时使用
拷贝对象属性:(复制属性)
- 有点像把一个对象属性转换为另一个对象属性
@Override
public void update(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO,employee);
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.update(employee);
}通过拦截器Basecontext获取修改者(JwtTokenAdminInterceptor内)
分类管理


公共字段的自动填充
技术点:枚举,自定义注解,AOP,反射
菜品管理
新增菜品


接口设计
-
根据类型查询分类(已完成)

-
文件上传

-
新增菜品

-
数据库设计

文件上传
返回类型:Result<String> 传入参数:MultipartFile file 注释:PostMapping(“…”)
传入过程:
- 用file.得出原始文件名
- 然后用subString(原始文件名.lastIndexOf(“.”))得出文件名后缀
- 用UUID拼接新的文件名
- 在Utils.upload传入参数拼接后的文件名:返回fielPath
@Slf4j
@RestController
@RequestMapping("/admin/common")
public class CommonController {
private AliOssUtil aliOssUtil;
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);
try {
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = UUID.randomUUID().toString()+extension;
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.info("文件上传失败:{}",file);
}
return null;
}
}DTO
在Java编程中,DTO(Data Transfer Object,数据传输对象)是一种用于在不同层之间传输数据的对象。DTO通常用于将业务逻辑层(Service层)的数据传输给表示层(Presentation层)或持久化层(Persistence层)。DTO对象通常包含业务领域的数据,但不包含业务逻辑12。
使用场景
DTO在以下场景中非常有用:
-
数据传输:在业务逻辑层和表示层之间传输数据时,使用DTO可以简化数据的传递过程。例如,在一个涉及多个表的业务操作中,可以使用DTO来封装不同表的数据2。
-
数据封装:DTO可以将多个实体对象的数据封装在一个对象中,便于在一次操作中传递多个数据。例如,在新增、查询和更新菜品时,通过DTO对象进行数据库交互,包括保存、查询和更新菜品及关联的口味信息2。
添加口味
获取insert的主键值:<insert id=“insert” useGeneratedKeys=“true” keyProperty=“id”>
在service层,通过DishDTO传入inset语句时,需new出一个dish给insert语句,因为DishDTO包含一个口味的集合 通过BeanUtils.copyPropertis复制属性
这里的insert语句上需加注解:@AutoFill(values = OperationType.INSERT)
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//像菜品表插入一条数据
dishMapper.insert(dish);
//获取insert语句生成的主键值(useGeneratedKeys="true" keyProperty="id)
Long dishId = dish.getId();
//像口味表插入n条数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors!=null && flavors.size() > 0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
dishFlavorMapper.insertBatch(flavors);
}
}insert 语句中的遍历插入(foreach标签的使用):
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value)
VALUES <foreach collection="flavors" item="df">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>分页查询

这里根据接口定义设计了DishPageQueryDTO 包含以上五个变量
这里根据接口定义设计了DishVO(vaule object)

DTO与VO,page工具的使用
DTO是前端传后端,VO是后传前(回显?)
//query方式:不需要像接受json数据一样加注解(直接在地址栏key=??)
这里的page范性内填<pageVO>(内容多一个分类名称),而不是像员工分页查询一样<Employee>
菜品删除
接口设计与数据库设计

传入参数
思路:传入参数为多个时(query方式), 第一种方法:接受形参为字符串,然后后续分割字符串处理. 第二张方式:SpringMVC处理,(@RequestParam List<Long> ids) 小总结: 传入为json格式:@RequestBody 传入为连续字符串:@RequestParam
多个参数的传入与sql书写
在查询setmeal_ids时,需使用动态sql到xml映射文件中,因为,传入的参数可能为多个,即
select setmeal_id from setmeal_dish in(1,2,3,4.....)
这里使用<foreach>标签
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
select setmeal_id from setmeal_dish where dish_id in
<foreach collection="dishIds" item="dishId" separator="," close=")" open="()">
#{dishId}
</foreach>
</select><foreach>中的collection:返回值的名称,item:取出变量的名称,close与open:前后需要拼接的符号
传入多个参数时的快速遍历
如这里ids,.出for方法即:ids.for回车
事务
删除操作需有一体性,使用事务
异常
不满操作时,抛出自定义的异常
@Transactional
@Override
public void deleteBatch(List<Long> ids) {
//判断是否能删除,1.是否起售.2.呗套餐关联
for (Long id : ids) {
Dish dish = dishMapper.getById(ids);
if(Objects.equals(dish.getStatus(), StatusConstant.ENABLE)){
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if(setmealIds !=null && !setmealIds.isEmpty()){
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品数据与口味数据
for (Long id : ids) {
dishMapper.deleteById(id);
dishFlavorMapper.deleteByDishId(id);
}
}菜品修改



路径参数
传入参数需称为路径参数时,使用@PathVariable注解
VO与DTO的选择
在修改菜品中的查询菜品id的操作时,返回需为DishVO,因为在项目中,VO除了包括基础的菜品数据时,还包括Flavor口味表 其实以上说法不对,VO相比于DTO多了更新时间等成员.主要是反馈给前端
dishDTO转换为普通的dish对象:使用BeanUitil方法赋值给新的dish对象
菜品回显
没什么nb的,就是在controller层返回了dishVO对象给前端(Result.success(dishVO))
具体口味的修改
技术层面来说,这里对口味的数据是先删除在添加.
<set>标签的使用
在更新菜品表时,使用sql语句
update dish set name = #{name},price = #{price}......
改为动态sql语句xml文件
<update id="update">
update dish
<set>
<if test="name != null">
name = #{name},
</if>
<if test="categoryId != null">
category_id = #{categoryId},
</if>
<if test="price != null">
price = #{price},
</if>
<if test="image != null">
image = #{image},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser},
</if>
</set>
where id = #{id}
</update>店铺营业状态(Redis)
Redis
Bean注解
-
@Bean:Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中;
-
SpringIOC 容器管理一个或者多个bean,这些bean都需要在@Configuration注解下进行创建,在一个方法上使用@Bean注解就表明这个方法需要交给Spring进行管理;
-
@Bean是一个方法级别上的注解,主要用在@Configuration注解的类里,也可以用在@Component注解的类里。添加的bean的id为方法名;
-
使用Bean时,即是把已经在xml文件中配置好的Bean拿来用,完成属性、方法的组装;比如@Autowired , @Resource,可以通过byTYPE(@Autowired)、byNAME(@Resource)的方式获取Bean;
-
注册Bean时,@Component , @Repository , @ Controller , @Service , @Configration这些注解都是把你要实例化的对象转化成一个Bean,放在IoC容器中,等你要用的时候,它会和上面的@Autowired , @Resource配合到一起,把对象、属性、方法完美组装;
-
@Configuration与@Bean结合使用:@Configuration可理解为用spring的时候xml里面的标签,@Bean可理解为用spring的时候xml里面的标签;
需求分析与设计
用户端吧路径中的admin改为user
营业状态存储方式:基于Redis的字符串进行存储

不同包下的同名类名处理
在RestController(“指定的不同类名”)
用户端
HttpClient

GET请求测试
@SpringBootTest
@Slf4j
public class HttpClientTest {
@Test
//GEt
public void testGET() throws Exception {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
//发送请求
CloseableHttpResponse response = httpClient.execute((httpGet));
//获取服务端返回的状态吗
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("状态码为:"+statusCode);
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("返回的数据为:"+body);
//关闭资源
response.close();
httpClient.close();
}
}POST请求测试
@Test
public void testPOST() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");
StringEntity entity = new StringEntity(jsonObject.toString());
//指定请求编码方式
entity.setContentEncoding("utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
CloseableHttpResponse response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应码为:"+statusCode);
HttpEntity entity1 = response.getEntity();
String body = EntityUtils.toString(entity1);
System.out.println("响应数据为"+body);
response.close();
httpClient.close();
}微信小程序开发
目录结构

微信登录接口


需配置微信小程序appid与secret在dev.yaml与yaml文件
用户登录代码开发
Controller层:
- 接受DTO,返回VO
- 调用sevice层返回user对象
- 对user对象解析并加入jwttonken等
- 返回VO
Service层
- 接受DTO后,加入四大护法(两个在本地)进Map对象
- 使用HttpClientUtil.doGet(微信官方api,map)得到json对象
- 对json对象解析出openid
- 判断openid
- 然后通过openid查询Mapper层
- (如果没有用户就插入用户表)
- 返回user
为用户端配置全局拦截器
开发完后,添加用户的全局拦截器
- JwtTokenUserInterceptor.java
- 之后在配置类内注册此拦截器WebMvcConfiguration.java
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}商品浏览代码




缓存菜品


- 数据库中的菜品数据变更时及时清理缓存数据
方案1:
private void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}在每次更新,或删除菜品时调用redisTemplate方法
方案2:SpringCache
SpringCache

套餐代码(双表)
添加套餐
- setmealDTO(传入参数)包含setmeal基础信息与setmealDish

在Service层:
- 对于setmeal的一堆零碎参数 使用BeanUtils赋值给新创建的setmeal对象 插入对应的setmealMapper
- 对于setmealDishs 从DTO中得出里面的DIsh表 遍历list<setmealDish>列表赋值相应的软连接(项目要求)
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO,setmeal);
setmealMapper.insert(setmeal);
Long setmeal_id = setmeal.getId();
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmeal_id);
});
setmealDisheMapper.insert(setmealDishes);
}在Mapper层
-
在xml文件中<parameterType=“返回的类型”>
-
<useGenerateKeys = “true”><KeyProperty=“id”> 通过这两个实现主键自增
-
在插入多条数据时,使用forEach
-
collection=“setmealDishes”:指定要遍历的集合名称,这里是
setmealDishes集合 -
item=“sd”:表示集合中每个元素的别名,在遍历过程中,每次迭代到的元素会被赋值给
sd变量。 -
separator=”,“:指定每次迭代之间的分隔符,这里是逗号
,,即在每次迭代生成的 SQL 片段之间用逗号隔开。
更新套餐
在service层:
- 更新套餐时,对于零碎对象用BeanUtils
- 对于setmealDish 先删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete 然后从DTO中获得dish.setmealId(forEach遍历赋值) 最后重新插入setmealDishes
更改套餐状态
在service层:
- 不直接创建单独的status的mapper语句,用biuder容器将id的setmealStatus改为相应的状态,并将此setmeal装入update语句
- 外连接查询 此处用的是dishMapper.从中间表Dish_Setmeal表查询
select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}分表查询的动态sql
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select
s.*,c.name categoryName
from
setmeal s
left join
category c
on
s.category_id = c.id
<where>
<if test="name != null">
and s.name like concat('%',#{name},'%')
</if>
<if test="status != null">
and s.status = #{status}
</if>
<if test="categoryId != null">
and s.category_id = #{categoryId}
</if>
</where>
order by s.create_time desc
</select>添加购物车


用户id的获取
在此项目中,通过拦截器可获取当前用户id
BaseContext.getCurrentId()
购物车的添加逻辑
@Override
public void addShoppingcart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
Long id = BaseContext.getCurrentId();
shoppingCart.setUserId(id);
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
//如果已经存在,数量+1
if(list!=null && !list.isEmpty()){
ShoppingCart cart = list.get(0);
cart.setNumber(cart.getNumber()+1);
shoppingCartMapper.updateNumberById(cart);
}else{
//如果不存在,插入新的一条购物车数据(判断条件:通过DTO里的dishId是否为空判断)
Long dishId = shoppingCartDTO.getDishId();
if(dishId != null){
Dish dish = dishMapper.getById(dishId);
shoppingCart.setName(dish.GetName())
shoppingCart.setImage(dish.getImage());
shoppingCart.setAmount(dish.getPrice());
}else{
//查询的是套餐
Long setmealId = shoppingCart.getSetmealId();
Setmeal setmeal = setmealMapper.select(setmealId);
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}缓存套餐
使用SpringCache
导入地址薄
需求分析

其中《修改地址需要先根据id查询在更新








用户下单






DTO设计

VO设计

@Transactional
@Override
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
//各种业务异常(地址薄为空,购物车数据为空)
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if(addressBook == null){
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);
List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
if(shoppingCartList == null || shoppingCartList.size() == 0){
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
//像订单表插入一条数据
Orders orders = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO,orders);
orders.setOrderTime(LocalDateTime.now());
orders.setPayStatus(Orders.UN_PAID);
orders.setStatus(Orders.PENDING_PAYMENT);
orders.setNumber(String.valueOf(System.currentTimeMillis()));
orders.setPhone(addressBook.getPhone());//通过地址薄拿到用户手机号
orders.setConsignee(addressBook.getConsignee());
orders.setUserId(userId);
orderMapper.insert(orders);//这里需要返回orders的主键值key,在xml中添加相应参数
//向订单明细表插入n条数据
List<OrderDetail> orderDetails = new ArrayList<>();
for(ShoppingCart cart : shoppingCartList){
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(cart,orderDetail);
orderDetail.setOrderId(orders.getId());
orderDetails.add(orderDetail);
}
orderDetailMapper.insert(orderDetails);
//清空当前用户购物车数据
shoppingCartMapper.delete(userId);
//封装VO返回
OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
.id(orders.getId())
.orderTime(orders.getOrderTime())
.orderNumber(orders.getNumber())
.orderAmount(orders.getAmount())
.build();
return orderSubmitVO;
}微信支付
、

Spring Task(订单状态定时处理)

cron表达式

需求分析

Websocket

来电提醒

Map map = new HashMap();
map.put("type",1);
map.put("orderId",ordersDB.getId());
map.put("content","订单号: "+outTradeNo);
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);图形统计
Apache ECharts
实战
用户端
查询历史订单
查询订单详情
取消订单
已完成
再来一单
未完成,
商家端
订单搜索
分页难点
在service层
Page
返回是
return new PageResult(page.getTotal(),orderVOList)
另外一个难点就是从DTO到VO需要手动加入菜品信息(订单中的菜品和数量) “同时vo对象里面需要一个String类型的描述字段,所以我们指定格式为菜品数量的格式进行格式化字符串封装进orderVO对象里面”
- 普通版本如下
private String getOrderDeshesStr(Orders orders) {
List<OrderDetail> orderDetailList = orderDetailMapper.getOrderDetailByOrdrId(orders.getId());
List<String> orderDishesList = new ArrayList<>();
for (OrderDetail orderDetail : orderDetailList) {
String orderDesh = orderDetail.getName()+"*"+orderDetail.getNumber()+";";
orderDishesList.add(orderDesh);
}
// 使用 StringBuilder 将所有菜品信息拼接在一起
StringBuilder result = new StringBuilder();
for (String dish : orderDishesList) {
result.append(dish);
}
// 返回最终拼接的字符串
return result.toString();
}- stream流版本如下
private String getOrderDishesStr(Orders orders) {
// 查询订单菜品详情信息(订单中的菜品和数量)
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());
// 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
List<String> orderDishList = orderDetailList.stream().map(x -> {
String orderDish = x.getName() + "*" + x.getNumber() + ";";
return orderDish;
}).collect(Collectors.toList());
// 将该订单对应的所有菜品信息拼接在一起
return String.join("", orderDishList);
}各个状态的订单数量统计
查询订单详情
接单,拒单
-
取消订单
-
派送订单
-
完成订单
当需要写理由时,传入DTO对象set理由 其他情况只需要传入id
营业额统计

当传入参数为时间时
@RequestMapping("/turnoverStatistics")
public Result<TurnoverReportVO> turnoverStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
log.info("营业额数据统计:{},{}",begin,end);
return Result.success(reportService.getTurnoverStatistics(begin,end));
}mapper传入参数为map时
使用动态xml ·
<select id="sumByMap" resultType="java.lang.Double">
select sum(amount) from orders
<where>
<if test="begin != null">
and order_time > #{begin}
</if>
<if test="end != null">
and order_time < #{end}
</if>
<if test="status != null">
ands status = #{status}
</if>
</where>
</select>新增用户统计
销量排名
Stream流的复杂使用
总结:
- 先转换时间格式
- 书写xml动态sql的出需要的数据列表(复杂,这里得到的是map)
- 取出列表里的独立数据
- 组装,返回VO数据
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
LocalDateTime beginTime = LocalDateTime.of(begin,LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(begin,LocalTime.MAX);
List<GoodsSalesDTO> salesTop = orderMapper.getSalesTop(beginTime, endTime);
List<String> names = salesTop.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
String nameList = StringUtils.join(names, ",");
List<Integer> numbers = salesTop.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
String numberList = StringUtils.join(numbers, ",");
return SalesTop10ReportVO.builder()
.nameList(nameList)
.numberList(numberList)
.build();
}复杂sql的书写(多表查询)
<select id="getSalesTop" resultType="com.sky.dto.GoodsSalesDTO">
select od.name, sum(od.number) number
from order_detail od, orders o
where od.order_id = o.id and o.status = 5
<if test="begin != null">
and o.order_time > #{begin}
</if>
<if test="end != null">
and o.order_time < #{end}
</if>
group by od.name
order by number desc
limit 0,10</select>