欢迎光临
我们一直在努力

7天学会SpringBoot+Vue3企业级项目RuoyiOffice(三):后端篇——从业务建模到接口开发,做出一个完整模块

7天学会SpringBoot+Vue3企业级项目RuoyiOffice(三):后端篇——从业务建模到接口开发,做出一个完整模块

🌐 文档地址:https://ruoyioffice.com 👇 文章底部获取源码和演示地址 👇 💬 :17156169080(获取产品咨询)

这是《7天学会SpringBoot+Vue3企业级项目RuoyiOffice》系列第 3 篇。前两天看完了架构地图,也把开发环境跑通了。第三天开始写业务:不再讲抽象概念,而是挑一个边界清楚、带审批、有主数据和单据的真实模块,从业务建模一路做到接口可被前端调用。读完你能回答三个问题:一个后端业务模块应该先画什么、代码按什么层次放、一次提交请求到底碰了哪几层。

后端篇核心视觉:用车模块的六层调用、三张表关系与两条状态线

▲ 本篇核心视觉:左侧是一次「提交」请求穿过的六层,右侧是用车主数据和两张单据的表关系,底部是流程状态与还车状态两条状态线。后面的每一节都能在这张图里找到位置。

引言:为什么用「用车」当案例

写后端文章最容易犯的错,是拿一个只有增删改查的玩具表做例子,读者看完依然不知道真实业务怎么落。企业项目的后端难点在于:主数据和单据怎么分、状态不止一种怎么管、审批流程怎么接、不同租户的数据怎么隔开。OA 用车模块恰好把这几件事都带上了,而且规模可控。

决策点选择理由
案例模块 OA 用车(车辆信息、用车申请单、还车申请单) 有主数据、有两张审批单据、有互相联动的状态
讲解范围 用车申请单为主线,车辆与还车单为辅 一条主线讲透,比三条主线各讲一半更有用
代码来源 仓库真实源码,不虚构类名与字段 读者可以直接在 IDE 里对照
流程部分 只讲「提交并回写实例编号」 审批引擎本身放到第六篇,这里不抢戏

说明:本文代码节选自仓库 ruoyi-office/yudao-module-oa,为便于阅读省略了部分空行与附件处理。表结构取自开发库的实际建表语句,字段含义以枚举和代码为准。

一、先建模:三个业务对象和两条状态线

动手写代码之前,先回答「这个模块里有哪些东西、它们有哪些状态」。用车模块有三个业务对象:

对象表名性质回答的问题
车辆信息 oa_car 主数据,长期存在 公司有哪些车、是否可用
用车申请单 oa_car_apply_bill 业务单据,走审批 谁在什么时间要用哪辆车
还车申请单 oa_car_return_bill 业务单据,走审批 用完之后车怎么还回来

先看系统里的车辆台账页面,这是主数据在界面上的样子:

车辆信息管理页面:左侧是车辆分类,右侧是车牌号、车辆名称、状态和图片组成的台账表格

▲ 车辆信息管理:车辆是主数据,状态只有「空闲」「停用」这类简单取值,和单据的审批状态是两回事。

单据的状态则复杂得多。一张用车申请单同时存在两条互不相同的状态线:

状态线字段谁来改取值
流程状态 process_status 流程引擎事件回写 -1 未提交、1 审批中、2 审批通过、3 审批不通过、4 已取消、5 已退回、10 已撤回
还车状态 return_status 用车与还车业务逻辑 0 未生效、1 待还车、2 还车中、3 已还车

这个区分是后面所有设计的出发点:流程状态属于引擎,还车状态属于业务。业务模块不能把自己的业务状态也塞给流程引擎去管,否则审批一撤回,业务就乱了。

用车申请单列表:单据状态用彩色标签区分审批中、已撤回、审批通过、审批不通过

▲ 用车申请单列表:同一个列表里并存审批中、已撤回、审批通过和审批不通过,这些就是 process_status 在界面上的投影。

二、建表:每个字段都要有归属

三张表都包含审计字段(creator、create_time、updater、update_time、deleted)和租户字段 tenant_id。下面是用车申请单的核心结构,节选自开发库建表语句:

