淘车湾项目实战一_项目分析_数据库设计_若依脚手架改造
淘车湾项目实战(一):项目分析、数据库设计与若依脚手架改造
本章位置:第三阶段 Java 企业项目 + AI 助手
对应课程:淘车湾项目实战——使用 AI 实现项目分析、数据库设计及脚手架改造
前置知识:SpringBoot、Vue3、MyBatis、Redis、Git、Activiti 7、若依脚手架、RBAC、数据权限、POI
后续衔接:养修预约、服务单项与套餐、结算单、结算单明细、Activiti 审批、我的待办/已办、流程图高亮
学习目标:从 0 到 1 完成一个企业业务项目的前期分析、数据库设计、若依脚手架改造与基础工程搭建,为后续业务模块开发建立统一的数据模型、权限模型和代码结构。
一、为什么这一章非常重要
很多初学者做项目时会直接:
打开 IDEA
↓
新建 Controller
↓
开始写 CRUD
这是非常容易返工的。
企业项目正确顺序应该是:
需求分析
↓
业务流程
↓
角色与权限
↓
核心实体
↓
数据库设计
↓
状态设计
↓
接口边界
↓
若依脚手架改造
↓
代码生成
↓
业务开发
如果前面设计错了:
后面 Controller 写得越多
返工越多
二、淘车湾项目是什么
本课程中的“淘车湾”可以理解为:
汽车养护 / 维修 / 服务管理后台
核心业务包括:
客户车辆
养修预约
服务单项
服务套餐
服务订单 / 工单
结算单
结算明细
审批流程
我的待办
我的已办
流程图
三、核心业务链路
完整主链路:
客户预约
↓
门店确认
↓
生成服务单 / 工单
↓
选择服务单项 / 套餐
↓
技师执行服务
↓
形成结算单
↓
结算明细
↓
满足条件则进入审批
↓
审批通过
↓
完成结算
四、项目角色
第一版建议保留:
系统管理员
门店管理员
服务顾问
技师
财务
审批人
不要第一版就加入:
供应商
保险公司
客户 App
加盟商总部
仓库管理员
营销人员
项目先完成:
课程核心业务
五、角色职责
系统管理员
负责:
系统用户
角色
菜单
字典
基础配置
门店管理员
负责:
门店业务查看
人员管理
服务配置
预约管理
业务统计
服务顾问
负责:
客户预约
车辆接待
创建服务单
选择服务项目
生成结算单
技师
负责:
查看分配任务
填写服务结果
完成维修 / 保养
财务
负责:
结算审核
金额确认
支付状态
审批人
负责:
特殊金额
优惠
异常结算
等审批。
六、为什么角色不直接写死
错误:
if (
"财务".equals(
currentUser.getRoleName()
)
) {
}
应该:
若依 Role
+
Permission Code
例如:
car:appointment:list
car:settlement:approve
car:service:edit
七、项目模块划分
建议业务模块:
客户车辆模块
预约模块
服务项目模块
套餐模块
服务单模块
结算模块
审批模块
八、若依 Maven 模块怎么改
如果课程项目不大:
可以新增一个业务模块:
ruoyi-car
目录:
ruoyi-car
负责:
淘车湾全部业务
九、为什么不建议一开始拆很多 Maven 模块
例如:
ruoyi-customer
ruoyi-appointment
ruoyi-service
ruoyi-order
ruoyi-settlement
对于课程项目:
过度拆分
会增加:
Maven 依赖
包引用
维护成本
十、推荐模块结构
ruoyi-car
└─ src/main/java
└─ com.ruoyi.car
├─ domain
├─ mapper
├─ service
├─ service.impl
└─ vo
Controller:
可以继续放 ruoyi-admin
经典若依很多业务:
Controller 在 admin
业务:
Service / Mapper / Domain
放业务模块
十一、为什么 Controller 放 ruoyi-admin
因为:
ruoyi-admin
就是 Web 入口
这样:
统一管理 HTTP API
十二、业务包建议
com.ruoyi.car
子包:
customer
vehicle
appointment
serviceitem
packageitem
workorder
settlement
workflow
十三、不要使用 service 作为实体包名
Java 中:
service
本来已经表示:
业务层
汽车“服务项目”建议:
serviceitem
不要:
service.service
十四、数据库前缀
推荐:
car_
例如:
car_customer
car_vehicle
car_appointment
car_service_item
car_service_package
car_work_order
car_settlement
十五、为什么不用 sys_
sys_:
若依系统表
业务表:
应该独立
十六、数据库设计原则
第一原则:
一个表描述一个核心业务实体
十七、第二原则
不要把:
所有业务信息
塞到一张超级大表。
十八、第三原则
多对多:
使用关联表
十九、第四原则
状态字段:
用稳定 Code
不要数据库存:
“待审批”
“已完成”
更推荐:
0
1
2
或:
DRAFT
PENDING
APPROVED
二十、第五原则
金额:
DECIMAL
不要:
float / double
二十一、第六原则
关键业务:
保留历史快照
例如结算单里的:
服务名称
服务单价
不要每次:
实时查 service_item
否则服务项目价格以后修改:
历史结算单金额会“变”
二十二、系统用户和员工业务关系
若依:
sys_user
保存:
登录账号
淘车湾业务可以直接:
user_id
引用 sys_user.user_id。
二十三、要不要再建员工表
课程第一版:
不强制
可以使用:
sys_user + sys_dept + sys_post
表示:
员工
部门
岗位
二十四、门店怎么表示
如果:
门店和部门层级相对简单
可以把门店:
建成 sys_dept
例如:
淘车湾总部
├─ 沈阳店
├─ 大连店
└─ 鞍山店
二十五、这样有什么好处
直接复用:
sys_dept
@DataScope
部门树
用户部门
二十六、什么时候独立 car_store
如果门店有很多:
营业时间
地址
经纬度
客服电话
门店图片
经营状态
则更适合:
car_store
二十七、课程项目推荐
如果只是后台业务:
先复用 sys_dept 表示门店
减少表数量。
二十八、核心表清单
本阶段先设计:
car_customer
car_vehicle
car_appointment
car_service_item
car_service_package
car_service_package_item
car_work_order
car_work_order_item
car_settlement
car_settlement_item
car_approval_record
Activiti:
ACT_*
由流程引擎自己管理。
二十九、car_customer
客户表。
字段建议:
customer_id
customer_name
phone
gender
id_card
address
remark
status
create_by
create_time
update_by
update_time
三十、客户表 SQL
CREATE TABLE car_customer (
customer_id BIGINT NOT NULL AUTO_INCREMENT,
customer_name VARCHAR(50) NOT NULL,
phone VARCHAR(20) NOT NULL,
gender CHAR(1) DEFAULT '2',
id_card VARCHAR(30),
address VARCHAR(255),
status CHAR(1) DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(64),
create_time DATETIME,
update_by VARCHAR(64),
update_time DATETIME,
PRIMARY KEY (customer_id),
KEY idx_customer_phone (phone)
);
三十一、手机号要不要 UNIQUE
不一定。
现实:
一个手机号
可能登记多个家庭成员
课程如果明确:
一个手机号只允许一个客户
可以:
UNIQUE KEY uk_customer_phone(phone)
三十二、不要没有业务依据就乱加唯一约束
唯一约束:
是业务规则
不是:
“看起来高级”
三十三、car_vehicle
车辆表。
核心:
一个客户
可以有多辆车
关系:
Customer 1 : N Vehicle
三十四、车辆字段
vehicle_id
customer_id
plate_no
vin
brand
series
model
vehicle_year
mileage
color
status
三十五、车辆 SQL
CREATE TABLE car_vehicle (
vehicle_id BIGINT NOT NULL AUTO_INCREMENT,
customer_id BIGINT NOT NULL,
plate_no VARCHAR(20) NOT NULL,
vin VARCHAR(50),
brand VARCHAR(50),
series VARCHAR(50),
model VARCHAR(100),
vehicle_year INT,
mileage INT,
color VARCHAR(30),
status CHAR(1) DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(64),
create_time DATETIME,
update_by VARCHAR(64),
update_time DATETIME,
PRIMARY KEY (vehicle_id),
UNIQUE KEY uk_vehicle_plate_no (plate_no),
KEY idx_vehicle_customer_id (customer_id)
);
三十六、VIN 是否 UNIQUE
理论上:
车辆 VIN 唯一
但课程数据可能:
允许空
MySQL UNIQUE:
允许多个 NULL
可以:
UNIQUE KEY uk_vehicle_vin(vin)
三十七、里程放车辆表还是预约表
两边都可以存在:
vehicle.mileage
保存最新里程
appointment.arrival_mileage
保存本次快照
三十八、为什么要快照
车辆最新里程:
以后会变化
历史预约:
必须知道当时里程
三十九、car_appointment
养修预约表。
核心流程:
客户预约
↓
门店确认
↓
到店
↓
生成服务单
四十、预约字段
appointment_id
appointment_no
customer_id
vehicle_id
dept_id
appointment_time
appointment_type
description
arrival_mileage
status
advisor_user_id
cancel_reason
四十一、预约编号
不要:
只依赖 appointment_id
页面更适合显示:
AP202609100001
四十二、为什么业务编号和数据库 ID 分开
数据库 ID:
内部关联
业务编号:
用户查看
打印
搜索
对账
四十三、预约状态
建议:
0 待确认
1 已确认
2 已到店
3 已转服务单
4 已完成
5 已取消
四十四、状态不要设计得过细
初学容易:
已预约
等待确认
确认中
门店已读
门店准备中
客户准备中
过多状态:
流程很难维护
四十五、预约 SQL
CREATE TABLE car_appointment (
appointment_id BIGINT NOT NULL AUTO_INCREMENT,
appointment_no VARCHAR(40) NOT NULL,
customer_id BIGINT NOT NULL,
vehicle_id BIGINT NOT NULL,
dept_id BIGINT NOT NULL,
appointment_time DATETIME NOT NULL,
appointment_type VARCHAR(20),
description VARCHAR(1000),
arrival_mileage INT,
advisor_user_id BIGINT,
status CHAR(1) NOT NULL DEFAULT '0',
cancel_reason VARCHAR(500),
remark VARCHAR(500),
create_by VARCHAR(64),
create_time DATETIME,
update_by VARCHAR(64),
update_time DATETIME,
PRIMARY KEY (appointment_id),
UNIQUE KEY uk_appointment_no (appointment_no),
KEY idx_appointment_customer (customer_id),
KEY idx_appointment_vehicle (vehicle_id),
KEY idx_appointment_dept_time (dept_id, appointment_time),
KEY idx_appointment_status (status)
);
四十六、预约索引为什么这样设计
常见查询:
按门店
按预约时间
按状态
按车辆
所以:
建立常用过滤索引
四十七、不要给每个字段都建索引
索引:
会占空间
影响 INSERT/UPDATE
只给:
常查
常关联
高选择性
字段。
四十八、car_service_item
服务单项。
例如:
机油更换
轮胎换位
刹车检查
空调清洗
四十九、字段
service_item_id
item_code
item_name
category
standard_price
estimated_minutes
status
五十、SQL
CREATE TABLE car_service_item (
service_item_id BIGINT NOT NULL AUTO_INCREMENT,
item_code VARCHAR(50) NOT NULL,
item_name VARCHAR(100) NOT NULL,
category VARCHAR(30),
standard_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
estimated_minutes INT,
status CHAR(1) DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(64),
create_time DATETIME,
update_by VARCHAR(64),
update_time DATETIME,
PRIMARY KEY (service_item_id),
UNIQUE KEY uk_service_item_code (item_code),
KEY idx_service_item_status (status)
);
五十一、金额类型
DECIMAL(10,2)
表示:
最大约 99999999.99
课程够用。
五十二、不要金额用 DOUBLE
浮点数:
存在二进制精度问题
金融 / 价格:
数据库用 DECIMAL
Java 用 BigDecimal
五十三、Java
private BigDecimal standardPrice;
五十四、car_service_package
服务套餐。
例如:
基础保养套餐
空调养护套餐
五十五、为什么套餐不能把服务项目 ID 存成字符串
错误:
item_ids = "1,3,5,7"
问题:
不好 JOIN
不好约束
不好查询
不好维护
五十六、正确:关联表
car_service_package
car_service_package_item
五十七、套餐表
CREATE TABLE car_service_package (
package_id BIGINT NOT NULL AUTO_INCREMENT,
package_code VARCHAR(50) NOT NULL,
package_name VARCHAR(100) NOT NULL,
package_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
status CHAR(1) DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(64),
create_time DATETIME,
update_by VARCHAR(64),
update_time DATETIME,
PRIMARY KEY (package_id),
UNIQUE KEY uk_package_code (package_code)
);
五十八、套餐项目关联表
CREATE TABLE car_service_package_item (
id BIGINT NOT NULL AUTO_INCREMENT,
package_id BIGINT NOT NULL,
service_item_id BIGINT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
PRIMARY KEY (id),
UNIQUE KEY uk_package_item (
package_id,
service_item_id
),
KEY idx_package_item_service (
service_item_id
)
);
五十九、为什么 quantity 也要有
有的套餐:
某项目可能需要多次 / 多单位
虽然当前第一版:
通常 = 1
但保留:
更合理
六十、car_work_order
服务单 / 工单。
它代表:
一次实际养修服务
六十一、预约和工单不是一回事
预约:
“我准备来”
工单:
“已经进入实际服务”
六十二、为什么不能直接用 appointment 表一直改
因为:
预约可以取消
工单可能没有预约直接到店
一条预约进入真正维修后
还会产生更多实际执行信息
六十三、工单字段
work_order_id
work_order_no
appointment_id
customer_id
vehicle_id
dept_id
advisor_user_id
technician_user_id
start_time
finish_time
status
fault_description
六十四、工单状态
例如:
0 待派工
1 待施工
2 施工中
3 已完工
4 待结算
5 已结算
6 已取消
六十五、工单 SQL
CREATE TABLE car_work_order (
work_order_id BIGINT NOT NULL AUTO_INCREMENT,
work_order_no VARCHAR(40) NOT NULL,
appointment_id BIGINT,
customer_id BIGINT NOT NULL,
vehicle_id BIGINT NOT NULL,
dept_id BIGINT NOT NULL,
advisor_user_id BIGINT,
technician_user_id BIGINT,
fault_description VARCHAR(1000),
start_time DATETIME,
finish_time DATETIME,
status CHAR(1) NOT NULL DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(64),
create_time DATETIME,
update_by VARCHAR(64),
update_time DATETIME,
PRIMARY KEY (work_order_id),
UNIQUE KEY uk_work_order_no (work_order_no),
KEY idx_work_order_appointment (appointment_id),
KEY idx_work_order_vehicle (vehicle_id),
KEY idx_work_order_dept_status (
dept_id,
status
)
);
六十六、appointment_id 为什么允许 NULL
因为:
现场直接到店客户
可以:
没有预约
直接建工单
六十七、car_work_order_item
工单项目明细。
六十八、为什么需要明细表
一张工单:
可能有多个服务项目
关系:
WorkOrder 1 : N WorkOrderItem
六十九、字段
work_order_item_id
work_order_id
service_item_id
item_name
unit_price
quantity
amount
technician_user_id
status
七十、为什么重复保存 item_name / unit_price
因为:
这是历史快照
服务项目以后:
改名 / 改价
历史工单:
不应该改变
七十一、明细 SQL
CREATE TABLE car_work_order_item (
work_order_item_id BIGINT NOT NULL AUTO_INCREMENT,
work_order_id BIGINT NOT NULL,
service_item_id BIGINT,
item_name VARCHAR(100) NOT NULL,
unit_price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
quantity INT NOT NULL DEFAULT 1,
amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
technician_user_id BIGINT,
status CHAR(1) DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(64),
create_time DATETIME,
update_by VARCHAR(64),
update_time DATETIME,
PRIMARY KEY (work_order_item_id),
KEY idx_work_order_item_order (
work_order_id
),
KEY idx_work_order_item_service (
service_item_id
)
);
七十二、amount 是否可以不存
理论上:
amount = unit_price * quantity
可以实时算。
但结算业务:
经常需要快照
所以保存:
amount
也合理。
七十三、必须防止前端自己传 amount
后端应该:
BigDecimal
↓
unitPrice * quantity
↓
计算 amount
不能信:
{
"unitPrice": 100,
"quantity": 2,
"amount": 1
}
七十四、car_settlement
结算单。
核心:
工单完成
↓
生成结算
七十五、结算字段
settlement_id
settlement_no
work_order_id
customer_id
vehicle_id
dept_id
total_amount
discount_amount
receivable_amount
paid_amount
payment_status
approval_status
process_instance_id
status
七十六、为什么 total / discount / receivable 分开
例如:
服务原价 1000
优惠 100
应收 900
以后:
审计
才能看清。
七十七、应收
receivable_amount
=
total_amount
-
discount_amount
七十八、支付状态与审批状态不要混为一个 status
支付:
未支付 / 部分支付 / 已支付
审批:
无需审批 / 审批中 / 通过 / 拒绝
业务状态:
草稿 / 已提交 / 已完成 / 作废
它们:
不同维度
七十九、一个 status 字段塞全部状态会怎样
例如:
1=审批中
2=已支付
3=审批通过未支付
4=部分支付审批通过
状态组合:
迅速爆炸
八十、正确设计
status
payment_status
approval_status
分开。
八十一、结算 SQL
CREATE TABLE car_settlement (
settlement_id BIGINT NOT NULL AUTO_INCREMENT,
settlement_no VARCHAR(40) NOT NULL,
work_order_id BIGINT NOT NULL,
customer_id BIGINT NOT NULL,
vehicle_id BIGINT NOT NULL,
dept_id BIGINT NOT NULL,
total_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
discount_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
receivable_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
paid_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
payment_status CHAR(1) NOT NULL DEFAULT '0',
approval_status CHAR(1) NOT NULL DEFAULT '0',
process_instance_id VARCHAR(64),
status CHAR(1) NOT NULL DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(64),
create_time DATETIME,
update_by VARCHAR(64),
update_time DATETIME,
PRIMARY KEY (settlement_id),
UNIQUE KEY uk_settlement_no (
settlement_no
),
UNIQUE KEY uk_settlement_work_order (
work_order_id
),
KEY idx_settlement_dept_status (
dept_id,
status
),
KEY idx_settlement_approval (
approval_status
)
);
八十二、为什么 work_order_id UNIQUE
当前设计假设:
一张工单
只有一张最终结算单
如果后续允许:
多次结算
分期结算
就不能 UNIQUE。
八十三、业务约束必须和索引一致
不要:
不知道需求
就加 UNIQUE
八十四、car_settlement_item
结算明细。
八十五、为什么不能直接拿 work_order_item 当结算明细
因为:
工单项目
是施工事实
结算明细:
是计费事实
可能不同。
例如:
工单有 3 个项目
其中 1 个赠送
结算:
金额可能为 0
八十六、结算明细字段
settlement_item_id
settlement_id
source_type
source_id
item_name
unit_price
quantity
original_amount
discount_amount
final_amount
八十七、source_type
例如:
SERVICE_ITEM
PACKAGE
八十八、为什么需要 source_type
结算明细可能来自:
单项
套餐
八十九、结算明细 SQL
CREATE TABLE car_settlement_item (
settlement_item_id BIGINT NOT NULL AUTO_INCREMENT,
settlement_id BIGINT NOT NULL,
source_type VARCHAR(20) NOT NULL,
source_id BIGINT,
item_name VARCHAR(100) NOT NULL,
unit_price DECIMAL(12,2) NOT NULL DEFAULT 0.00,
quantity INT NOT NULL DEFAULT 1,
original_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
discount_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
final_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
remark VARCHAR(500),
create_by VARCHAR(64),
create_time DATETIME,
update_by VARCHAR(64),
update_time DATETIME,
PRIMARY KEY (settlement_item_id),
KEY idx_settlement_item_settlement (
settlement_id
)
);
九十、为什么结算明细必须快照
假设:
昨天机油服务 300
今天:
改价 350
昨天结算单:
仍应显示 300
所以:
item_name
unit_price
final_amount
保存历史。
九十一、car_approval_record
审批业务记录表。
Activiti 已经有:
ACT_HI_*
为什么还要业务表?
因为业务页面需要:
审批人
动作
意见
节点
时间
简单查询。
九十二、审批记录 SQL
CREATE TABLE car_approval_record (
approval_record_id BIGINT NOT NULL AUTO_INCREMENT,
business_type VARCHAR(30) NOT NULL,
business_id BIGINT NOT NULL,
process_instance_id VARCHAR(64),
task_id VARCHAR(64),
activity_id VARCHAR(100),
activity_name VARCHAR(100),
operator_user_id BIGINT NOT NULL,
action VARCHAR(30) NOT NULL,
comment VARCHAR(1000),
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (approval_record_id),
KEY idx_approval_business (
business_type,
business_id
),
KEY idx_approval_process (
process_instance_id
)
);
九十三、为什么 business_type
未来可能审批:
SETTLEMENT
REFUND
DISCOUNT
一张记录表:
统一复用
九十四、数据库关系总图
car_customer
│
└─< car_vehicle
│
└─< car_appointment
│
└─ car_work_order
│
├─< car_work_order_item
│
└─ car_settlement
│
├─< car_settlement_item
│
└─< car_approval_record
car_service_item
│
└─< car_service_package_item
>─ car_service_package
九十五、为什么现在就设计审批字段
后面课程有:
Activiti 7
如果现在完全没预留:
process_instance_id
approval_status
后面:
要改数据库
提前设计:
后续更平滑
九十六、但不要现在就实现全部 Activiti
这一阶段:
只预留字段
后面审批课:
再正式接流程
九十七、业务状态设计文档
建议每张状态表:
单独写状态机
九十八、预约状态机
待确认
↓
已确认
↓
已到店
↓
已转工单
↓
已完成
旁路:
待确认 / 已确认
↓
已取消
九十九、工单状态机
待派工
↓
待施工
↓
施工中
↓
已完工
↓
待结算
↓
已结算
一百、结算状态机
草稿
↓
已提交
↓
审批中
↓
审批通过
↓
已完成
拒绝:
审批中
↓
审批拒绝
一百零一、不要允许任意状态互相改
错误:
前端传 status=5
后端直接 update
一百零二、正确
Service:
检查当前状态
↓
检查目标状态
↓
判断是否允许转换
一百零三、状态枚举
例如:
public enum AppointmentStatus {
PENDING_CONFIRM("0"),
CONFIRMED("1"),
ARRIVED("2"),
CONVERTED("3"),
COMPLETED("4"),
CANCELLED("5");
private final String code;
}
一百零四、数据库为什么还能用字典
Enum:
保证后端逻辑
若依字典:
负责前端 Label
两者:
可以共存
一百零五、淘车湾字典清单
建议:
car_customer_status
car_vehicle_status
car_appointment_status
car_appointment_type
car_service_item_status
car_work_order_status
car_settlement_status
car_payment_status
car_approval_status
car_approval_action
一百零六、不要所有状态共用 sys_normal_disable
例如:
预约状态
不是:
正常/停用
应该:
独立业务字典
一百零七、菜单规划
一级:
淘车湾管理
一百零八、二级菜单
客户管理
车辆管理
养修预约
服务单项
服务套餐
服务工单
结算管理
审批管理
一百零九、审批管理子菜单
后面:
我的待办
我的已办
流程定义
一百一十、权限码规范
模块前缀统一:
car
一百一十一、客户
car:customer:list
car:customer:query
car:customer:add
car:customer:edit
car:customer:remove
car:customer:export
一百一十二、车辆
car:vehicle:list
car:vehicle:query
car:vehicle:add
car:vehicle:edit
car:vehicle:remove
一百一十三、预约
car:appointment:list
car:appointment:query
car:appointment:add
car:appointment:edit
car:appointment:confirm
car:appointment:cancel
car:appointment:convert
一百一十四、服务项目
car:serviceItem:list
car:serviceItem:add
car:serviceItem:edit
car:serviceItem:remove
一百一十五、套餐
car:package:list
car:package:add
car:package:edit
car:package:remove
一百一十六、工单
car:workOrder:list
car:workOrder:add
car:workOrder:assign
car:workOrder:start
car:workOrder:finish
一百一十七、结算
car:settlement:list
car:settlement:add
car:settlement:edit
car:settlement:submit
car:settlement:approve
car:settlement:export
一百一十八、为什么动作权限要单独设计
例如:
预约确认
不是普通:
edit
业务含义不同。
一百一十九、角色与权限建议
服务顾问:
customer
vehicle
appointment
workOrder
settlement draft
一百二十、技师
workOrder:list
workOrder:start
workOrder:finish
一百二十一、财务
settlement:list
settlement:query
settlement:approve
一百二十二、管理员
全部
一百二十三、数据权限
门店级项目非常适合:
若依 @DataScope
一百二十四、每张业务表建议保存 dept_id
例如:
appointment.dept_id
work_order.dept_id
settlement.dept_id
一百二十五、为什么
查询:
本门店
可以直接:
按 dept_id
一百二十六、如果业务表没有 dept_id
要通过:
用户
工单
其他表
多次 JOIN 才知道:
属于哪个门店
数据权限:
更复杂
一百二十七、客户表是否需要 dept_id
取决于:
客户是全平台共享
还是门店私有
一百二十八、课程推荐
客户可以:
全平台共享
车辆也:
跟客户走
预约 / 工单 / 结算:
归属门店
一百二十九、这样数据权限更合理
客户信息
可按业务权限查看
业务单据
按门店限制
一百三十、若依 @DataScope 使用
例如预约列表:
@DataScope(
deptAlias = "a"
)
public List<CarAppointment>
selectCarAppointmentList(
CarAppointment query
) {
return mapper
.selectCarAppointmentList(
query
);
}
一百三十一、Mapper SQL
SELECT ...
FROM car_appointment a
WHERE 1 = 1
${params.dataScope}
一百三十二、注意 alias
注解:
deptAlias = "a"
SQL:
a.dept_id
必须一致。
一百三十三、客户列表是否加 DataScope
如果客户全平台共享:
可以不按 dept_id
但详情仍要:
有功能权限
一百三十四、若依脚手架改造步骤总览
1. 创建 Git 基线
2. 创建 ruoyi-car Maven 模块
3. 父 pom 注册模块
4. ruoyi-admin 依赖 ruoyi-car
5. 初始化业务 SQL
6. 建业务字典
7. 建菜单权限
8. 配置代码生成
9. 生成基础 CRUD
10. 调整实体/VO/DTO
11. 增加 DataScope
12. 验证权限
13. 验证前端菜单
14. 提交 Git
一百三十五、第一步 Git 基线
如果若依刚拉下来:
git status
确认。
然后:
git add .
git commit -m "chore: baseline ruoyi project"
一百三十六、为什么
接下来:
改 Maven
改菜单
改数据库
改模块
如果坏:
能恢复
一百三十七、新建 ruoyi-car
目录:
RuoYi-Vue
├─ ruoyi-admin
├─ ruoyi-common
├─ ruoyi-framework
├─ ruoyi-generator
├─ ruoyi-quartz
├─ ruoyi-system
└─ ruoyi-car
一百三十八、ruoyi-car pom
示意:
<project>
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>${revision}</version>
</parent>
<artifactId>ruoyi-car</artifactId>
<dependencies>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
</dependencies>
</project>
具体:
按当前若依 pom 版本结构调整
一百三十九、父 pom
添加:
<module>ruoyi-car</module>
一百四十、ruoyi-admin
加入:
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-car</artifactId>
</dependency>
一百四十一、为什么 admin 要依赖业务模块
Controller:
需要注入 Car Service
启动模块:
需要加载业务 Bean
一百四十二、SpringBoot 扫描包
如果业务包:
com.ruoyi.car
且启动类扫描:
com.ruoyi
通常:
会自动扫描
一百四十三、MyBatis Mapper 扫描
若依:
通常已有 MapperScan
你需要确认:
是否包含 com.ruoyi.**.mapper
一百四十四、不要新建第二套 MyBatisConfig
除非:
真的需要
否则:
复用若依现有扫描
一百四十五、代码生成器
把 SQL 表:
导入若依代码生成
一百四十六、先生成哪几张
第一阶段:
car_customer
car_vehicle
car_appointment
car_service_item
一百四十七、为什么不一次生成 11 张
可以生成,
但学习:
先少量
验证:
模块
包名
菜单
权限码
正确后再批量。
一百四十八、生成配置
例如客户表:
生成包路径:
com.ruoyi.car.customer
模块名:
car
业务名:
customer
功能名:
客户管理
一百四十九、菜单路径
前端建议:
car/customer/index
一百五十、生成后检查什么
Domain
Mapper
Mapper XML
Service
ServiceImpl
Controller
Vue API
Vue Page
Menu SQL
一百五十一、不要生成后直接运行
先检查:
字段名
主键
权限前缀
字典
查询条件
Excel 字段
一百五十二、Entity vs DTO vs VO
若依代码生成:
常直接用 Domain
学习项目:
可以先接受
一百五十三、复杂业务要逐渐分开
例如结算提交:
SettlementSubmitDTO
返回详情:
SettlementDetailVO
一百五十四、为什么
结算:
不只是普通 CRUD
如果所有接口:
都直接传 CarSettlement Entity
前端可能:
修改不该修改的字段
一百五十五、例如不能让前端控制
approval_status
process_instance_id
paid_amount
一百五十六、DTO 白名单
public class SettlementCreateDTO {
@NotNull
private Long workOrderId;
@NotNull
private BigDecimal discountAmount;
private String remark;
}
一百五十七、后端自己算
totalAmount
receivableAmount
approvalStatus
一百五十八、VO
public class SettlementDetailVO {
private Long settlementId;
private String settlementNo;
private String customerName;
private String plateNo;
private BigDecimal totalAmount;
private BigDecimal discountAmount;
private BigDecimal receivableAmount;
private String approvalStatus;
private List<SettlementItemVO>
items;
}
一百五十九、为什么现在就强调 DTO/VO
后面:
结算
审批
如果直接 Entity:
安全和维护问题非常明显
一百六十、编号生成
业务编号:
appointment_no
work_order_no
settlement_no
一百六十一、简单课程方案
前缀
+
yyyyMMdd
+
数据库 ID / 随机序列
一百六十二、例如
AP202609100001
WO202609100001
ST202609100001
一百六十三、不要只使用毫秒时间戳
高并发:
仍可能碰撞
一百六十四、也不要前端生成
业务编号:
后端负责
一百六十五、可使用 Redis INCR
例如:
seq:appointment:20260910
执行:
INCR
得到:
1
2
3
一百六十六、拼:
AP
+
20260910
+
0001
一百六十七、为什么 Redis INCR 适合
它:
原子递增
多实例:
也能共享
一百六十八、Redis 挂了怎么办
课程项目:
可以降级 UUID / 数据库序列
真正生产:
编号生成要独立设计
一百六十九、不要用编号作为主键
主键:
BIGINT
业务编号:
VARCHAR + UNIQUE
更方便。
一百七十、数据库外键要不要用
若依项目:
很多时候不使用物理 FOREIGN KEY
由:
业务 Service
维护一致性。
一百七十一、为什么
大型业务:
迁移
分库
高并发
批量操作
物理外键:
有时增加维护复杂度
一百七十二、课程项目怎么选
可以:
不加物理 FOREIGN KEY
但一定要:
逻辑上理解关系
一百七十三、没有 FK 不代表可以乱数据
例如车辆新增:
customer_id
Service 必须:
检查客户存在
一百七十四、删除客户
如果客户有:
车辆
预约
工单
不能:
直接 DELETE
一百七十五、推荐逻辑删除 / 停用
例如:
status = 1
不让继续新业务。
一百七十六、若依默认是否所有表都逻辑删除
不是。
你需要:
按业务决定
一百七十七、历史业务单通常不能真删
例如:
结算单
应该:
作废
而不是:
DELETE
一百七十八、为什么
财务 / 审计:
要保留历史
一百七十九、字典初始化 SQL
例如:
INSERT INTO sys_dict_type (
dict_name,
dict_type,
status
) VALUES (
'养修预约状态',
'car_appointment_status',
'0'
);
一百八十、字典数据
例如:
0 待确认
1 已确认
2 已到店
3 已转工单
4 已完成
5 已取消
一百八十一、不要在 Vue 写
if (status === '0') ...
使用:
useDict
一百八十二、Controller 权限
例如:
@PreAuthorize(
"@ss.hasPermi('car:appointment:list')"
)
@GetMapping("/list")
public TableDataInfo list(
CarAppointment query
) {
startPage();
List<CarAppointment> list =
appointmentService
.selectAppointmentList(
query
);
return getDataTable(
list
);
}
一百八十三、业务动作接口
确认预约:
POST /car/appointment/{id}/confirm
比:
PUT /car/appointment
status=1
更清晰。
一百八十四、为什么
业务动作:
有规则
Confirm 可能需要:
当前状态必须待确认
当前用户必须有 confirm 权限
设置确认人
设置确认时间
一百八十五、Service
@Transactional
public void confirm(
Long appointmentId
) {
CarAppointment appointment =
findById(
appointmentId
);
if (
!AppointmentStatus
.PENDING_CONFIRM
.getCode()
.equals(
appointment.getStatus()
)
) {
throw new ServiceException(
"当前预约状态不能确认"
);
}
appointment.setStatus(
AppointmentStatus
.CONFIRMED
.getCode()
);
mapper.update...
}
一百八十六、状态转换必须后端控制
不能:
前端想改什么就改什么
一百八十七、取消预约
POST /car/appointment/{id}/cancel
Body:
{
"reason": "客户临时有事"
}
一百八十八、取消条件
例如:
待确认
已确认
允许取消。
一百八十九、已经转工单
不能直接取消预约
因为:
实际服务已经开始
一百九十、转工单
POST /car/appointment/{id}/convert
一百九十一、事务
创建 WorkOrder
+
更新 Appointment status
必须:
一个事务
一百九十二、为什么
如果:
工单创建成功
但预约状态没改:
用户还能再次转工单
导致:
重复工单
一百九十三、还需要唯一约束
如果业务要求:
一条预约只能转一个工单
可以:
car_work_order.appointment_id
加:
UNIQUE
一百九十四、事务 + UNIQUE 双保险
事务:
保证一次操作内部一致
唯一约束:
防并发重复
一百九十五、为什么这比 Redis 锁更基础
如果一个数据库:
UNIQUE 往往是最后防线
Redis 锁:
只能做前置协调
一百九十六、服务项目缓存
car_service_item:
读多写少
可以 Redis:
缓存启用项目列表
一百九十七、但第一阶段要不要上 Redis
不用急。
先:
MySQL 正确
后续性能:
再缓存
一百九十八、不要为了“项目用了 Redis”到处缓存
缓存应该:
有明确热点
一百九十九、若依 Redis 已经用于
登录
验证码
字典
所以项目已经:
真实使用 Redis
二百、前端页面结构
一级菜单:
淘车湾管理
二百零一、客户页
查询表单
表格
新增
修改
详情
导出
二百零二、车辆页
车牌号
客户
品牌
车型
VIN
状态
二百零三、预约页
预约编号
客户
车辆
门店
预约时间
预约类型
状态
服务顾问
操作
二百零四、预约操作按钮
按状态显示:
待确认
→ 确认 / 取消
已确认
→ 到店 / 取消
已到店
→ 转工单
已转工单
→ 查看工单
二百零五、按钮权限 + 状态双判断
显示“确认”:
有 car:appointment:confirm 权限
+
row.status == 待确认
二百零六、但后端仍要再次校验状态
前端只是:
体验
后端:
安全 + 业务正确性
二百零七、Vue API
src/api/car/customer.js
src/api/car/vehicle.js
src/api/car/appointment.js
二百零八、页面
src/views/car/customer/index.vue
src/views/car/vehicle/index.vue
src/views/car/appointment/index.vue
二百零九、不要所有页面一个 3000 行 index.vue
复杂页面:
拆组件
例如预约:
AppointmentForm.vue
AppointmentDetail.vue
二百一十、若依代码生成后先别追求 UI
第一目标:
CRUD 正常
权限正常
字典正常
分页正常
之后:
再优化 UI
二百一十一、项目开发顺序
推荐:
客户
↓
车辆
↓
预约
↓
服务项目
↓
套餐
↓
工单
↓
结算
↓
审批
二百一十二、为什么
后一个模块:
依赖前一个模块数据
例如预约:
需要客户 + 车辆
二百一十三、套餐:
依赖服务单项
二百一十四、工单:
依赖预约 + 服务项目
二百一十五、结算:
依赖工单
二百一十六、审批:
依赖结算
二百一十七、项目分析文档应该包含什么
至少:
项目目标
用户角色
核心模块
业务流程
核心状态
数据库 ER 关系
权限设计
接口风格
技术栈
非功能要求
二百一十八、技术栈
后端:
JDK 17
SpringBoot 3
Spring Security
MyBatis
Redis
Activiti 7
Maven
二百一十九、前端
Vue3
Vite
Element Plus
Pinia
Axios
二百二十、基础框架
RuoYi-Vue springboot3
RuoYi-Vue3
二百二十一、数据库
MySQL 8
二百二十二、版本不要乱写最新
项目真正开发时:
以当前 pom/package.json
为准
二百二十三、非功能要求
课程第一版:
权限正确
状态一致
数据不越权
操作可审计
代码结构清晰
接口统一
异常统一
二百二十四、暂时不做
微服务
分库分表
亿级流量
复杂规则引擎
全链路追踪
二百二十五、为什么
当前阶段:
企业项目基础
不是:
架构炫技
二百二十六、错误数据库设计 1
一张:
car_order
塞:
客户
车辆
预约
工单
服务项目
审批
支付
后面:
完全无法维护
二百二十七、错误设计 2
套餐项目:
"1,2,3,4"
字符串保存多 ID。
二百二十八、错误设计 3
金额:
DOUBLE
二百二十九、错误设计 4
数据库保存:
“已审批”
而不是:
稳定 Code
二百三十、错误设计 5
历史结算实时 JOIN 当前价格。
二百三十一、错误设计 6
所有业务表不保存:
dept_id
后面数据权限:
很难做
二百三十二、错误设计 7
前端传:
totalAmount
approvalStatus
后端:
直接信
二百三十三、错误设计 8
用户能直接:
PUT status
任意改状态。
二百三十四、错误设计 9
删除历史结算单:
DELETE
二百三十五、错误设计 10
AI 重新写:
第二套 Security
第二套 JWT
第二套 Redis Token
二百三十六、脚手架改造原则
第一:
保留若依核心认证
二百三十七、第二:
新增业务模块
二百三十八、第三:
复用 BaseController
AjaxResult
TableDataInfo
@PreAuthorize
@Log
@DataScope
useDict
二百三十九、第四:
不要改 sys_* 表来承载所有业务
二百四十、第五:
用代码生成器做 CRUD 基线
二百四十一、第六:
复杂业务手工 Service 重构
二百四十二、代码生成不是最终代码
生成:
标准 CRUD
你仍然要:
增加状态机
事务
业务校验
DTO/VO
关联查询
权限
二百四十三、客户删除例子
生成器:
deleteByIds
可能直接:
DELETE
业务真实需求:
有历史工单
不能删除
二百四十四、所以要改 Service
先检查关联
或者:
只允许停用
二百四十五、代码生成后不能机械照用
这是:
企业项目和 CRUD 作业
最大的区别。
二百四十六、Controller 不应放大量业务
错误:
@PostMapping("/convert")
public AjaxResult convert(...) {
// 查询预约
// 查客户
// 查车辆
// 生成编号
// insert 工单
// update 预约
// 写日志
// ...
}
二百四十七、正确
Controller:
参数接收
↓
调用 Service
↓
返回
二百四十八、业务放 Service
@Transactional
public Long convertToWorkOrder(
Long appointmentId
) {
}
二百四十九、为什么 Service 加事务
它:
代表一个业务用例
二百五十、异常
使用:
ServiceException
二百五十一、例如
throw new ServiceException(
"预约已转为工单,请勿重复操作"
);
二百五十二、防重复提交
转换工单接口:
@RepeatSubmit(
interval = 3000
)
可以减少:
双击
二百五十三、但还不够
还要:
状态校验
数据库 UNIQUE
二百五十四、三层防护
前端按钮 disabled
+
@RepeatSubmit
+
数据库唯一约束 / 状态事务
二百五十五、这才是企业思维
不要相信:
“一个注解解决所有并发”
二百五十六、日志
关键动作:
确认预约
取消预约
转工单
派工
完工
提交结算
审批
建议:
@Log
二百五十七、普通查询需要 @Log 吗
一般:
没必要每次 list 都记录业务操作日志
否则:
日志爆炸
二百五十八、关键变更记录
新增
修改
删除
状态变更
审批
导入导出
更适合。
二百五十九、数据字典优先级
页面所有状态:
尽量用 useDict
二百六十、不要每个页面自己定义 options
错误:
const statusOptions = [
{ label: '待确认', value: '0' },
...
]
二百六十一、为什么
以后管理员:
改 Label
页面:
不会同步
二百六十二、前端 API 命名
例如:
listAppointment()
getAppointment()
addAppointment()
updateAppointment()
confirmAppointment()
cancelAppointment()
convertAppointment()
二百六十三、不要
changeStatus(id, status)
让所有业务状态:
随便改
二百六十四、REST + 业务动作
普通 CRUD:
GET
POST
PUT
DELETE
业务动作:
POST /{id}/confirm
POST /{id}/cancel
POST /{id}/convert
二百六十五、为什么用 POST
这些动作:
修改服务器状态
并且:
不是普通资源整体更新
二百六十六、项目查询设计
例如预约列表筛选:
appointmentNo
customerName
phone
plateNo
deptId
appointmentTimeRange
status
二百六十七、不要为了页面方便全部存 appointment 表
customerName:
可以 JOIN customer
二百六十八、哪些字段该快照
如果业务要求:
预约提交后客户名称变了
预约历史仍要显示当时名称
才考虑:
snapshot
二百六十九、不要无脑反范式
先:
标准关系设计
真正有历史审计需求:
再快照
二百七十、结算必须快照
因为:
价格历史
是强需求。
二百七十一、分页
所有:
列表接口
使用:
startPage()
+
getDataTable()
二百七十二、导出
导出:
不能只导当前分页
通常:
按筛选条件查询全部允许数据
二百七十三、但大数据要限制
课程项目:
可以限制 1 万条
避免:
内存爆炸
二百七十四、客户手机号脱敏
普通列表可以:
138****0000
详情:
按权限决定是否完整显示
二百七十五、身份证
更应该:
脱敏
二百七十六、为什么这和若依第二篇有关
框架已经:
有数据脱敏能力
业务:
直接复用
二百七十七、数据库初始化文件
建议:
sql/car_init.sql
包括:
业务表
业务字典
菜单权限
二百七十八、不要所有东西混进若依原始 SQL
保留:
官方初始化 SQL
另外:
业务增量 SQL
二百七十九、为什么
升级若依时:
更容易对比
二百八十、Git Commit 建议
数据库:
git commit -m "feat: add car business schema"
二百八十一、模块:
git commit -m "feat: add ruoyi-car module"
二百八十二、客户车辆:
git commit -m "feat: add customer and vehicle management"
二百八十三、预约:
git commit -m "feat: add maintenance appointment module"
二百八十四、为什么拆 Commit
以后 Bug:
容易定位
AI 改坏:
容易回退
二百八十五、用 Cursor 分析项目
第一步:
只分析,不修改
二百八十六、提示词:项目分析
当前项目基于官方 RuoYi-Vue springboot3 + RuoYi-Vue3。
业务项目名称:淘车湾汽车养护维修管理系统。
请只分析,不修改任何文件。
请基于现有若依框架,分析以下核心业务:
客户
车辆
养修预约
服务单项
服务套餐
服务工单
结算单
审批
要求:
1. 复用 sys_user/sys_dept/sys_role/sys_menu
2. 不新增第二套认证/权限
3. 业务表统一 car_ 前缀
4. 门店先复用 sys_dept
5. 预留 Activiti processInstanceId
6. 输出表关系、状态和权限码
二百八十七、提示词:数据库审查
请审查当前 car_* SQL。
重点检查:
1. 主键
2. UNIQUE 业务约束
3. 常用查询索引
4. DECIMAL 金额
5. 状态字段是否混用
6. 是否保留结算历史价格快照
7. 是否具备 dept_id 数据权限字段
8. Activiti 是否预留 process_instance_id
9. 是否存在逗号分隔多 ID
10. 是否有明显冗余或缺失关系
不要直接修改,先给审查结果。
二百八十八、提示词:若依改造
请基于当前项目真实 pom.xml 和包结构,
给出新增 ruoyi-car Maven 模块的最小改动方案。
必须:
1. 使用当前项目实际 version/revision 写法
2. 复用 ruoyi-common、ruoyi-system
3. 不新建第二套 MyBatisConfig
4. 不修改现有 Security 核心
5. 列出需要修改的真实 pom 文件
二百八十九、为什么不能让 AI 凭空写 pom
若依不同版本:
parent/version/revision
可能不同。
AI 必须:
先读真实 pom
二百九十、提示词:生成业务模块
请基于若依代码生成器现有生成风格,
为 car_appointment 设计生成配置。
要求:
模块名 car
业务名 appointment
功能名 养修预约
权限前缀 car:appointment
生成后需要手工增加:
confirm
cancel
arrive
convertToWorkOrder
这些业务动作不要通过普通 update status 代替。
二百九十一、AI 代码审查重点
每次生成:
看 Diff
检查:
有没有修改 SecurityConfig
有没有新增 JwtUtil
有没有新增 RedisConfig
有没有改 sys_user
有没有硬编码角色名
有没有让前端控制金额和状态
二百九十二、项目第一阶段验收
必须完成:
若依正常启动
MySQL 正常
Redis 正常
admin 正常登录
ruoyi-car 正常编译
业务 SQL 执行成功
菜单出现
角色授权正常
客户 CRUD 正常
车辆 CRUD 正常
预约基础 CRUD 正常
字典正常
@DataScope 正常
403 正常
二百九十三、不要急着写 Activiti
只有:
基础业务稳定
后面审批:
才好接
二百九十四、数据库验收 SQL
确认表:
SHOW TABLES LIKE 'car_%';
二百九十五、确认索引
SHOW INDEX
FROM car_appointment;
二百九十六、确认字典
SELECT *
FROM sys_dict_type
WHERE dict_type LIKE 'car_%';
二百九十七、确认菜单权限
SELECT
menu_id,
menu_name,
perms
FROM sys_menu
WHERE perms LIKE 'car:%';
二百九十八、接口验收
普通用户:
没有 car:customer:list
访问:
客户列表
应该:
403
二百九十九、数据权限验收
A 门店:
不能查看 B 门店预约
三百、状态验收
已取消预约:
不能再次确认
三百零一、并发验收
同一预约:
快速双击“转工单”
只能:
成功一次
三百零二、为什么项目第一阶段就要做这些
如果:
基础权限和状态都错
后面:
结算 + Activiti
只会更乱。
三百零三、面试题 1:为什么项目开发前要做数据库设计
答:
数据库决定核心业务实体、关系、状态和历史数据结构。
如果在 CRUD 开发后频繁改变表关系,
会同时影响 Mapper、Service、接口、前端和测试。
因此复杂业务通常先梳理流程和实体,
再进行数据库设计。
三百零四、面试题 2:预约和工单为什么分表
答:
预约表示客户未来的服务意向,
可能被取消,也可能没有真正到店。
工单表示已经进入实际维修/保养执行阶段的业务单据。
二者生命周期和字段不同,
并且现场客户可能没有预约直接创建工单,
因此应该拆分。
三百零五、面试题 3:为什么服务套餐需要关联表
答:
一个套餐包含多个服务项目,
一个服务项目也可以属于多个套餐,
这是典型多对多关系。
使用 package_item 关联表
比在 package 表中保存逗号分隔的 ID 字符串
更符合关系数据库设计,
也更容易 JOIN、约束和维护。
三百零六、面试题 4:为什么结算明细保存价格快照
答:
服务项目和套餐价格未来可能修改。
历史结算单属于已经发生的财务事实,
不能因为基础价格表修改而改变历史金额。
因此结算明细需要保存当时的名称、单价、数量、
优惠和最终金额等快照字段。
三百零七、面试题 5:为什么金额用 DECIMAL + BigDecimal
答:
float/double 是二进制浮点数,
可能产生精度误差。
价格、结算等金额数据
数据库通常使用 DECIMAL,
Java 使用 BigDecimal。
三百零八、面试题 6:为什么业务状态不能让前端直接修改
答:
业务状态存在合法流转规则。
例如已取消预约不能再次确认,
已转工单预约不能再次生成工单。
如果前端可以任意传 status,
就能绕过业务规则。
因此应通过 confirm、cancel、convert 等明确业务方法
在 Service 层执行状态校验和更新。
三百零九、面试题 7:为什么每张业务单据保存 dept_id
答:
门店型系统需要按组织范围做数据权限。
在预约、工单、结算等核心业务单据中保存归属 dept_id,
可以直接配合若依 @DataScope
按门店或部门范围过滤数据,
避免每次通过复杂 JOIN 推导归属。
三百一十、面试题 8:为什么复用 sys_dept 表示门店
答:
如果门店主要用于组织人员和数据范围,
而没有大量独立门店属性,
可以直接复用若依 sys_dept。
这样能够直接使用部门树、用户部门关系和 @DataScope,
减少重复开发。
如果门店以后有大量独立业务属性,
再增加 car_store 等业务表也可以。
三百一十一、面试题 9:为什么不修改 sys_user 加所有业务字段
答:
sys_user 主要承担系统账号和认证身份。
如果把车辆、学院、积分、业务会员等大量字段都塞进去,
会把框架账号模型和具体业务强耦合。
更合理的是业务表通过 user_id
和 sys_user 建立关联。
三百一十二、面试题 10:代码生成器的作用
答:
若依代码生成器适合生成标准 CRUD 基线,
包括 Domain、Mapper、Service、Controller、
Vue API、页面和菜单 SQL。
但生成代码不是最终业务实现,
复杂状态、事务、权限、DTO/VO 和业务校验
仍需要手工设计。
三百一十三、面试题 11:为什么 @RepeatSubmit 不能完全解决重复业务
答:
@RepeatSubmit 主要阻止短时间内重复 HTTP 请求。
真正业务并发还需要状态校验、事务、
数据库唯一约束或幂等键等机制。
例如一条预约只能生成一个工单,
最终可以通过 appointment_id 唯一约束兜底。
三百一十四、面试题 12:若依脚手架改造时为什么不要重写认证体系
答:
若依已经有成熟的 Spring Security、TokenService、
LoginUser、Redis 登录状态和权限判断体系。
业务模块应该复用现有能力。
如果又增加第二套 JWT Filter、LoginInterceptor、
SecurityConfig,会出现两套认证规则并存,
增加 401/403 和权限冲突风险。
三百一十五、淘车湾项目知识树
淘车湾
│
├─ 基础
│ ├─ RuoYi
│ ├─ SpringBoot
│ ├─ Vue3
│ ├─ MyBatis
│ ├─ Redis
│ └─ Git
│
├─ Customer
│ ├─ car_customer
│ └─ car_vehicle
│
├─ Appointment
│ └─ car_appointment
│
├─ Service
│ ├─ car_service_item
│ ├─ car_service_package
│ └─ car_service_package_item
│
├─ WorkOrder
│ ├─ car_work_order
│ └─ car_work_order_item
│
├─ Settlement
│ ├─ car_settlement
│ └─ car_settlement_item
│
├─ Workflow
│ ├─ process_instance_id
│ ├─ Activiti
│ └─ car_approval_record
│
├─ Permission
│ ├─ sys_user
│ ├─ sys_role
│ ├─ sys_menu
│ └─ @DataScope
│
└─ Scaffold
├─ ruoyi-car
├─ Code Generator
├─ useDict
├─ @PreAuthorize
├─ @Log
└─ @RepeatSubmit
三百一十六、项目总流程图
Customer
↓
Vehicle
↓
Appointment
↓
Confirm
↓
Arrive
↓
WorkOrder
↓
Service Items / Package
↓
Technician
↓
Finish Work
↓
Settlement
↓
Settlement Items
↓
Approval
↓
Payment
↓
Completed
三百一十七、项目数据库核心关系
Customer 1:N Vehicle
Customer 1:N Appointment
Vehicle 1:N Appointment
Appointment 0..1 : 1 WorkOrder
WorkOrder 1:N WorkOrderItem
ServicePackage N:M ServiceItem
WorkOrder 1:1 Settlement
Settlement 1:N SettlementItem
Settlement 1:N ApprovalRecord
三百一十八、第一阶段最终总结
这一阶段最重要的不是:
写了多少 Controller
而是:
业务结构是否正确
核心实体:
客户
车辆
预约
服务项目
套餐
工单
结算
审批
核心数据库原则:
多对多用关联表
金额 DECIMAL
状态 Code 化
历史数据做快照
业务编号和主键分离
核心业务保留归属 dept_id
Activiti 提前预留 process_instance_id
若依改造原则:
新增 ruoyi-car
复用若依 Security
复用 Redis
复用 RBAC
复用 @DataScope
复用 useDict
复用 @Log
复用代码生成器
不要:
重写第二套认证
业务数据塞 sys_user
所有状态前端随便改
所有金额前端传什么就存什么
生成器代码不审查直接上线
三百一十九、下一篇
按照课程表,下一篇正式进入:
《淘车湾项目实战(二):养修预约模块分析与实现》
会从本章的:
car_customer
car_vehicle
car_appointment
开始真正写业务。
重点包括:
客户车辆选择
预约编号生成
预约表单
预约列表
多条件查询
门店数据权限
预约状态机
确认预约
取消预约
客户到店
转服务工单
事务
防重复提交
唯一约束
前端按钮权限
Element Plus Dialog
若依代码生成后的二次改造
完整 Controller / Service / Mapper / Vue 实现