软件开发整体介绍
软件开发流程
需求分析
- 需求规格说明书:形成文档介绍
- 产品原型:通过静态网页展示业务功能
设计
- UI设计:将页面各个方面的细节设计完善
- 数据库设计:先设计E-R图,然后再具体设计表的字段和类型等详细细节
- 接口设计:就是设计接口文档(使用Apifox)
编码
- 项目代码:业务逻辑的代码
- 单元测试:用于测试项目代码的单元测试(开发人员自测)
测试
- 测试用例:以接口为单位编写测试用例
- 测试报告:对测试情况进行报告
上线运维
- 软件环境安装:安装运行环境
- 配置:进行一些线上的配置,如Nginx
角色分工
- 项目经理:对整个项目负责,任务分配、把控进度
- 产品经理:进行需求调研,输出需求调研文档、产品原型等
- UI设计师:根据产品原型输出界面效果图
- 架构师:项目整体架构设计、技术选型等
- 开发工程师:代码实现
- 测试工程师:编写测试用例,输出测试报告
- 运维工程师:软件环境搭建、项目上线
软件环境
- 开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
- 测试环境(testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
- 生产环境(production):即线上环境,正式提供对外服务的环境
Apifox等相关应用也应该采用与此统一的三种软件环境
苍穹外卖项目介绍
项目介绍
产品原型
技术选型
开发环境搭建
前端环境搭建
后端环境搭建
注意,此处的搭建方式和目录结构并不是最佳实践,实际目录结构要根据具体项目来进行具体的架构设计
搭建好之后提交git仓库
数据库设计文档
注意,这里的数据库设计并非最佳实践,实际上数据库的表名的最佳实践应该为
模块名_功能点
,如此更容易后期拓展其他模块另外,用户名(账号)和密码这里做的是不错的,都应该是
varchar
,有的项目中账号使用数字,是不正确的做法
序号 | 数据表名 | 中文名称 |
---|---|---|
1 | employee | 员工表 |
2 | category | 分类表 |
3 | dish | 菜品表 |
4 | dish_flavor | 菜品口味表 |
5 | setmeal | 套餐表 |
6 | setmeal_dish | 套餐菜品关系表 |
7 | user | 用户表 |
8 | address_book | 地址表 |
9 | shopping_cart | 购物车表 |
10 | orders | 订单表 |
11 | order_detail | 订单明细表 |
1. employee
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 |
2. category
category表为分类表,用于存储商品的分类信息。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 分类名称 | 唯一 |
type | int | 分类类型 | 1菜品分类 2套餐分类 |
sort | int | 排序字段 | 用于分类数据的排序 |
status | int | 状态 | 1启用 0禁用 |
create_time | datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人id | |
update_user | bigint | 最后修改人id |
3. dish
dish表为菜品表,用于存储菜品的信息。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 菜品名称 | 唯一 |
category_id | bigint | 分类id | 逻辑外键 |
price | decimal(10,2) | 菜品价格 | |
image | varchar(255) | 图片路径 | |
description | varchar(255) | 菜品描述 | |
status | int | 售卖状态 | 1起售 0停售 |
create_time | datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人id | |
update_user | bigint | 最后修改人id |
4. dish_flavor
dish_flavor表为菜品口味表,用于存储菜品的口味信息。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
dish_id | bigint | 菜品id | 逻辑外键 |
name | varchar(32) | 口味名称 | |
value | varchar(255) | 口味值 |
5. setmeal
setmeal表为套餐表,用于存储套餐的信息。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 套餐名称 | 唯一 |
category_id | bigint | 分类id | 逻辑外键 |
price | decimal(10,2) | 套餐价格 | |
image | varchar(255) | 图片路径 | |
description | varchar(255) | 套餐描述 | |
status | int | 售卖状态 | 1起售 0停售 |
create_time | datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人id | |
update_user | bigint | 最后修改人id |
6. setmeal_dish
setmeal_dish表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
setmeal_id | bigint | 套餐id | 逻辑外键 |
dish_id | bigint | 菜品id | 逻辑外键 |
name | varchar(32) | 菜品名称 | 冗余字段 |
price | decimal(10,2) | 菜品单价 | 冗余字段 |
copies | int | 菜品份数 |
7. user
user表为用户表,用于存储C端用户的信息。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
openid | varchar(45) | 微信用户的唯一标识 | |
name | varchar(32) | 用户姓名 | |
phone | varchar(11) | 手机号 | |
sex | varchar(2) | 性别 | |
id_number | varchar(18) | 身份证号 | |
avatar | varchar(500) | 微信用户头像路径 | |
create_time | datetime | 注册时间 |
8. address_book
address_book表为地址表,用于存储C端用户的收货地址信息。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
user_id | bigint | 用户id | 逻辑外键 |
consignee | varchar(50) | 收货人 | |
sex | varchar(2) | 性别 | |
phone | varchar(11) | 手机号 | |
province_code | varchar(12) | 省份编码 | |
province_name | varchar(32) | 省份名称 | |
city_code | varchar(12) | 城市编码 | |
city_name | varchar(32) | 城市名称 | |
district_code | varchar(12) | 区县编码 | |
district_name | varchar(32) | 区县名称 | |
detail | varchar(200) | 详细地址信息 | 具体到门牌号 |
label | varchar(100) | 标签 | 公司、家、学校 |
is_default | tinyint(1) | 是否默认地址 | 1是 0否 |
9. shopping_cart
shopping_cart表为购物车表,用于存储C端用户的购物车信息。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
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 | 创建时间 |
10. orders
orders表为订单表,用于存储C端用户的订单数据。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
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) | 详细地址信息 | |
user_name | varchar(32) | 用户姓名 | |
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选择具体数量 |
11. order_detail
order_detail表为订单明细表,用于存储C端用户的订单明细数据。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
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) | 商品单价 |
前后端联调
前后端都搭建好之后,通过实现登录等基础功能,进行前后端的联调,尽早联调防止后续出现问题
Nginx反向代理
正向代理:代理客户端(隐藏用户)
反向代理:代理服务端(隐藏服务器)
补充知识
最好是前端和后端都进行加密,如此一来更加安全
实际开发过程中,最好是结合使用HTTP状态码、业务状态码以及业务信息,HTTP状态码负责划分错误大体信息,业务状态码负责明确错误详细信息,业务信息用来提供用户友好型的提示信息
另外,业务状态码可以采用阿里文档中的状态码,其中没有的状态码可以自己新增,需要遵守文档状态码的分类模式
因此Result枚举中应该如下所示:
全局自定义异常捕获应该如下所示:
前后端分离开发流程
使用Swagger进行接口测试
注意,实际开发过程中使用Apifox进行调试和测试即可,比这个方便很多很多
但是实际开发过程中,Swagger还是需要使用的,因为其放在源码中,可以便捷的生成接口文档,算是一种冗余策略(Swagger注解和注释都要有)
业务开发
JWT令牌完整流程
JWT令牌原理
JWT令牌分为三部分:头部,载荷,签名
头部和载荷是不加密的,而签名是通过不可逆的签名算法获取到的密文
签名=不可逆的签名算法(头部+载荷,密钥)
JWT令牌=头部.载荷.不可逆的签名算法(头部+载荷,密钥)
前端与后端交互时,将JWT令牌给到后端,后端通过令牌中公开的头部和载荷,以及只有后端自己知道的密钥,再次进行签名,将此签名和前端传过来的前面对比一下,一样的话就通过校验,不一样就不通过
注:不可逆的签名算法指的是计算过程只能从左到右,不能从右到左的算法,例如36×78=2808,这个算式从左到右很好算,但是你只知道2808,想要知道算式左侧是啥就很难办)
如何获取到JWT令牌中存储的信息
每个请求的处理都是独立的线程,而ThreadLocal(线程局部存储)提供了一种独立存储每个线程局部变量的方式,其提供的方法可以以线程独立的方式存取当前线程的数据,因此我们可以在解析JWT的时候,可以将JWT信息放到ThreadLocal中,从而使当前线程随时能访问JWT中的信息
public class JwtContext {
// 使用 ThreadLocal 来存储每个线程的 JWT 信息
private static ThreadLocal<String> jwtToken = new ThreadLocal<>();
// 设置当前线程的 JWT 信息
public static void setJwtToken(String token) {
jwtToken.set(token);
}
// 获取当前线程的 JWT 信息
public static String getJwtToken() {
return jwtToken.get();
}
// 清理线程的 JWT 信息
public static void clear() {
jwtToken.remove();
}
}
1. ThreadLocal
的工作机制
ThreadLocal
为每个线程提供一个独立的存储空间。每个线程在访问 ThreadLocal
时,都会看到与自己相关的数据,而不会与其他线程共享数据。
- 当你通过
ThreadLocal.set()
方法存储数据时,数据会被存储在当前线程的局部存储空间中。 - 通过
ThreadLocal.get()
获取数据时,只能访问当前线程的存储数据。
2. 线程生命周期
- 当线程结束时,ThreadLocal 中存储的数据会被销毁。这意味着如果某个请求处理结束,线程被回收,那么该线程在
ThreadLocal
中存储的任何数据都会随之销毁。 - 如果 线程池复用线程(例如在 Web 应用中使用的线程池),则线程并不会完全销毁,而是会在下一次使用时继续工作。在这种情况下,ThreadLocal 数据可能在下一次线程复用时仍然存在,除非你显式地调用
ThreadLocal.remove()
来清除它。
3. 避免内存泄漏
在某些环境中,尤其是线程池复用的情况下,线程的生命周期比请求的生命周期长。如果在请求结束后没有显式地清除 ThreadLocal
中的内容,它可能会导致内存泄漏,因为 ThreadLocal
会保持对数据的引用,而这些线程可能长时间存在于线程池中。
为了避免这种情况,应该显式地清除 ThreadLocal
中的数据
4. 总结
- 当线程结束时,
ThreadLocal
存储的数据会被销毁,不会占用额外的内存。 - 但如果使用 线程池,在复用线程时,
ThreadLocal
中的内容不会自动清除,这时需要手动调用ThreadLocal.remove()
来确保清理,避免内存泄漏。
因此,为了确保每个请求处理完成后 ThreadLocal
中的数据被清理,应该在请求处理的结束阶段调用 ThreadLocal.remove()
,尤其是在使用线程池的情况下。