CREATE TABLE `oa_car_apply_bill` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`bill_code` varchar(32) NOT NULL COMMENT '单据编号',
`process_instance_id` varchar(64) DEFAULT NULL COMMENT '流程实例编号',
`process_status` tinyint DEFAULT NULL COMMENT '单据状态',
`car_id` bigint DEFAULT NULL COMMENT '车辆',
`go_time` datetime DEFAULT NULL COMMENT '出车时间',
`return_time` datetime DEFAULT NULL COMMENT '回车时间',
`go_area` varchar(150) DEFAULT NULL COMMENT '出车地点',
`return_area` varchar(150) DEFAULT NULL COMMENT '回车地点',
`cause` varchar(255) DEFAULT NULL COMMENT '用车事由',
`creator` varchar(64) DEFAULT NULL COMMENT '创建者',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
`dept_id` bigint DEFAULT NULL COMMENT '部门ID',
`company_id` bigint DEFAULT NULL COMMENT '公司ID',
`return_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '还车状态,0-未还车,1-还车中,2-已还车',
`car_no` varchar(32) DEFAULT NULL COMMENT '车牌号码',
PRIMARY KEY (`id`),
KEY `idx_oa_car_apply_bill_is_returned` (`return_status`),
KEY `idx_oa_car_apply_bill_company_returned` (`company_id`,`return_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用车申请单';

几个值得拎出来的字段决策:

字段设计意图容易踩的坑
bill_code 业务可读的单据号,由 BillCodeUtils 生成 还车单用它而不是主键 id 来指向用车单
process_instance_id 单据与流程实例的绑定凭证 为空说明流程没提交成功,是排障第一线索
process_status 流程引擎回写的审批状态 前端不要自己改它
return_status 业务自己的还车进度 建表注释写的是「0 未还车、1 还车中、2 已还车」,与枚举不一致
tenant_id 多租户隔离 默认值 0,不要手工写
dept_id / company_id 部门与公司冗余 为列表按公司过滤、权限范围服务

特别注意 return_status:建表注释是旧的,代码里的枚举 CarReturnStatusEnum 是 0 未生效、1 待还车、2 还车中、3 已还车。遇到注释和枚举冲突,永远以枚举和字典为准,并顺手修掉注释,否则下一个读代码的人会被误导。

三、分层:每一层只做一件事

OA 模块拆成 yudao-module-oa-api 和 yudao-module-oa-server。对外的枚举和错误码在 api,业务实现在 server。用车申请单涉及的文件如下:

层位置(yudao-module-oa 下)职责
枚举与错误码 oa-api/…/enums/ErrorCodeConstants、CarReturnStatusEnum、OaBillTypeEnum 对外共享,错误码段 1_101_000_xxx
Controller oa-server/…/controller/admin/car/CarApplyBillController 路由、权限、参数校验、统一返回
VO oa-server/…/controller/admin/car/vo/ 请求与响应对象,隔离 DO
Service oa-server/…/service/car/CarApplyBillServiceImpl 业务规则、事务与流程协作
Mapper oa-server/…/dal/mysql/car/CarApplyBillMapper 查询条件拼装
DO oa-server/…/dal/dataobject/car/CarApplyBillDO 与表一一对应

对象转换这一层,官方规范里写的是 MapStruct 的 convert 包,但用车模块实际用的是 BeanUtils.toBean,目录里并没有 convert。这是读源码时要接受的现实:规范是方向,具体模块以代码为准,新模块选哪种方式要和团队约定一致。

四、Controller:权限、校验和统一返回

Controller 只做四件事:声明路径、声明权限、触发参数校验、包装返回。用车申请单的保存与提交是这样写的:

@Tag(name = "OA协同办公 – 用车申请单")
@RestController
@RequestMapping("/oa/car-apply-bill")
@Validated
public class CarApplyBillController {

@Resource
private CarApplyBillService carApplyBillService;

@PostMapping("/save")
@Operation(summary = "保存用车申请单")
@PreAuthorize("@ss.hasPermission('oa:car-apply-bill:save')")
public CommonResult<Long> saveCarApplyBill(@Valid @RequestBody CarApplyBillSaveReqVO saveReqVO) {
return success(carApplyBillService.saveCarApplyBill(saveReqVO));
}

@PostMapping("/submit")
@Operation(summary = "提交用车申请单")
@PreAuthorize("@ss.hasPermission('oa:car-apply-bill:create')")
public CommonResult<Long> submitCarApplyBill(@Valid @RequestBody CarApplyBillSaveReqVO createReqVO) {
return success(carApplyBillService.submitCarApplyBill(createReqVO));
}

这里有三个读代码时容易忽略的点:

