软件开发整体介绍
软件开发流程

角色分工
- 项目经理:对整个项目负责,任务分配、把控进度
- 产品经理:进行需求调研,输出需求调研文档、产品原型等
- UI设计师:根据产品原型输出界面效果图
- 架构师:项目整体架构设计、技术选型等
- 开发工程师:代码实现
- 测试工程师:编写测试用例,输出测试报告
- 运维工程师:软件环境搭建、项目上线
软件环境
- 开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
- 测试环境(testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
- 生产环境(production):即线上环境,正式提供对外服务的环境
项目
项目介绍


产品原型
页面效果

技术选型

开发环境搭建
前端环境

后端环境





Git
后端环境搭建-使用Git进行版本控制
使用Git进行项目代码的版本控制,具体操作:
- 创建Git本地仓库
- 创建Git远程仓库
- 将本地文件推送到Git远程仓库
数据库环境

前后端联调

思考
场景描述
- 前端请求地址:
http://localhost/api/employee/login(POST请求)
- 后端接口地址:
http://localhost:8080/admin/employee/login
后端接口定义(EmployeeController)
1 2 3 4 5 6 7 8 9 10 11 12
| Java@RestController @RequestMapping("/admin/employee") // 类级别的请求路径前缀 public class EmployeeController {
@Autowired private EmployeeService employeeService;
@PostMapping("/login") // 方法级别的请求路径 public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) { // 登录逻辑... } }
|
1 2 3 4 5
| Request URLhttp://localhost/api/employee/login Request Method:POST Status Code:200 Remote Address: 127.0.0.1:80 Referrer Policy: strict-origin-when-cross-origin
|
Nginx


nginx反向代理的配置方式:
1 2 3 4 5 6 7
| server{ listen 80; server_namelocalhost; location /api/ { proxy_passshttp://localhost:8080/admin/;#反向代理 } }
|
nginx负载均衡的配置方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| # 1. 定义上游服务器集群(负载均衡池) upstream webservers { server 192.168.100.128:8080; # 后端服务器1 server 192.168.100.129:8080; # 后端服务器2 }
# 2. 配置虚拟主机 server { listen 80; # 监听端口 server_name localhost; # 域名
# 3. 路径匹配与负载均衡转发 location /api/ { # 匹配以 /api/ 开头的请求 proxy_pass http://webservers/admin/; # 转发到上游集群,路径重写(/api/ → /admin/) } }
|
nginx负载均衡策略:
| 策略名称 |
说明 |
| 轮询 |
默认方式,请求按顺序轮流分配到后端服务器 |
| weight |
权重方式(默认权重为 1),权重越高,被分配的请求越多 |
| ip_hash |
依据客户端 IP 分配请求,固定 IP 始终访问同一后端服务器(解决会话保持问题) |
| least_conn |
依据后端服务器连接数分配,优先将请求分配给当前连接数最少的服务器 |
| url_hash |
依据请求 URL 分配,相同 URL 始终访问同一后端服务器(适用于缓存场景) |
| fair |
依据后端服务器响应时间分配,响应时间越短的服务器被优先分配请求 |
完善登录功能(加密密码)

完善登录功能
核心步骤
- 数据库密码加密:将数据库中存储的明文密码,改为 MD5加密后的密文(不可逆加密,增强安全性)。
- 代码逻辑调整:前端提交的密码,需先进行 MD5加密,再与数据库中的密文密码比对。
关键代码实现(密码比对逻辑)
1 2 3 4 5 6 7 8
| password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) { throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR); }
|
导入接口文档

用yapi导入接口文档
Swagger
介绍
1. Swagger
2. Knife4j
- 定位:为 Java MVC 框架(如 Spring Boot)集成 Swagger 的增强解决方案,提供更友好的 UI 界面和更丰富的功能(如接口排序、导出文档等)。
3. Maven 依赖(Knife4j 起步依赖)
1 2 3 4 5
| XML<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.2</version> </dependency>
|
使用方式
- 导入Knife4j的Maven坐标
- 在配置类中加入Knife4j相关配置
- 设置静态资源映射(否则接口文档页面无法访问)
配置类示例(WebMvcConfiguration)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Bean public Docket docket() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build();
Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller")) .paths(PathSelectors.any()) .build();
return docket; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
protected void addResourceHandlers(ResourceHandlerRegistry registry) { log.info("开始设置静态资源映射...");
registry.addResourceHandler("/doc.html") .addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); }
|
通过这个功能可以查看接口文档和测试
思考
通过Swagger就可以生成接口文档,那么我们就不需要Yapi了?
- Yapi是设计阶段使用的工具,管理和维护接口
- Swagger在开发阶段使用的框架,帮助后端开发人员做后端的接口测试
常用注解
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
| 注解 |
说明 |
@Api |
用在类上(如Controller),对类进行说明(如接口模块名称、功能描述)。 |
@ApiModel |
用在类上(如Entity、DTO、VO),描述数据模型的含义(如“用户登录请求DTO”)。 |
@ApiModelProperty |
用在属性上,描述属性的含义、数据类型、是否必填等(如“用户名,长度2-20字符”)。 |
@ApiOperation |
用在方法上(如Controller的接口方法),说明方法的用途、作用(如“用户登录接口,返回JWT令牌”)。 |
新增员工
需求分析和设计


