淘车湾项目实战一_项目分析_数据库设计_若依脚手架改造

O泡李华 6

淘车湾项目实战(一):项目分析、数据库设计与若依脚手架改造

本章位置:第三阶段 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 实现