  • 权限标识的命名是 模块:资源:动作,如 oa:car-apply-bill:create。同一个字符串会出现在三个地方:Controller 的 @PreAuthorize、菜单管理里按钮的「权限标识」、前端按钮的 auth 配置。第五篇会专门讲这条链。
  • 「保存」和「提交」是两个接口、两个权限。保存只落库不起流程,提交才进入审批。
  • 并不是每个接口都标了权限。/get 和 /page 没有 @PreAuthorize,而专供审批页面嵌入的 /get-for-bpm 注释写明「不需要菜单权限,但会校验当前用户是否为流程参与者」。数据范围怎么兜底,是第五篇的内容。
  • VO 层也要如实说一件事:CarApplyBillSaveReqVO 里没有声明 @NotNull 等校验注解,@Valid 触发后实际没有字段级约束。目前必填由前端表单的 rules: 'required' 把关,后端只在 Service 里校验时间。如果你的新模块对数据质量要求高,建议在 VO 上补齐必填约束,不要只依赖前端。

    时间字段是另一处约定:goTime 与 returnTime 在 VO 里是 LocalDateTime 且没有 @JsonFormat,按项目规范会以毫秒时间戳序列化,前端日期选择器需要配 valueFormat: 'x'。这个细节会在第四篇和前端代码一一对上。

    五、Service:业务规则都在这里

    Service 是整个模块的重心。提交用车申请要依次完成:生成单号、校验时间冲突、保存、提交流程、回写实例编号。核心方法如下(省略附件处理):