数据库设计(employee表)
| 字段名 |
数据类型 |
说明 |
备注 |
| id |
bigint |
主键 |
自增 |
| name |
varchar(32) |
姓名 |
|
| username |
varchar(32) |
用户名 |
唯一 |
| password |
varchar(64) |
密码 |
|
| phone |
varchar(11) |
手机号 |
|
| sex |
varchar(2) |
性别 |
|
| id_number |
varchar(18) |
身份证号 |
|
| status |
int |
账号状态 |
1正常 0锁定 |
| create_time |
datetime |
创建时间 |
|
| update_time |
datetime |
最后修改时间 |
|
| create_user |
bigint |
创建人id |
|
| update_user |
bigint |
最后修改人id |
|
代码

1 2 3 4 5 6 7 8 9 10
| @Data public class EmployeeDTO implements Serializable{ private Long id; private String username; private String name; private String phone; private String sex; private String idNumber; }
|
测试(视频多看)
完善
程序存在的问题:
- 录入的用户名已存在,抛出异常后没有处理
- 新增员工时,创建人id和修改人id设置为了固定值
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @ExceptionHandler(SQLIntegrityConstraintViolationException.class) public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) { Log.error("异常信息:{}", ex.getMessage()); String message = ex.getMessage(); if (message.contains("Duplicate key")) { String[] split = message.split(" "); String username = split[2]; String msg = username + MessageConstant.ALREADY_EXISTS; return Result.error(msg); } else { return Result.error(MessageConstant.UNKNOWN_ERROR); } }
|
1.从请求头获取并解析JWT令牌,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Java// JwtTokenAdminInterceptor 拦截器中的核心逻辑 // 1. 从请求头获取令牌(令牌名称从配置文件读取,如 "token") String token = request.getHeader(jwtProperties.getAdminTokenName());
// 2. 校验令牌并解析员工ID try { // 调用工具类解析JWT令牌(需传入密钥和令牌字符串) Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); // 从令牌的Claims中提取员工ID(EMP_ID为自定义常量,对应生成令牌时存入的键) Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); // 3. 解析成功,放行请求 return true; } catch (Exception ex) { // 4. 解析失败(令牌无效/过期),返回401未授权 response.setStatus(401); return false; }
|
2. 问题:解析出的员工ID如何传递给Service的save方法?
3.代码完善 Threadlocal
- ThreadLocal并不是一个Thread,而是Thread的局部变量。
- ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不
- 能访问。
员工分页查询
需求分析和设计


代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) { PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize()); Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO); long total = page.getTotal(); List<Employee> records = page.getResult(); return new PageResult(total, records); }
|
完善
时间格式
方式一:在属性上加入注解,对日期进行格式化
1 2
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") privateLocalDateTimeupdateTime;
|
方式二:在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式化处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { log.info("开始扩展消息转换器...");
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(new JacksonObjectMapper());
converters.add(0, converter); }
|
启用禁用员工账号
需求分析和设计
业务规则:
可以对状态为“启用”的员工账号进行“禁用”操作
可以对状态为“禁用”的员工账号进行“启用”操作
状态为“禁用”的员工账号不能登录系统

代码
完善
编辑员工
需求分析和设计
代码
1 2 3 4 5 6 7 8
| @override public void update(EmployeeDT0 employeeDTO){ Employee employee = new Employee(); BeanUtils.copyProperties(employeeDTo, employee); employee.setUpdateTime(LocalDateTime.now()); employee.setUpdateUser(BaseContext.getCurrentId()): employeeMapper.update(employee); }
|
导入分类模块功能代码
需求分析和设计
业务规则:
- 分类名称必须是唯一的
- 分类按照类型可以分为菜品分类和套餐分类
- 新添加的分类状态默认为**“禁用”**
接口设计:
- 新增分类
- 分类分页查询
- 根据id删除分类
- 修改分类
- 启用禁用分类
- 根据类型查询分类

