大数跨境

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

7天学会SpringBoot+Vue3企业级项目RuoyiOffice(三):后端篇——从业务建模到接口开发,做出一个完整模块 企业软件源码
2026-10-03
20
导读:oy

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")@Validatedpublic 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));    }

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

  1. 权限标识的命名
    是 模块:资源:动作,如 oa:car-apply-bill:create。同一个字符串会出现在三个地方:Controller 的 @PreAuthorize、菜单管理里按钮的「权限标识」、前端按钮的 auth 配置。第五篇会专门讲这条链。
  2. 「保存」和「提交」是两个接口、两个权限
    。保存只落库不起流程,提交才进入审批。
  3. 并不是每个接口都标了权限
    。/get 和 /page 没有 @PreAuthorize,而专供审批页面嵌入的 /get-for-bpm 注释写明「不需要菜单权限,但会校验当前用户是否为流程参与者」。数据范围怎么兜底,是第五篇的内容。

VO 层也要如实说一件事:CarApplyBillSaveReqVO 里没有声明 @NotNull 等校验注解,@Valid 触发后实际没有字段级约束。目前必填由前端表单的 rules: 'required' 把关,后端只在 Service 里校验时间。如果你的新模块对数据质量要求高,建议在 VO 上补齐必填约束,不要只依赖前端。

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

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

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

@Overridepublic 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 系列方法拼接条件,值为空时自动跳过,列表页的筛选就是这样落地的:

@Mapperpublic 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 ===@Overridepublic 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_statusFROM oa_car_apply_billWHERE process_status = 1  AND (process_instance_id IS NULL OR process_instance_id = '');

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

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

▲ 时序图:权限与参数校验在 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

十、第三天的学习结果

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

  1. 能区分主数据、业务单据、流程状态与业务状态,并说出用车模块里各对应什么。
  2. 能画出 Controller、Service、Mapper、DO、表五层,各自只负责什么。
  3. 能解释 businessKey、processDefinitionKey 和 process_instance_id 三者的关系。
  4. 能说出「前端传 creator 无效」背后的原因,以及它为什么比前端过滤更可靠。
  5. 能指出本模块在事务边界、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(获取产品咨询)

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

【声明】内容源于网络
0
0
企业软件源码
RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
内容 140
粉丝 0
企业软件源码 RuoyiOffice 是一套基于 Spring Boot + Vue3 +Uniapp 的企业一体化管理平台,集 OA、CRM、ERP、工作流、HR、资产、合同、项目、AI应用等业务于一体,帮助企业用一个系统协同管理多类核心业务。
总阅读2.4k
粉丝0
内容140