    @Override
    public Long submitCarApplyBill(CarApplyBillSaveReqVO saveReqVO) {
    // 如果单号为空,需要生成
    if(StringUtils.isBlank(saveReqVO.getBillCode())){
    saveReqVO.setBillCode(BillCodeUtils.generateBillCode(SystemEnum.OA, OaBillTypeEnum.OA_CAR_APPLY_BILL));
    }
    // 校验时间冲突
    validateTimeConflict(saveReqVO);
    // 保存或更新
    CarApplyBillDO carApplyBill = BeanUtils.toBean(saveReqVO, CarApplyBillDO.class)
    .setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus());
    carApplyBillMapper.insertOrUpdate(carApplyBill);
    // 智能提交 BPM 流程(如果流程实例不存在则创建,存在则审批发起人任务)
    Map<String, Object> processInstanceVariables = BpmProcessVariableUtils.buildBillVariables(saveReqVO);
    String processInstanceId = processInstanceApi.submitProcessInstance(Long.valueOf(saveReqVO.getCreator()),
    new BpmProcessInstanceCreateReqDTO()
    .setProcessDefinitionKey(OaBillTypeEnum.OA_CAR_APPLY_BILL.getProcessDefinitionKey())
    .setVariables(processInstanceVariables)
    .setStartUserSelectAssignees(saveReqVO.getStartUserSelectAssignees())
    .setBusinessKey(String.valueOf(carApplyBill.getId()))
    ).getCheckedData();
    // 将工作流的编号,更新到单据中
    carApplyBillMapper.updateById(new CarApplyBillDO().setId(carApplyBill.getId()).setProcessInstanceId(processInstanceId));
    return carApplyBill.getId();
    }

    读这段代码,抓住三个关键点:

    • businessKey 是业务与流程的桥。提交流程时把单据 id 当作 businessKey 交给引擎,审批结束后引擎再带着这个值回来找单据,这就是第六篇「回写」的起点。
    • processDefinitionKey 来自枚举 OaBillTypeEnum,用车申请单对应 oa_car_apply_bill。流程模型的标识必须和它一致,否则提交时找不到流程定义。
    • 提交时先把状态置为「审批中」再入库,随后才拿到实例编号回写。这就是为什么排障时看到「审批中」但 process_instance_id 为空,意味着流程提交这一步没成功。

    用车场景最有业务含量的规则是时间冲突校验:同一辆车在重叠时间段内,不能同时存在两张有效的申请单。

    private void validateTimeConflict(CarApplyBillSaveReqVO saveReqVO) {
    if (saveReqVO.getCarId() == null || saveReqVO.getGoTime() == null || saveReqVO.getReturnTime() == null) {
    return; // 如果必要字段为空,跳过校验
    }

    // 校验出车时间不能晚于回车时间
    if (saveReqVO.getGoTime().isAfter(saveReqVO.getReturnTime())) {
    throw exception(CAR_TIME_CONFLICT);
    }

    // 查询同一车辆在相同时间段内的申请单
    List<CarApplyBillDO> conflictBills = carApplyBillMapper.selectList(
    new LambdaQueryWrapperX<CarApplyBillDO>()
    .eq(CarApplyBillDO::getCarId, saveReqVO.getCarId())
    .ne(saveReqVO.getId() != null, CarApplyBillDO::getId, saveReqVO.getId()) // 排除当前编辑的记录
    .and(wrapper -> wrapper
    // 场景1:存在审批中的申请单且时间重合
    .and(subWrapper -> subWrapper
    .eq(CarApplyBillDO::getProcessStatus, RUNNING.getStatus())
    // ……时间重叠条件(三种区间重叠情形)

    这里的设计思路值得借鉴:哪些单据会「占住」一辆车,由两个状态共同决定。场景一是流程状态为审批中的单据;场景二是审批已通过、且还车状态为「待还车」或「还车中」的单据。已还车、被拒绝、被撤回的单据不再占车。这正是两条状态线配合的价值:流程状态说明审批走到哪,还车状态说明车是否真被占着。

    六、Mapper 与 DO:查询条件和数据边界

    Mapper 继承 BaseMapperX,分页查询用 LambdaQueryWrapperX 的 xxxIfPresent 系列方法拼接条件,值为空时自动跳过,列表页的筛选就是这样落地的:

    @Mapper
    public interface CarApplyBillMapper extends BaseMapperX<CarApplyBillDO> {

    default PageResult<CarApplyBillDO> selectPage(CarApplyBillPageReqVO reqVO) {
    return selectPage(reqVO, new LambdaQueryWrapperX<CarApplyBillDO>()
    .eqIfPresent(CarApplyBillDO::getId, reqVO.getId())
    .likeIfPresent(CarApplyBillDO::getBillCode, reqVO.getBillCode())
    .eqIfPresent(CarApplyBillDO::getProcessInstanceId, reqVO.getProcessInstanceId())
    .eqIfPresent(CarApplyBillDO::getProcessStatus, reqVO.getProcessStatus())
    .eqIfPresent(CarApplyBillDO::getCarNo, reqVO.getCarNo())
    .betweenIfPresent(CarApplyBillDO::getGoTime, reqVO.getGoTime())
    .betweenIfPresent(CarApplyBillDO::getReturnTime, reqVO.getReturnTime())
    .eqIfPresent(CarApplyBillDO::getCreator, reqVO.getCreator())
    .betweenIfPresent(CarApplyBillDO::getCreateTime, reqVO.getCreateTime())
    .eqIfPresent(CarApplyBillDO::getDeptId, reqVO.getDeptId())
    .eqIfPresent(CarApplyBillDO::getCompanyId, reqVO.getCompanyId())
    .eqIfPresent(CarApplyBillDO::getReturnStatus, reqVO.getReturnStatus())
    // ……其余字段条件同理
    .orderByDesc(CarApplyBillDO::getId));
    }
    }

    Mapper 里看不到任何 tenant_id 条件,是因为租户隔离不写在业务代码里:DO 继承 BaseDO,查询与写入时由 MyBatis-Plus 的租户拦截器按当前请求的租户自动拼接条件。第五篇会把这条拦截链完整讲一遍。

    另一个业务层面的数据边界写在 Service 里:

    // === CarApplyBillServiceImpl ===
    @Override
    public PageResult<CarApplyBillDO> getCarApplyBillPage(CarApplyBillPageReqVO pageReqVO) {
    // 自动添加创建人过滤条件(当前登录用户)
    Long currentUserId = SecurityFrameworkUtils.getLoginUserId();
    if (currentUserId != null) {
    pageReqVO.setCreator(String.valueOf(currentUserId));
    }
    return carApplyBillMapper.selectPage(pageReqVO);
    }

    // === ErrorCodeConstants(oa-api)===
    ErrorCode CAR_NOT_EXISTS = new ErrorCode(1_101_000_000, "车辆信息不存在");
    ErrorCode CAR_APPLY_BILL_NOT_EXISTS = new ErrorCode(1_101_000_001, "用车申请单不存在");
    ErrorCode CAR_RETURN_BILL_NOT_EXISTS = new ErrorCode(1_101_000_003, "还车申请单不存在");
    ErrorCode CAR_APPLY_BILL_ALREADY_RETURNED = new ErrorCode(1_101_000_004, "用车申请单已还车,不能重复还车");
    ErrorCode CAR_TIME_CONFLICT = new ErrorCode(1_101_000_005, "车辆使用时间冲突,该时间段已有其他申请单");

    注意分页方法强制把 creator 改写成当前登录用户。也就是说,前端传什么 creator 都没用,列表只会返回自己创建的单据。这就是「数据范围不能只靠前端」的最小例子:即使有人改了请求参数,后端也会把它纠正回来。错误码则统一放在 ErrorCodeConstants,按模块分段,业务代码里只写 throw exception(CAR_TIME_CONFLICT),文案和错误码不散落在各处。

    七、事务与一致性:先看清再决定

    读完提交方法,你应该问一个问题:保存单据、提交流程、回写实例编号,这三步在一个事务里吗?

    按当前源码,CarApplyBillServiceImpl 与 CarReturnBillServiceImpl 没有声明 @Transactional,这三步各自独立提交。这意味着:如果流程提交抛异常,前面已经入库的单据会保留,状态是「审批中」而实例编号为空。

    这既不是危言耸听,也不是要你照抄去加注解——是否应该放进一个事务、失败后回滚还是补偿,取决于流程引擎调用的失败语义。本文的建议是:新模块要显式设计这个边界,并在文档里写清楚,不要把「碰巧能跑」当成设计。已有数据排查时,用下面的查询找出这类单据:

    SELECT id, bill_code, process_status
    FROM oa_car_apply_bill
    WHERE process_status = 1
    AND (process_instance_id IS NULL OR process_instance_id = '');

    八、闭环:一次提交从浏览器到数据库

    把前面各层串起来,一次「提交用车申请」的完整链路如下:

    提交用车申请时序图:浏览器、Controller、Service、Mapper、MySQL 与 BPM 接口依次协作

    ▲ 时序图:权限与参数校验在 Controller,业务规则在 Service,租户条件在持久层自动拼接,流程提交通过 BpmProcessInstanceApi 完成,实例编号最后回写到单据。

    步骤发生在哪一层产出
    1 浏览器调用 POST /admin-api/oa/car-apply-bill/submit 带 Token 的请求
    2 Controller 校验 oa:car-apply-bill:create 与 @Valid 不通过直接返回错误
    3 Service 生成单号、校验时间冲突 冲突抛 CAR_TIME_CONFLICT
    4 Mapper 落库,状态置为审批中 得到单据 id
    5 调用 submitProcessInstance,businessKey 为单据 id 得到流程实例编号
    6 回写 process_instance_id 单据与流程绑定完成
    7 返回 CommonResult.success(id) 前端据此刷新页面

    自己验证时不必写任何新代码:在 PC 端新建一张用车申请并提交,打开浏览器 Network,找到 submit 请求,响应里应该是成功码和一个数字 id;再用上面的 SQL 查最新一条记录,看 process_status 与 process_instance_id 是否同时有值。本文没有替你执行这些步骤,请在自己的环境里核对。

    提交之后的页面长什么样?下图是一张已通过审批的用车申请单详情:

    用车申请单详情页:单据信息、审批信息、流程图与流程评论四个页签,状态为审批通过

    ▲ 用车申请单详情:顶部是单据号、申请人和状态,下方的页签把业务数据与审批过程放在同一个页面里,这一页的前端实现放到第四篇讲。

    还车申请单则是另一张单据,通过 apply_bill 指向用车单的 bill_code,并联动修改用车单的 return_status:

    还车申请单列表:展示单据状态、关联用车单、车辆与回车时间

    ▲ 还车申请单列表:每张还车单都关联一张用车单,还车审批通过后,用车单才会变成「已还车」。

    九、故障与验收矩阵

    模块做完后,用下表自检,也可以在联调出现问题时当排查顺序:

    现象最可能的层怎么确认
    点提交提示无权限 权限标识 看响应码;核对菜单按钮权限标识是否为 oa:car-apply-bill:create 并已授权给角色
    提示「车辆使用时间冲突」 Service 业务规则 对照 CAR_TIME_CONFLICT;查同一车辆的审批中与待还车单据
    列表只有自己的单据 Service 数据边界 这是分页方法强制按创建人过滤的结果,属于设计行为
    时间选择器显示 NaN 前后端日期约定 看接口返回是否为毫秒数,前端是否配 valueFormat: 'x'
    状态是审批中但没有实例编号 事务边界 用第七节的 SQL 查,再看后端日志里流程提交的异常
    审批通过后还车状态仍是 0 流程回写 看第六篇的事件监听链路是否执行了 updateProcessStatus
    重复还车被拒绝 业务规则 对照 CAR_APPLY_BILL_ALREADY_RETURNED

    十、第三天的学习结果

    做完后端篇,建议逐项确认:

  • 能区分主数据、业务单据、流程状态与业务状态,并说出用车模块里各对应什么。
  • 能画出 Controller、Service、Mapper、DO、表五层,各自只负责什么。
  • 能解释 businessKey、processDefinitionKey 和 process_instance_id 三者的关系。
  • 能说出「前端传 creator 无效」背后的原因,以及它为什么比前端过滤更可靠。
  • 能指出本模块在事务边界、VO 校验和建表注释上的不足,并知道怎么避免在新模块里重复。
  • 常见问题(FAQ)

    新增一个业务模块,后端应该先写什么?

    先写业务建模,再写代码:列出业务对象,区分主数据和单据,列出每个对象的状态线,再决定建表。建表之后再按 DO、VO、Mapper、Service、Controller 的顺序写,最后补错误码和单元测试。

    为什么业务状态不直接用流程引擎的状态?

    流程状态只表示审批走到哪一步,会被撤回、退回、取消等操作反复改变。用车单的「待还车、还车中、已还车」是业务自己的进度,必须独立存放,否则审批一动业务就乱。

    Controller 方法没有 @PreAuthorize 会有风险吗?

    要看接口是否返回敏感数据以及后端有没有数据范围兜底。本文案例中分页接口在 Service 里强制按创建人过滤,详情接口另有 get-for-bpm 校验流程参与者。新接口建议默认加权限,确认不需要时再显式取消。

    BeanUtils.toBean 与 MapStruct 怎么选?

    用车模块实际使用的是 BeanUtils.toBean,字段同名同类型时最省事;需要复杂映射、跨对象合并时再引入 MapStruct。关键是同一个模块内风格统一,并在评审时说清选择。

    本文的代码我能直接复制到自己项目吗?

    不建议整段复制。它们依赖 BaseMapperX、BillCodeUtils、BpmProcessInstanceApi 等平台能力。正确用法是对照仓库源码理解分层思路,在自己的模块里按同样的职责边界重写。

    系列进度与下一篇预告

    篇目主题状态
    (一) 架构篇——一个底座,多端通达 已发布
    (二) 启动篇——从源码到前后端联调,跑通开发环境 已发布
    (三) 后端篇——从业务建模到接口开发,做出一个完整模块 本篇
    (四) 前端篇——从菜单路由到表单列表,完成业务页面 下一篇
    (五) 权限篇——把登录、角色、数据权限与租户隔离讲透 预告
    (六) 流程篇——接入审批,让业务单据真正流转起来 预告
    (七) 上线篇——从测试验收到部署上线,完成企业项目交付 预告

    后端接口已经有了,但用户看不到页面就等于没有。下一篇进入前端篇:从菜单与路由出发,用 Vben 的 Page、useVbenVxeGrid 和 BasicForm 做出用车申请的列表页与表单页,并把今天提到的时间戳、权限标识和状态字典在界面上一一对上。想深入同一业务的设计细节,可延伸阅读 公务用车:申请占坑、还车释放、撞时段怎么拦。

    如果这篇对你有用,点个「在看」或收藏。

    🌐 演示地址:https://ruoyioffice.com/web 📦 GitHub 源码:https://github.com/yuqing2026/ruoyi-office 📦 Gitee 源码:https://gitee.com/yqzy1688/ruoyi-office 💬 微信:17156169080(获取产品咨询)

    RuoYi Office 企业管理一体化平台

    打开演示地址直接查看系统。

    赞(0)
    未经允许不得转载:171主机测评 » 7天学会SpringBoot+Vue3企业级项目RuoyiOffice(三):后端篇——从业务建模到接口开发,做出一个完整模块
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址