公共字段自动填充
问题分析
1. 公共字段定义
| 序号 |
字段名 |
含义 |
数据类型 |
| 1 |
create_time |
创建时间 |
datetime |
| 2 |
create_user |
创建人ID |
bigint |
| 3 |
update_time |
修改时间 |
datetime |
| 4 |
update_user |
修改人ID |
bigint |
2. 问题:代码冗余(传统方式)
在新增/修改业务数据时,需重复设置公共字段(以员工、分类为例):
1 2 3 4 5 6 7 8 9 10 11
| Java// 员工新增时设置公共字段 employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); employee.setCreateUser(BaseContext.getCurrentId()); // 从ThreadLocal获取当前登录人ID employee.setUpdateUser(BaseContext.getCurrentId());
// 分类新增时设置公共字段(重复代码) category.setCreateTime(LocalDateTime.now()); category.setUpdateTime(LocalDateTime.now()); category.setCreateUser(BaseContext.getCurrentId()); category.setUpdateUser(BaseContext.getCurrentId());
|
问题:每个实体类的新增/修改方法都需重复编写上述代码,导致冗余、维护成本高。
3. 解决思路
通过 AOP切面 或 MyBatis拦截器 统一处理公共字段赋值,避免重复代码。
实现思路
| 序号 |
字段名 |
含义 |
数据类型 |
操作类型(何时填充) |
| 1 |
create_time |
创建时间 |
datetime |
insert(新增时) |
| 2 |
create_user |
创建人ID |
bigint |
insert(新增时) |
| 3 |
update_time |
修改时间 |
datetime |
insert、update(新增/修改时) |
| 4 |
update_user |
修改人ID |
bigint |
insert、update(新增/修改时) |
- 自定义注解AutoFill,用于标识需要进行公共字段自动填充的方法
- 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
- 在Mapper的方法上加入AutoFill注解
新增菜品
需求分析和设计
业务规则:
- 菜品名称必须是唯一的
- 菜品必须属于某个分类下,不能单独存在
- 新增菜品时可以根据情况选择菜品的口味
- 每个菜品必须对应一张图片
接口设计:
新增菜品

代码
有点复杂,记得多看看视频
菜品分页查询
需求分析和设计
业务规则:
- 根据页码展示菜品信息
- 每页展示10条数据
- 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询

代码
代码与之前的分页查询一样,注意Pagehelper的用法
删除菜品
需求分析和设计
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉


只需要写一个接口,即批量删除接口。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
@Transactional @Override public void deleteBatch(List<Long> ids) { log.info("批量删除菜品,ids:{}", ids); for (Long id : ids) { Dish dish = dishMapper.getById(id); if(dish.getStatus() == StatusConstant.ENABLE){ throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } } List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); if(setmealIds !=null && setmealIds.size() > 0){ throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL); }
dishMapper.deleteByIds(ids); dishFlavorMapper.deleteByDishIds(ids); }
|
修改菜品
需求分析和设计
回写信息



代码
套餐管理
useGeneratedKeys=”true”和keyProperty=”id”这两个属性对于MyBatis在插入数据后将数据库生成的ID回填到Java对象中至关重要。
以下是它们各自的作用:
- useGeneratedKeys=”true”:这个属性告诉MyBatis,数据库会为这次插入操作生成一个主键。当设置为true时,MyBatis会尝试获取这个由数据库生成的主键。
- keyProperty=”id”:这个属性指定了输入参数对象(在本例中是Setmea1对象)中哪个属性应该被更新为数据库生成的主键。 id 表示 Setmeal 类的 id 属性。
- 如果没有这两个属性,MyBatis会执行一个普通的插入操作,但它不会期望或尝试去获取任何数据库自动生成的主键。因此,在插入操作完成后,传递给insert方法的Setmeal对象的id字段将保持其初始值(通常是nul1或0),因为 MyBatis 并不知道要去获取并设置这个生成的主键。
- 简单来说,这两个属性共同协作,使MyBatis能够感知到数据库的自动增长主键机制,并在插入成功后,将数据库生成的主键值“回填”到你的Java对象中,这样你就可以在后续操作中立即使用这个ID。
1 2 3 4 5 6 7 8 9 10 11 12
| <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sky.mapper.SetmealMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into setmeal (name, category_id, price, image, status, description) values (#{name}, #{categoryId}, #{price}, #{image}, #{status}, #{description}) </insert>
</mapper>
|
注意事务和数据库关系
Redis
介绍

下载


发布 ·TPORADOWSKI/REDIS (github.com)
启动与注册window服务
在命令行里输入
1
| redis-server.exe redis.windows.conf
|
在Redis的目录下输入:
1
| redis-server --service-install redis.windows.conf --service-name RedisService --port 6379
|
和
1
| redis-server --service-start --service-name RedisService
|
显示:
1
| Redis service successfully started
|
即为成功
这样设置完后,就可以让Redis服务开机自启
Redis数据类型
5种常用的数据类型
Redis存储的是key-value结构的数据,其中key是字符串类型,value有s种常用的数据类型:
- 字符串 string
- 哈希 hash
- 列表list
- 集合 set
- 有序集合sorted set/zset

常用命令
字符串操作命令
| 命令格式 |
说明 |
SET key value |
设置指定 key 的值 |
GET key |
获取指定 key 的值 |
SETEX key seconds value |
设置指定 key 的值,并将 key 的过期时间设为 seconds 秒 |
SETNX key value |
只有在 key 不存在时设置 key 的值 |
哈希操作命令
Redis Hash 是一个 String 类型的 field 和 value 的映射表,特别适合存储对象(如用户信息、商品属性等)。
| 命令格式 |
说明 |
HSET key field value |
将哈希表 key 中的字段 field 的值设为 value |
HGET key field |
获取哈希表 key 中指定字段 field 的值 |
HDEL key field |
删除哈希表 key 中的指定字段 field |
HKEYS key |
获取哈希表 key 中的所有字段名(field) |
HVALS key |
获取哈希表 key 中的所有字段值(value) |
Hash 结构示意图
1 2 3 4
| key ├─ field1 → value1 ├─ field2 → value2 └─ ...
|
(一个 key 对应多个 field-value 键值对,类似 JSON 对象)
列表操作命令
Redis 列表是简单的字符串列表,按照插入顺序排序,可模拟栈、队列等数据结构。
| 命令格式 |
说明 |
LPUSH key value1 [value2] |
将一个或多个值插入到列表头部(左侧) |
LRANGE key start stop |
获取列表中指定范围的元素(start 起始索引,stop 结束索引,-1 表示最后一个元素) |
RPOP key |
移除并获取列表最后一个元素(右侧) |
LLEN key |
获取列表的长度(元素个数) |
列表结构示意图
1 2 3
| key ↓ [a, b, c, d] # 列表元素按插入顺序排列(示例:LPUSH key d c b a 后的结果)
|
- 左侧:列表头部(
LPUSH 插入,LPOP 移除)
- 右侧:列表尾部(
RPUSH 插入,RPOP 移除)

集合操作命令
Redis Set 是 String 类型的无序集合,集合成员唯一(不允许重复),支持交集、并集等集合操作。
| 命令格式 |
说明 |
SADD key member1 [member2] |
向集合添加一个或多个成员 |
SMEMBERS key |
返回集合中的所有成员(无序) |
SCARD key |
获取集合的成员数( cardinality ) |
SINTER key1 [key2] |
返回给定所有集合的交集(共同成员) |
SUNION key1 [key2] |
返回所有给定集合的并集(合并成员) |
SREM key member1 [member2] |
删除集合中一个或多个成员 |
Set 结构示意图
1 2 3
| key ↓ { a, b, c } # 无序集合,成员唯一(重复添加会自动去重)
|
- 无序性:成员存储顺序与插入顺序无关,
SMEMBERS 返回结果不固定;
- 唯一性:集合中不会有重复成员(
SADD 重复成员会忽略)。
有序集合操作命令
Redis 有序集合(Sorted Set)是 String 类型元素的集合,不允许重复成员,每个元素关联一个 double 类型的分数(score),并按分数从小到大排序。
| 命令格式 |
说明 |
ZADD key score1 member1 [score2 member2] |
向有序集合添加一个或多个成员(指定分数 score) |
ZRANGE key start stop [WITHSCORES] |
通过索引区间返回有序集合中指定区间内的成员(从小到大排序),WITHSCORES 可携带分数 |
ZINCRBY key increment member |
为有序集合中指定成员的分数加上增量 increment(可正可负) |
ZREM key member [member ...] |
移除有序集合中的一个或多个成员 |
有序集合结构示意图
1 2 3 4 5 6 7
| key ↓ { a: 0.1, // 成员a,分数0.1 c: 2.5, // 成员c,分数2.5 b: 3.0 // 成员b,分数3.0(按分数升序排序:a → c → b) }
|
- 有序性:成员按分数(score)从小到大排序,可通过索引快速获取排名区间的成员;
- 唯一性:成员不可重复,若添加已存在的成员,会更新其分数并重新排序。

分数越大排得越前
通用命令
| 命令格式 |
说明 |
KEYS pattern |
查找所有符合给定模式(pattern)的 key(如 KEYS user:* 匹配所有以 user: 开头的 key) |
EXISTS key |
检查给定 key 是否存在(返回 1 存在,0 不存在) |
TYPE key |
返回 key 所储存的值的数据类型(如 string、hash、list 等) |
DEL key |
删除指定 key(若 key 存在则删除,返回删除的 key 数量) |
例子:
在Java中使用Redis
Redis的Java客户端
Redis的Java客户端很多,常用的几种:
- Jedis
- Lettuce
- Spring Data Redis
SpringDataRedis是Spring的一部分,对Redis底层开发包进行了高度封装。
在Spring项目中,可以使用SpringDataRedis来简化操作。
Spring Data Redis
使用方式
操作步骤:
导入SpringDataRedis的maven坐标
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| spring: redis: host: localhost port: 6379 password: 123456 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 2000ms
|
配置Redis数据源
编写配置类,创建RedisTemplate对象
通过RedisTemplate对象操作Redis
操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Configuration @Slf4j public class RedisConfiguration {
@Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { log.info("开始创建redis模板类..."); RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
| package com.sky.test;
import lombok.val; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.core.*;
import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit;
@SpringBootTest public class SpringDataRedisTest { @Autowired private RedisTemplate redisTemplate;
@Test public void testRedisTemplate() { System.out.println(redisTemplate); ValueOperations valueOperations = redisTemplate.opsForValue(); HashOperations hashOperations = redisTemplate.opsForHash(); ListOperations listOperations = redisTemplate.opsForList(); SetOperations setOperations = redisTemplate.opsForSet(); ZSetOperations zSetOperations = redisTemplate.opsForZSet(); }
@Test public void testRedisString() { redisTemplate.opsForValue().set("city", "北京"); String city = (String) redisTemplate.opsForValue().get("city"); System.out.println(city); redisTemplate.opsForValue().set("code", "1234", 3, TimeUnit.MINUTES); redisTemplate.opsForValue().setIfAbsent("lock", "1"); redisTemplate.opsForValue().setIfAbsent("lock", "2"); }
@Test public void testRedisHash() { HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.put("user1", "name", "张三"); hashOperations.put("user1", "age", "12"); String name = (String) hashOperations.get("user1", "name"); System.out.println(name); Set keys = hashOperations.keys("user1"); System.out.println(keys); List values = hashOperations.values("user1"); System.out.println(values);
hashOperations.delete("user1", "age");
}
@Test public void testRedislist() { ListOperations listOperations = redisTemplate.opsForList(); listOperations.leftPushAll("mylist", "a", "b", "c"); listOperations.leftPush("mylist", "d"); List mylist = listOperations.range("mylist", 0, -1); System.out.println(mylist); listOperations.rightPop("mylist");
Long size = listOperations.size("mylist"); System.out.println(size); }
@Test public void testRedisSet(){ SetOperations setOperations = redisTemplate.opsForSet(); setOperations.add( "set1","a","b","c","d"); setOperations.add( "set2","a","b","x","y");
Set members =setOperations.members("set1"); System.out.println(members);
Long size=setOperations.size( "set1"); System.out.println(size);
Set intersect = setOperations.intersect("set1", "set2"); System.out.println(intersect);
Set union = setOperations.union("set1", "set2"); System.out.println(union); setOperations.remove("set1","a","b"); }
@Test public void testRedisZSet(){ ZSetOperations zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("zset1", "a", 10); zSetOperations.add("zset1", "b", 20); zSetOperations.add("zset1", "c", 30);
Set zset1 = zSetOperations.range("zset1", 0, -1); System.out.println(zset1);
zSetOperations.incrementScore("zset1", "a", 10); System.out.println(zset1);
zSetOperations.remove("zset1", "a"); System.out.println(zset1); }
@Test public void testRedisCommon(){ Set keys =redisTemplate.keys( "*"); System.out.println(keys); Boolean name=redisTemplate.hasKey("name"); Boolean set1=redisTemplate.hasKey("set1"); for (Object key : keys) { DataType type = redisTemplate.type(key); System.out.println(type.name()); } redisTemplate.delete("mylist"); } }
|
店铺营业状态设置
需求分析和设计



代码
完善
HttpClient
介绍
就是可以再Java里面写http协议并发送
HttpClient 介绍
HttpClient 是 Apache Jakarta Common 下的子项目,提供高效、功能丰富的 HTTP 协议客户端编程工具包,支持 HTTP 协议最新版本和建议。
Maven 依赖
1 2 3 4 5
| XML<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency>
|
核心 API
HttpClient
HttpClients
CloseableHttpClient
HttpGet
HttpPost
发送请求步骤
- 创建 HttpClient 对象
- 创建 Http 请求对象(如 HttpGet/HttpPost)
- 调用 HttpClient 的 execute 方法发送请求
入门案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| package com.sky.test;
import com.alibaba.fastjson.JSONObject; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException; import java.io.UnsupportedEncodingException;
@SpringBootTest public class HttpClientTest {
@Test public void testGet() throws IOException { 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(); }
@Test public void testPost() throws IOException { 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();
} }
|
微信小程序
介绍




准备工作

入门案例


微信登陆

需求分析

商品浏览
需求分析
接口设计:
- 查询分类
- 根据分类id查询菜品
- 根据分类id查询套餐
- 根据套餐id查询包含的菜品




菜品缓存
问题说明


实现思路

在管理端增删改的时候要进行清理缓存,以防数据不一致
缓存套餐
Spring Cache
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
Maven 依赖
1 2 3 4 5
| XML<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version> </dependency>
|
常用注解
| 注解 |
说明 |
@EnableCaching |
开启缓存注解功能,通常加在启动类上 |
@Cacheable |
在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
@CachePut |
将方法的返回值放到缓存中 |
@CacheEvict |
将一条或多条数据从缓存中删除 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @PostMapping @CachePut(cacheNames = "userCache", key = "#user.id") public User save(@RequestBody User user){ userMapper.insert(user); return user; }
@CachePut(cacheNames = "userCache", key = "#user.id")
@GetMapping @Cacheable(cacheNames = "userCache", key = "#id") public User getById(Long id){ User user = userMapper.getById(id); return user; }
@DeleteMapping @CacheEvict(cacheNames = "userCache", key = "#id") public void deleteById(Long id){ userMapper.deleteById(id); }
@DeleteMapping("/delAll") @CacheEvict(cacheNames = "userCache", allEntries = true) public void deleteAll(){ userMapper.deleteAll(); }
|
实现思路
具体的实现思路如下:
- 导入SpringCache和Redis相关maven坐标
- 在启动类上加入@EnableCaching注解,开启缓存注解功能
- 在用户端接口SetmealController的list方法上加入@Cacheable注解
- 在管理端接口SetmealController的save、delete、update、startOrStop等方法上加入CacheEvict注解
添加购物车
需求分析

数据库设计
- 作用:暂时存放所选商品的地方
- 选的什么商品
- 每个商品都买了几个
- 不同用户的购物车需要区分开
| 字段名 |
数据类型 |
说明 |
备注 |
| id |
bigint |
主键 |
自增 |
| name |
varchar(32) |
商品名称 |
冗余字段 |
| image |
varchar(255) |
商品图片路径 |
冗余字段 |
| user_id |
bigint |
用户id |
逻辑外键 |
| dish_id |
bigint |
菜品id |
逻辑外键 |
| setmeal_id |
bigint |
套餐id |
逻辑外键 |
| dish_flavor |
varchar(50) |
菜品口味 |
|
| number |
int |
商品数量 |
|
| amount |
decimal(10,2) |
商品单价 |
冗余字段 |
| create_time |
datetime |
创建时间 |
|
查看购物车
需求分析

清空购物车
需求分析

地址簿
需求分析
接口设计:
- 新增地址
- 查询当前登录用户的所有地址信息
- 查询默认地址
- 根据id修改地址
- 根据id删除地址
- 根据id查询地址
- 设置默认地址
用户下单
需求分析




数据库设计:订单表orders
| 字段名 |
数据类型 |
说明 |
备注 |
| id |
bigint |
主键 |
自增 |
| number |
varchar(50) |
订单号 |
|
| status |
int |
订单状态 |
1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 |
| user_id |
bigint |
用户id |
逻辑外键 |
| address_book_id |
bigint |
地址id |
逻辑外键 |
| order_time |
datetime |
下单时间 |
|
| checkout_time |
datetime |
付款时间 |
|
| pay_method |
int |
支付方式 |
1微信支付 2支付宝支付 |
| pay_status |
tinyint |
支付状态 |
0未支付 1已支付 2退款 |
| amount |
decimal(10,2) |
订单金额 |
|
| remark |
varchar(100) |
备注信息 |
|
| phone |
varchar(11) |
手机号 |
冗余字段 |
| address |
varchar(255) |
详细地址信息 |
冗余字段 |
| consignee |
varchar(32) |
收货人 |
|
| cancel_reason |
varchar(255) |
订单取消原因 |
|
| rejection_reason |
varchar(255) |
拒单原因 |
|
| cancel_time |
datetime |
订单取消时间 |
|
| estimated_delivery_time |
datetime |
预计送达时间 |
|
| delivery_status |
tinyint |
配送状态 |
1立即送出 0选择具体时间 |
| delivery_time |
datetime |
送达时间 |
|
| pack_amount |
int |
打包费 |
|
| tableware_number |
int |
餐具数量 |
|
| tableware_status |
tinyint |
餐具数量状态 |
1按餐量提供 0选择具体数量 |
数据库设计:订单明细表order_detail
| 字段名 |
数据类型 |
说明 |
备注 |
| id |
bigint |
主键 |
自增 |
| name |
varchar(32) |
商品名称 |
冗余字段 |
| image |
varchar(255) |
商品图片路径 |
冗余字段 |
| order_id |
bigint |
订单id |
逻辑外键 |
| dish_id |
bigint |
菜品id |
逻辑外键 |
| setmeal_id |
bigint |
套餐id |
逻辑外键 |
| dish_flavor |
varchar(50) |
菜品口味 |
|
| number |
int |
商品数量 |
|
| amount |
decimal(10,2) |
商品单价 |
|


订单支付


流程



微信支付准备工作
调用过程如何保证数据安全?
微信后台如何调用到商户系统?
调用微信支付的API需要有公网ip

Spring Task
介绍
定时任务

应用场景:
- 信用卡每月还款提醒
- 银行贷款每月还款提醒
- 火车票售票系统处理未支付订单
- 入职纪念日为用户发送通知
只要是需要定时处理的场景都可以使用SpringTask
cron表达式
Cron表达式是一个字符串,用于定义任务触发的时间,由6或7个域组成(空格分隔),每个域代表特定时间含义。
1. 构成规则
| 域 |
含义 |
是否可选 |
| 第1域 |
秒 |
必选 |
| 第2域 |
分钟 |
必选 |
| 第3域 |
小时 |
必选 |
| 第4域 |
日 |
必选 |
| 第5域 |
月 |
必选 |
| 第6域 |
周 |
必选 |
| 第7域 |
年 |
可选 |
2. 示例:2022年10月12日上午9点整
| 秒 |
分钟 |
小时 |
日 |
月 |
周 |
年 |
| 0 |
0 |
9 |
12 |
10 |
? |
2022 |
对应的Cron表达式: 0 0 9 12 10 ? 2022
说明
- 周域(第6域) 使用
? 表示不指定(避免与“日”域冲突,两者通常只指定一个)。
- 年域(第7域) 可省略,省略后表达式为6个域(如
0 0 9 12 10 ? 表示每年10月12日9点触发)。

用ai和网站就行
入门案例

订单状态定时处理
需求分析
用户下单后可能存在的异常状态
- 待支付超时:下单后未支付,订单一直处于“待支付”状态。
- 派送中未完成:用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态。
定时任务解决方案
通过定时任务自动修正异常订单状态,具体逻辑如下:
| 定时任务场景 |
触发频率 |
判定条件 |
处理逻辑 |
| 支付超时订单处理 |
每分钟检查一次 |
下单后超过15分钟仍未支付 |
订单状态改为“已取消” |
| 派送中订单自动完成 |
每天凌晨1点检查 |
订单状态为“派送中”(默认用户已收货) |
订单状态改为“已完成” |
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| package com.sky.task;
import com.sky.entity.Orders; import com.sky.mapper.OrderMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component;
import java.time.LocalDateTime; import java.util.List;
@Component @Slf4j public class OrderTask {
@Autowired private OrderMapper orderMapper;
@Scheduled(cron = "0 * * * * ? ") public void processTimeoutOrder(){ log.info("处理订单超时,{}", LocalDateTime.now()); LocalDateTime time = LocalDateTime.now().plusMinutes(-15); List<Orders> byStatusAndOrderTime = orderMapper.getByStatusAndOrderTime(Orders.PENDING_PAYMENT, time); if (byStatusAndOrderTime != null && byStatusAndOrderTime.size() > 0){ for (Orders order : byStatusAndOrderTime) { order.setStatus(Orders.CANCELLED); order.setCancelReason("订单超时取消"); order.setCancelTime(LocalDateTime.now()); orderMapper.update(order); } } }
@Scheduled(cron = "0 0 1 * * ? ") public void processDeliveryOrder(){ log.info("处理派送中的订单,{}", LocalDateTime.now()); LocalDateTime time = LocalDateTime.now().plusMinutes(-60); List<Orders> statusAndOrderTime = orderMapper.getByStatusAndOrderTime(Orders.DELIVERY_IN_PROGRESS, time); if (statusAndOrderTime != null && statusAndOrderTime.size() > 0){ for (Orders order : statusAndOrderTime) { order.setStatus(Orders.CANCELLED); orderMapper.update(order); } } }
}
|
WebSocket(网络协议)
介绍
不需要刷新

应用场景:
- 视频弹幕
- 网页聊天
- 体育实况更新
- 股票基金报价实时更新
来单提醒
需求分析
实现目标
通过WebSocket实现管理端页面与服务端的长连接,实时推送订单相关消息(如来单提醒、客户催单)。
核心设计
- 长连接保持
- 通过WebSocket协议实现管理端页面与服务端的持久连接,避免HTTP短轮询的性能损耗。
- 消息推送触发时机
- 当客户完成支付后,服务端调用WebSocket API主动向管理端推送消息。
- 客户端消息处理
- 管理端浏览器解析服务端推送的JSON消息,根据消息类型触发消息提示和语音播报(如来单提醒时自动播放语音)。
消息数据格式(JSON)
服务端推送的消息固定包含以下字段:
| 字段名 |
说明 |
取值示例 |
type |
消息类型 |
1(来单提醒)、2(客户催单) |
orderId |
订单ID |
1001(关联具体订单) |
content |
消息内容 |
"您有新的订单,请及时处理!" |
客户催单
需求分析
核心设计
长连接保持:通过WebSocket实现管理端页面和服务端保持长连接状态。
消息触发时机:当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息。
客户端处理:客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报。
数据格式约定
:服务端发送给客户端浏览器的JSON数据格式包含以下字段:
type:消息类型(1为来单提醒,2为客户催单)
orderId:订单id
content:消息内容

Apache ECharts(前端的技术)
介绍






营业额统计
需求分析


用户统计
需求分析


订单统计
需求分析


销量排名统计
需求分析


工作台
需求分析

名词解释:
- 营业额:已完成订单的总金额
- 有效订单:已完成订单的数量
- 订单完成率:有效订单数/总订单数*100%
- 平均客单价:营业额/有效订单数
- 新增用户:新增用户的数量
接口设计:
- 今日数据接口
- 订单管理接口
- 菜品总览接口
- 套餐总览接口
- 订单搜索 (已完成)
- 各个状态的订单数量统计 (已完成)




ApachePOI
## 介绍

ApachePOI的应用场景:
- 银行网银系统导出交易明细
- 各种业务系统导出Excel报表
- 批量导入业务数据
ApachePOl_入门案例_
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| package com.sky.test;
import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream;
public class POITest {
public static void write() throws Exception{ XSSFWorkbook excel = new XSSFWorkbook(); XSSFSheet sheet = excel.createSheet("info"); XSSFRow row = sheet.createRow(1); row.createCell(1).setCellValue("姓名"); row.createCell(2).setCellValue("城市");
row = sheet.createRow(2); row.createCell(1).setCellValue("张三"); row.createCell(2).setCellValue("北京");
row = sheet.createRow(3); row.createCell(1).setCellValue("李四"); row.createCell(2).setCellValue("南京");
FileOutputStream out = new FileOutputStream(new File("D:\\info.xlsx")); excel.write(out);
out.close(); excel.close(); }
public static void read() throws Exception{ InputStream in = new FileInputStream(new File("D:\\info.xlsx"));
XSSFWorkbook excel = new XSSFWorkbook(in); XSSFSheet sheet = excel.getSheetAt(0);
int lastRowNum = sheet.getLastRowNum();
for (int i = 1; i <= lastRowNum ; i++) { XSSFRow row = sheet.getRow(i); String cellValue1 = row.getCell(1).getStringCellValue(); String cellValue2 = row.getCell(2).getStringCellValue(); System.out.println(cellValue1 + " " + cellValue2); }
in.close(); excel.close(); }
public static void main(String[] args) throws Exception { read(); } }
|
导出运营数据Excel报表
需求分析和设计


先设计好一个模板文件

代码开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
|
@Override public void exportBusinessData(HttpServletResponse response) { LocalDate beginDate = LocalDate.now().minusDays(30); LocalDate endDate = LocalDate.now().plusDays(1); LocalDateTime beginTime = LocalDateTime.of(beginDate, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(endDate, LocalTime.MAX); BusinessDataVO businessData = workspaceService.getBusinessData(beginTime, endTime);
InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx"); try { XSSFWorkbook excel = new XSSFWorkbook(in); XSSFSheet sheet = excel.getSheet("Sheet1"); sheet.getRow(1).getCell(1).setCellValue(beginDate + "至" + endDate); XSSFRow row = sheet.getRow(3); row.getCell(2).setCellValue(businessData.getTurnover()); row.getCell(4).setCellValue(businessData.getOrderCompletionRate()); row.getCell(6).setCellValue(businessData.getNewUsers());
row = sheet.getRow(4); row.getCell(2).setCellValue(businessData.getValidOrderCount()); row.getCell(4).setCellValue(businessData.getUnitPrice());
for (int i=0;i<30;i++){ LocalDate date = beginDate.plusDays(i); BusinessDataVO businessData1 = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX)); row = sheet.getRow(7 + i); row.getCell(1).setCellValue(date.toString()); row.getCell(2).setCellValue(businessData1.getTurnover()); row.getCell(3).setCellValue(businessData1.getValidOrderCount()); row.getCell(4).setCellValue(businessData1.getOrderCompletionRate()); row.getCell(5).setCellValue(businessData1.getUnitPrice()); row.getCell(6).setCellValue(businessData1.getNewUsers()); } ServletOutputStream out = response.getOutputStream(); excel.write(out); out.close(); excel.close(); } catch (IOException e) { throw new RuntimeException(e); } }
|