Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17

# Conflicts:
#	yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtilsTest.java
#	yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/AbstractBpmTaskCandidateDeptLeaderStrategy.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategy.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
#	yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java
#	yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java
#	yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java
#	yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceMessageController.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaTaskMapper.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/cal/plan/MesCalPlanMapper.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImpl.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImpl.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/PayChannelEnum.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/PayClientFactoryImpl.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
#	yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java
#	yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImplTest.java
#	yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImplTest.java
This commit is contained in:
YunaiV
2026-08-30 21:00:44 +08:00
989 changed files with 118479 additions and 3720 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

+22 -4
View File
@@ -28,8 +28,8 @@
| 【完整版】[ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [`master`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master-jdk17/) 分支 | [`master-jdk25`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master-jdk25/) 分支 |
| 【精简版】[yudao-boot-mini](https://gitee.com/yudaocode/yudao-boot-mini) | [`master`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master-jdk17/) 分支 | [`master-jdk25`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master-jdk25/) 分支 |
* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能
* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、HRM、FMS、IM 即时通讯、AI 大模型、IoT 物联网等功能
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、HRM、FMS、IM 即时通讯、AI 大模型、IoT 物联网等功能
可参考 [《迁移文档》](https://doc.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
@@ -102,7 +102,7 @@
团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。
项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 即时通讯、微信公众号、微信小程序等等。
项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、HRM 人力资源系统、FMS 财务系统、CMS 系统、HIS 系统、支付系统、IM 即时通讯、微信公众号、微信小程序等等。
## 🐼 内置功能
@@ -112,7 +112,7 @@
* 通用模块(必选):系统功能、基础设施
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏
* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、HRM 人力资源管理、FMS 财务管理、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
>
@@ -292,6 +292,22 @@
![功能图](/.image/common/mes-preview.png)
### HRM 人力资源管理系统
演示地址:<https://doc.iocoder.cn/hrm-preview/>
![功能图](/.image/common/hrm-feature.png)
![预览图](/.image/common/hrm-preview.png)
### FMS 财务管理系统
演示地址:<https://doc.iocoder.cn/fms-preview/>
![功能图](/.image/common/fms-feature.png)
![预览图](/.image/common/fms-preview.png)
### AI 大模型
演示地址:<https://doc.iocoder.cn/ai-preview/>
@@ -340,6 +356,8 @@
| `yudao-module-crm` | CRM 系统的 Module 模块 |
| `yudao-module-mes` | MES 系统的 Module 模块 |
| `yudao-module-wms` | WMS 系统的 Module 模块 |
| `yudao-module-hrm` | HRM 人力资源管理的 Module 模块 |
| `yudao-module-fms` | FMS 财务管理的 Module 模块 |
| `yudao-module-im` | IM 即时通讯的 Module 模块 |
| `yudao-module-ai` | AI 大模型的 Module 模块 |
| `yudao-module-iot` | IoT 物联网的 Module 模块 |
+4 -2
View File
@@ -26,7 +26,9 @@
<!-- <module>yudao-module-iot</module>-->
<!-- <module>yudao-module-mes</module>-->
<!-- <module>yudao-module-wms</module>-->
<!-- <module>yudao-module-im</module>-->
<!-- <module>yudao-module-hrm</module>-->
<!-- <module>yudao-module-fms</module>-->
<!-- <module>yudao-module-im</module>-->
<!-- 请参考 https://doc.iocoder.cn/ai/build/ 文档,完成 AI 模块的启动!!! -->
<!-- <module>yudao-module-ai</module>-->
</modules>
@@ -36,7 +38,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.07-SNAPSHOT</revision>
<revision>2026.08-SNAPSHOT</revision>
<!-- Maven 相关 -->
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
+1
View File
@@ -716,6 +716,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (117, 11, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code', 0, 'primary', '', '支付宝 Wap 网站支付', '1', '2021-12-03 10:42:26', '1', '2023-07-19 20:09:16', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (118, 12, '支付宝 App 支付', 'alipay_app', 'pay_channel_code', 0, 'primary', '', '支付宝 App 支付', '1', '2021-12-03 10:42:55', '1', '2023-07-19 20:09:20', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (119, 14, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code', 0, 'primary', '', '支付宝扫码支付', '1', '2021-12-03 10:43:10', '1', '2023-07-19 20:09:28', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3449, 15, '支付宝小程序支付', 'alipay_lite', 'pay_channel_code', 0, 'primary', '', '支付宝小程序支付', '1', '2026-08-18 00:00:00', '1', '2026-08-18 00:00:00', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (120, 10, '通知成功', '10', 'pay_notify_status', 0, 'success', '', '通知成功', '1', '2021-12-03 11:02:41', '1', '2023-07-19 10:08:19', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (121, 20, '通知失败', '20', 'pay_notify_status', 0, 'danger', '', '通知失败', '1', '2021-12-03 11:02:59', '1', '2023-07-19 10:08:21', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (122, 0, '等待通知', '0', 'pay_notify_status', 0, 'info', '', '未通知', '1', '2021-12-03 11:03:10', '1', '2023-07-19 10:08:24', '0');
+1 -1
View File
@@ -817,6 +817,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (117, 11, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code', 0, 'primary', '', '支付宝 Wap 网站支付', '1', '2021-12-03 10:42:26', '1', '2023-07-19 20:09:16', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (118, 12, '支付宝 App 支付', 'alipay_app', 'pay_channel_code', 0, 'primary', '', '支付宝 App 支付', '1', '2021-12-03 10:42:55', '1', '2023-07-19 20:09:20', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (119, 14, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code', 0, 'primary', '', '支付宝扫码支付', '1', '2021-12-03 10:43:10', '1', '2023-07-19 20:09:28', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3449, 15, '支付宝小程序支付', 'alipay_lite', 'pay_channel_code', 0, 'primary', '', '支付宝小程序支付', '1', '2026-08-18 00:00:00', '1', '2026-08-18 00:00:00', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (120, 10, '通知成功', '10', 'pay_notify_status', 0, 'success', '', '通知成功', '1', '2021-12-03 11:02:41', '1', '2023-07-19 10:08:19', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (121, 20, '通知失败', '20', 'pay_notify_status', 0, 'danger', '', '通知失败', '1', '2021-12-03 11:02:59', '1', '2023-07-19 10:08:21', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (122, 0, '等待通知', '0', 'pay_notify_status', 0, 'info', '', '未通知', '1', '2021-12-03 11:03:10', '1', '2023-07-19 10:08:24', '0');
@@ -6195,4 +6196,3 @@ COMMIT;
DROP SEQUENCE IF EXISTS yudao_demo03_student_seq;
CREATE SEQUENCE yudao_demo03_student_seq
START 10;
+2 -1
View File
@@ -817,6 +817,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (117, 11, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code', 0, 'primary', '', '支付宝 Wap 网站支付', '1', '2021-12-03 10:42:26', '1', '2023-07-19 20:09:16', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (118, 12, '支付宝 App 支付', 'alipay_app', 'pay_channel_code', 0, 'primary', '', '支付宝 App 支付', '1', '2021-12-03 10:42:55', '1', '2023-07-19 20:09:20', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (119, 14, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code', 0, 'primary', '', '支付宝扫码支付', '1', '2021-12-03 10:43:10', '1', '2023-07-19 20:09:28', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3449, 15, '支付宝小程序支付', 'alipay_lite', 'pay_channel_code', 0, 'primary', '', '支付宝小程序支付', '1', '2026-08-18 00:00:00', '1', '2026-08-18 00:00:00', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (120, 10, '通知成功', '10', 'pay_notify_status', 0, 'success', '', '通知成功', '1', '2021-12-03 11:02:41', '1', '2023-07-19 10:08:19', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (121, 20, '通知失败', '20', 'pay_notify_status', 0, 'danger', '', '通知失败', '1', '2021-12-03 11:02:59', '1', '2023-07-19 10:08:21', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (122, 0, '等待通知', '0', 'pay_notify_status', 0, 'info', '', '未通知', '1', '2021-12-03 11:03:10', '1', '2023-07-19 10:08:24', '0');
@@ -1663,7 +1664,7 @@ COMMIT;
DROP SEQUENCE IF EXISTS system_dict_data_seq;
CREATE SEQUENCE system_dict_data_seq
START 3449;
START 3450;
-- ----------------------------
-- Table structure for system_dict_type
+5488 -3637
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -817,6 +817,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (117, 11, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code', 0, 'primary', '', '支付宝 Wap 网站支付', '1', '2021-12-03 10:42:26', '1', '2023-07-19 20:09:16', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (118, 12, '支付宝 App 支付', 'alipay_app', 'pay_channel_code', 0, 'primary', '', '支付宝 App 支付', '1', '2021-12-03 10:42:55', '1', '2023-07-19 20:09:20', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (119, 14, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code', 0, 'primary', '', '支付宝扫码支付', '1', '2021-12-03 10:43:10', '1', '2023-07-19 20:09:28', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3449, 15, '支付宝小程序支付', 'alipay_lite', 'pay_channel_code', 0, 'primary', '', '支付宝小程序支付', '1', '2026-08-18 00:00:00', '1', '2026-08-18 00:00:00', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (120, 10, '通知成功', '10', 'pay_notify_status', 0, 'success', '', '通知成功', '1', '2021-12-03 11:02:41', '1', '2023-07-19 10:08:19', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (121, 20, '通知失败', '20', 'pay_notify_status', 0, 'danger', '', '通知失败', '1', '2021-12-03 11:02:59', '1', '2023-07-19 10:08:21', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (122, 0, '等待通知', '0', 'pay_notify_status', 0, 'info', '', '未通知', '1', '2021-12-03 11:03:10', '1', '2023-07-19 10:08:24', '0');
@@ -1663,7 +1664,7 @@ COMMIT;
DROP SEQUENCE IF EXISTS system_dict_data_seq;
CREATE SEQUENCE system_dict_data_seq
START 3449;
START 3450;
-- ----------------------------
-- Table structure for system_dict_type
+2 -1
View File
@@ -769,6 +769,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (117, 11, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code', 0, 'primary', '', '支付宝 Wap 网站支付', '1', to_date('2021-12-03 10:42:26', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-07-19 20:09:16', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (118, 12, '支付宝 App 支付', 'alipay_app', 'pay_channel_code', 0, 'primary', '', '支付宝 App 支付', '1', to_date('2021-12-03 10:42:55', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-07-19 20:09:20', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (119, 14, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code', 0, 'primary', '', '支付宝扫码支付', '1', to_date('2021-12-03 10:43:10', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-07-19 20:09:28', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3449, 15, '支付宝小程序支付', 'alipay_lite', 'pay_channel_code', 0, 'primary', '', '支付宝小程序支付', '1', to_date('2026-08-18 00:00:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-08-18 00:00:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (120, 10, '通知成功', '10', 'pay_notify_status', 0, 'success', '', '通知成功', '1', to_date('2021-12-03 11:02:41', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-07-19 10:08:19', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (121, 20, '通知失败', '20', 'pay_notify_status', 0, 'danger', '', '通知失败', '1', to_date('2021-12-03 11:02:59', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-07-19 10:08:21', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (122, 0, '等待通知', '0', 'pay_notify_status', 0, 'info', '', '未通知', '1', to_date('2021-12-03 11:03:10', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-07-19 10:08:24', 'SYYYY-MM-DD HH24:MI:SS'), '0');
@@ -1614,7 +1615,7 @@ COMMIT;
-- @formatter:on
CREATE SEQUENCE system_dict_data_seq
START WITH 3449;
START WITH 3450;
-- ----------------------------
-- Table structure for system_dict_type
+1
View File
@@ -817,6 +817,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (117, 11, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code', 0, 'primary', '', '支付宝 Wap 网站支付', '1', '2021-12-03 10:42:26', '1', '2023-07-19 20:09:16', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (118, 12, '支付宝 App 支付', 'alipay_app', 'pay_channel_code', 0, 'primary', '', '支付宝 App 支付', '1', '2021-12-03 10:42:55', '1', '2023-07-19 20:09:20', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (119, 14, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code', 0, 'primary', '', '支付宝扫码支付', '1', '2021-12-03 10:43:10', '1', '2023-07-19 20:09:28', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3449, 15, '支付宝小程序支付', 'alipay_lite', 'pay_channel_code', 0, 'primary', '', '支付宝小程序支付', '1', '2026-08-18 00:00:00', '1', '2026-08-18 00:00:00', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (120, 10, '通知成功', '10', 'pay_notify_status', 0, 'success', '', '通知成功', '1', '2021-12-03 11:02:41', '1', '2023-07-19 10:08:19', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (121, 20, '通知失败', '20', 'pay_notify_status', 0, 'danger', '', '通知失败', '1', '2021-12-03 11:02:59', '1', '2023-07-19 10:08:21', '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (122, 0, '等待通知', '0', 'pay_notify_status', 0, 'info', '', '未通知', '1', '2021-12-03 11:03:10', '1', '2023-07-19 10:08:24', '0');
+1
View File
@@ -2258,6 +2258,7 @@ GO
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (118, 12, N'支付宝 App 支付', N'alipay_app', N'pay_channel_code', 0, N'primary', N'', N'支付宝 App 支付', N'1', N'2021-12-03 10:42:55', N'1', N'2023-07-19 20:09:20', N'0')
GO
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (119, 14, N'支付宝扫码支付', N'alipay_qr', N'pay_channel_code', 0, N'primary', N'', N'支付宝扫码支付', N'1', N'2021-12-03 10:43:10', N'1', N'2023-07-19 20:09:28', N'0')
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3449, 15, N'支付宝小程序支付', N'alipay_lite', N'pay_channel_code', 0, N'primary', N'', N'支付宝小程序支付', N'1', N'2026-08-18 00:00:00', N'1', N'2026-08-18 00:00:00', N'0')
GO
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (120, 10, N'通知成功', N'10', N'pay_notify_status', 0, N'success', N'', N'通知成功', N'1', N'2021-12-03 11:02:41', N'1', N'2023-07-19 10:08:19', N'0')
GO
+34 -20
View File
@@ -14,7 +14,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.07-SNAPSHOT</revision>
<revision>2026.08-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
<!-- 统一依赖管理 -->
<spring.boot.version>3.5.15</spring.boot.version>
@@ -24,15 +24,15 @@
<!-- DB 相关 -->
<druid.version>1.2.28</druid.version>
<mybatis.version>3.5.19</mybatis.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
<mybatis-plus.version>3.5.17</mybatis-plus.version>
<mybatis-plus-join.version>1.5.9</mybatis-plus-join.version>
<dynamic-datasource.version>4.5.0</dynamic-datasource.version>
<easy-trans.version>3.1.5</easy-trans.version>
<redisson.version>4.6.1</redisson.version>
<easy-trans.version>3.1.8</easy-trans.version>
<redisson.version>4.7.0</redisson.version>
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
<kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version>
<opengauss.jdbc.version>7.0.0-RC3-og</opengauss.jdbc.version>
<taos.version>3.8.4</taos.version>
<taos.version>3.9.0</taos.version>
<!-- 消息队列 -->
<rocketmq-spring.version>2.3.6</rocketmq-spring.version>
<!-- 服务保障相关 -->
@@ -40,48 +40,50 @@
<!-- 监控相关 -->
<skywalking.version>9.6.0</skywalking.version>
<spring-boot-admin.version>3.5.9</spring-boot-admin.version>
<opentelemetry.version>1.39.0</opentelemetry.version>
<opentelemetry.version>1.65.0</opentelemetry.version>
<!-- Test 测试相关 -->
<h2.version>2.3.232</h2.version>
<podam.version>8.0.2.RELEASE</podam.version>
<jedis-mock.version>1.1.15</jedis-mock.version>
<jedis-mock.version>1.1.18</jedis-mock.version>
<mockito-inline.version>5.2.0</mockito-inline.version>
<!-- Bpm 工作流相关 -->
<flowable.version>8.0.0</flowable.version>
<!-- 工具类相关 -->
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
<jsoup.version>1.22.2</jsoup.version>
<jsoup.version>1.23.2</jsoup.version>
<sensitive-word.version>0.29.5</sensitive-word.version>
<pinyin4j.version>2.5.1</pinyin4j.version>
<lombok.version>1.18.46</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version>
<hutool-5.version>5.8.46</hutool-5.version>
<hutool-5.version>5.8.47</hutool-5.version>
<hutool-6.version>6.0.0-M22</hutool-6.version>
<fastexcel.version>1.3.0</fastexcel.version>
<aviator.version>5.4.4</aviator.version>
<velocity.version>2.4.1</velocity.version>
<fastjson2.version>2.0.63</fastjson2.version>
<guava.version>33.6.0-jre</guava.version>
<fastjson2.version>2.0.64</fastjson2.version>
<guava.version>33.7.1-jre</guava.version>
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<commons-net.version>3.13.0</commons-net.version>
<commons-lang3.version>3.20.0</commons-lang3.version>
<jsch.version>2.28.3</jsch.version>
<jsch.version>2.28.7</jsch.version>
<tika-core.version>3.3.1</tika-core.version>
<ip2region.version>2.7.0</ip2region.version>
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
<netty.version>4.2.15.Final</netty.version>
<netty.version>4.2.17.Final</netty.version>
<mqtt.version>1.2.5</mqtt.version>
<vertx.version>4.5.26</vertx.version>
<okhttp.version>4.12.0</okhttp.version>
<californium.version>3.14.0</californium.version>
<j2mod.version>3.3.0</j2mod.version>
<!-- 三方云服务相关 -->
<awssdk.version>2.46.17</awssdk.version>
<awssdk.version>2.54.7</awssdk.version>
<justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>2.3.4</jimureport.version>
<jimubi.version>2.3.2</jimubi.version>
<weixin-java.version>4.8.4-20260623.211820</weixin-java.version>
<jimureport.version>2.5.1</jimureport.version>
<jimubi.version>2.5.0</jimubi.version>
<weixin-java.version>4.8.6-20260825.155844</weixin-java.version>
<bouncycastle.version>1.80</bouncycastle.version>
<alipay-sdk-java.version>4.40.865.ALL</alipay-sdk-java.version>
<alipay-sdk-java.version>4.40.978.ALL</alipay-sdk-java.version>
</properties>
<dependencyManagement>
@@ -267,7 +269,7 @@
</exclusion>
<exclusion>
<groupId>org.redisson</groupId>
<!-- Redisson 4.6.x 默认依赖 redisson-spring-data-41(适配 Spring Data Redis 4.x),排除后使用下方的 spring-data-35 适配 Spring Boot 3.5 -->
<!-- Redisson 4.7.x 默认依赖 redisson-spring-data-41(适配 Spring Data Redis 4.x),排除后使用下方的 spring-data-35 适配 Spring Boot 3.5 -->
<artifactId>redisson-spring-data-41</artifactId>
</exclusion>
</exclusions>
@@ -413,6 +415,12 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
<dependency>
<groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 -->
<artifactId>jedis-mock</artifactId>
@@ -490,6 +498,12 @@
<version>${fastexcel.version}</version>
</dependency>
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>${aviator.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId> <!-- 文件类型的识别 -->
@@ -4,8 +4,10 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import com.google.common.collect.ImmutableMap;
import java.math.BigDecimal;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
@@ -33,6 +35,34 @@ public class CollectionUtils {
return from.stream().anyMatch(predicate);
}
public static <T> long sum(Collection<T> from, ToLongFunction<T> valueFunc) {
if (CollUtil.isEmpty(from)) {
return 0L;
}
return from.stream().mapToLong(valueFunc).sum();
}
public static <T> BigDecimal sumBigDecimal(Collection<T> from, Function<T, BigDecimal> valueFunc) {
if (CollUtil.isEmpty(from)) {
return BigDecimal.ZERO;
}
return from.stream().map(valueFunc).map(NumberUtils::zeroIfNull).reduce(BigDecimal.ZERO, BigDecimal::add);
}
public static <T> long count(Collection<T> from, Predicate<T> predicate) {
if (CollUtil.isEmpty(from)) {
return 0L;
}
return from.stream().filter(predicate).count();
}
public static <T, R> long distinctCount(Collection<T> from, Function<T, R> keyMapper) {
if (CollUtil.isEmpty(from)) {
return 0L;
}
return from.stream().map(keyMapper).filter(Objects::nonNull).distinct().count();
}
public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
if (CollUtil.isEmpty(from)) {
return new ArrayList<>();
@@ -124,6 +154,15 @@ public class CollectionUtils {
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
}
public static <T, U, S extends Set<U>> S convertSetBySupplier(
Collection<T> from, Function<T, U> func, Supplier<S> supplier) {
if (CollUtil.isEmpty(from)) {
return supplier.get();
}
return from.stream().map(func).filter(Objects::nonNull)
.collect(Collectors.toCollection(supplier));
}
public static <T, U> Set<U> convertLinkedSet(Collection<T> from, Function<T, U> func) {
if (CollUtil.isEmpty(from)) {
return new LinkedHashSet<>();
@@ -398,4 +437,4 @@ public class CollectionUtils {
return list;
}
}
}
@@ -6,6 +6,9 @@ import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.sql.Timestamp;
import java.time.*;
@@ -14,13 +17,16 @@ import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.TimeZone;
import java.util.stream.Collectors;
import static cn.hutool.core.date.DatePattern.*;
/**
* 时间工具类,用于 {@link LocalDateTime}
* 时间工具类,用于 {@link LocalDate}、{@link LocalDateTime}
*
* @author 芋道源码
*/
@@ -54,6 +60,16 @@ public class LocalDateTimeUtils {
}
}
/**
* 解析年月字符串为 {@link YearMonth},格式为 yyyy-MM
*
* @param month 年月字符串
* @return 年月
*/
public static YearMonth parseYearMonth(String month) {
return YearMonth.parse(month, NORM_MONTH_FORMATTER);
}
public static LocalDateTime addTime(Duration duration) {
return LocalDateTime.now().plus(duration);
}
@@ -70,6 +86,319 @@ public class LocalDateTimeUtils {
return date.isAfter(LocalDateTime.now());
}
public static boolean beforeOrEqualNow(LocalDateTime time) {
LocalDateTime now = LocalDateTime.now();
return time.isBefore(now) || time.isEqual(now);
}
public static boolean beforeNow(LocalDate date) {
return date.isBefore(LocalDate.now());
}
public static boolean beforeOrEqualNow(LocalDate date) {
LocalDate today = LocalDate.now();
return date.isBefore(today) || date.isEqual(today);
}
public static boolean afterNow(LocalDate date) {
return date.isAfter(LocalDate.now());
}
/**
* 判断时间是否处于一月
*
* @param time 时间
* @return 是否处于一月
*/
public static boolean isJanuary(LocalDateTime time) {
return time.getMonthValue() == 1;
}
/**
* 判断第一个日期是否早于或等于第二个日期
*
* @param firstDate 第一个日期
* @param secondDate 第二个日期
* @return 是否早于或等于
*/
public static boolean isBeforeOrEqual(LocalDate firstDate, LocalDate secondDate) {
return firstDate.isBefore(secondDate) || firstDate.isEqual(secondDate);
}
/**
* 判断第一个日期是否晚于或等于第二个日期
*
* @param firstDate 第一个日期
* @param secondDate 第二个日期
* @return 是否晚于或等于
*/
public static boolean isAfterOrEqual(LocalDate firstDate, LocalDate secondDate) {
return firstDate.isAfter(secondDate) || firstDate.isEqual(secondDate);
}
/**
* 判断第一个时间是否早于或等于第二个时间
*
* @param firstTime 第一个时间
* @param secondTime 第二个时间
* @return 是否早于或等于
*/
public static boolean isBeforeOrEqual(LocalDateTime firstTime, LocalDateTime secondTime) {
return firstTime.isBefore(secondTime) || firstTime.isEqual(secondTime);
}
/**
* 判断第一个时间是否晚于或等于第二个时间
*
* @param firstTime 第一个时间
* @param secondTime 第二个时间
* @return 是否晚于或等于
*/
public static boolean isAfterOrEqual(LocalDateTime firstTime, LocalDateTime secondTime) {
return firstTime.isAfter(secondTime) || firstTime.isEqual(secondTime);
}
/**
* 判断时间范围是否有效
*
* @param beginTime 开始时间
* @param endTime 结束时间
* @return 开始、结束时间非空,且结束时间晚于开始时间时返回 {@code true}
*/
public static boolean isTimeRangeValid(LocalDateTime beginTime, LocalDateTime endTime) {
return beginTime != null && endTime != null && endTime.isAfter(beginTime);
}
/**
* 判断时间范围是否存在有效边界
*
* @param times 时间范围
* @return 任一边界非空时返回 {@code true}
*/
public static boolean isTimeRangePresent(LocalDateTime[] times) {
return ArrayUtils.get(times, 0) != null || ArrayUtils.get(times, 1) != null;
}
/**
* 判断指定日期是否位于闭区间内
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @param date 指定日期
* @return 是否位于闭区间内
*/
public static boolean isBetween(LocalDate beginDate, LocalDate endDate, LocalDate date) {
return beginDate != null && endDate != null && date != null
&& isAfterOrEqual(date, beginDate) && isBeforeOrEqual(date, endDate);
}
/**
* 判断指定日期是否不在闭区间内
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @param date 指定日期
* @return 是否不在闭区间内;任一参数为空时返回 {@code true}
*/
public static boolean isNotBetween(LocalDate beginDate, LocalDate endDate, LocalDate date) {
return !isBetween(beginDate, endDate, date);
}
/**
* 判断指定时间是否位于闭区间内
*
* @param beginTime 开始时间
* @param endTime 结束时间
* @param time 指定时间
* @return 是否位于闭区间内
*/
public static boolean isBetween(LocalDateTime beginTime, LocalDateTime endTime, LocalDateTime time) {
return beginTime != null && endTime != null && time != null
&& isAfterOrEqual(time, beginTime) && isBeforeOrEqual(time, endTime);
}
/**
* 判断指定时间是否不在闭区间内
*
* @param beginTime 开始时间
* @param endTime 结束时间
* @param time 指定时间
* @return 是否不在闭区间内;任一参数为空时返回 {@code true}
*/
public static boolean isNotBetween(LocalDateTime beginTime, LocalDateTime endTime, LocalDateTime time) {
return !isBetween(beginTime, endTime, time);
}
/**
* 计算两个日期之间经过的完整年数
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 完整年数;日期为空或开始日期晚于结束日期时,返回 0
*/
public static int getYearsBetween(LocalDate beginDate, LocalDate endDate) {
if (beginDate == null || endDate == null || beginDate.isAfter(endDate)) {
return 0;
}
return Period.between(beginDate, endDate).getYears();
}
/**
* 从时间范围列表中扣除指定时间范围
*
* @param sourceRanges 原时间范围列表
* @param excludedRange 需要扣除的时间范围
* @return 扣除后的时间范围列表
*/
public static List<TimeRange> subtractTimeRanges(List<TimeRange> sourceRanges, TimeRange excludedRange) {
List<TimeRange> result = new ArrayList<>();
for (TimeRange sourceRange : sourceRanges) {
TimeRange overlap = intersectTimeRange(sourceRange, excludedRange);
if (overlap == null) {
result.add(sourceRange);
continue;
}
if (sourceRange.getStartTime().isBefore(overlap.getStartTime())) {
result.add(new TimeRange(sourceRange.getStartTime(), overlap.getStartTime()));
}
if (overlap.getEndTime().isBefore(sourceRange.getEndTime())) {
result.add(new TimeRange(overlap.getEndTime(), sourceRange.getEndTime()));
}
}
return result;
}
/**
* 合并重叠或首尾相接的时间范围
*
* @param timeRanges 时间范围列表
* @return 合并后的时间范围列表
*/
public static List<TimeRange> mergeTimeRanges(List<TimeRange> timeRanges) {
if (CollUtil.isEmpty(timeRanges)) {
return Collections.emptyList();
}
List<TimeRange> sortedRanges = timeRanges.stream()
.sorted(Comparator.comparing(TimeRange::getStartTime))
.collect(Collectors.toList());
List<TimeRange> result = new ArrayList<>();
TimeRange currentRange = CollUtil.getFirst(sortedRanges);
for (int i = 1; i < sortedRanges.size(); i++) {
TimeRange nextRange = sortedRanges.get(i);
if (nextRange.getStartTime().isAfter(currentRange.getEndTime())) {
result.add(currentRange);
currentRange = nextRange;
continue;
}
LocalDateTime endTime = currentRange.getEndTime().isAfter(nextRange.getEndTime())
? currentRange.getEndTime() : nextRange.getEndTime();
currentRange = new TimeRange(currentRange.getStartTime(), endTime);
}
result.add(currentRange);
return result;
}
/**
* 计算两个时间范围列表的交集
*
* @param firstRanges 第一组时间范围
* @param secondRanges 第二组时间范围
* @return 合并后的交集时间范围列表
*/
public static List<TimeRange> intersectTimeRanges(
List<TimeRange> firstRanges, List<TimeRange> secondRanges) {
List<TimeRange> result = new ArrayList<>();
for (TimeRange firstRange : firstRanges) {
for (TimeRange secondRange : secondRanges) {
TimeRange overlap = intersectTimeRange(firstRange, secondRange);
if (overlap != null) {
result.add(overlap);
}
}
}
return mergeTimeRanges(result);
}
/**
* 计算两个时间范围的交集
*
* @param firstRange 第一个时间范围
* @param secondRange 第二个时间范围
* @return 交集时间范围;没有交集时返回 {@code null}
*/
public static TimeRange intersectTimeRange(TimeRange firstRange, TimeRange secondRange) {
LocalDateTime startTime = firstRange.getStartTime().isAfter(secondRange.getStartTime())
? firstRange.getStartTime() : secondRange.getStartTime();
LocalDateTime endTime = firstRange.getEndTime().isBefore(secondRange.getEndTime())
? firstRange.getEndTime() : secondRange.getEndTime();
return endTime.isAfter(startTime) ? new TimeRange(startTime, endTime) : null;
}
/**
* 计算时间范围的总分钟数
*
* @param timeRanges 时间范围列表
* @return 总分钟数
*/
public static int calculateDurationMinutes(List<TimeRange> timeRanges) {
return timeRanges.stream()
.mapToInt(range -> Math.toIntExact(Duration.between(
range.getStartTime(), range.getEndTime()).toMinutes()))
.sum();
}
/**
* 构建日期范围内每天对应的时间范围
*
* 当结束时刻早于或等于开始时刻时,按跨天时间范围处理。
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @param beginTime 每日开始时刻
* @param endTime 每日结束时刻
* @return 每日时间范围列表
*/
public static List<TimeRange> buildDailyTimeRanges(
LocalDate beginDate, LocalDate endDate, LocalTime beginTime, LocalTime endTime) {
if (beginDate == null || endDate == null || beginTime == null || endTime == null
|| beginDate.isAfter(endDate)) {
return Collections.emptyList();
}
List<TimeRange> result = new ArrayList<>();
for (LocalDate date = beginDate; isBeforeOrEqual(date, endDate); date = date.plusDays(1)) {
LocalDateTime rangeBeginTime = date.atTime(beginTime);
LocalDateTime rangeEndTime = date.atTime(endTime);
if (rangeEndTime.isBefore(rangeBeginTime) || rangeEndTime.isEqual(rangeBeginTime)) {
rangeEndTime = rangeEndTime.plusDays(1);
}
result.add(new TimeRange(rangeBeginTime, rangeEndTime));
}
return result;
}
/**
* 查找包含指定时刻的每日时间范围
*
* 每日时间范围允许跨天,因此会同时检查基准日期的前一天、当天和后一天。
* 范围匹配使用左右闭区间。
*
* @param date 基准日期
* @param beginTime 每日开始时刻
* @param endTime 每日结束时刻
* @param time 指定时刻
* @return 包含指定时刻的时间范围;不存在时返回 {@code null}
*/
public static TimeRange findDailyTimeRange(
LocalDate date, LocalTime beginTime, LocalTime endTime, LocalDateTime time) {
if (date == null || time == null) {
return null;
}
List<TimeRange> timeRanges = buildDailyTimeRanges(
date.minusDays(1), date.plusDays(1), beginTime, endTime);
return CollUtil.findOne(timeRanges, timeRange -> isBetween(
timeRange.getStartTime(), timeRange.getEndTime(), time));
}
/**
* 将 Unix 秒时间戳转换为默认时区的本地时间
*
@@ -184,6 +513,62 @@ public class LocalDateTimeUtils {
LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2));
}
/**
* 获得指定日期的开始时间
*
* @param date 日期
* @return 当日开始时间;日期为空时返回 {@code null}
*/
public static LocalDateTime getDayBeginTime(LocalDate date) {
return date == null ? null : date.atStartOfDay();
}
/**
* 获得指定时间所在日期的开始时间
*
* @param time 时间
* @return 当日开始时间;时间为空时返回 {@code null}
*/
public static LocalDateTime getDayBeginTime(LocalDateTime time) {
return time == null ? null : getDayBeginTime(time.toLocalDate());
}
/**
* 获得指定日期的结束时间
*
* @param date 日期
* @return 当日结束时间;日期为空时返回 {@code null}
*/
public static LocalDateTime getDayEndTime(LocalDate date) {
return date == null ? null : date.atTime(LocalTime.MAX);
}
/**
* 获得自然日期范围对应的日期时间闭区间
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 从开始日期零点到结束日期最后时刻的闭区间;两个日期都为空时返回 {@code null}
*/
public static LocalDateTime[] getDateTimeRange(LocalDate beginDate, LocalDate endDate) {
if (beginDate == null && endDate == null) {
return null;
}
return new LocalDateTime[]{getDayBeginTime(beginDate), getDayEndTime(endDate)};
}
/**
* 获得指定月份的日期时间闭区间
*
* @param year 年份
* @param month 月份
* @return 从当月月初零点到月末最后时刻的闭区间
*/
public static LocalDateTime[] getMonthDateTimeRange(int year, int month) {
LocalDate monthBeginDate = LocalDate.of(year, month, 1);
return getDateTimeRange(monthBeginDate, monthBeginDate.with(TemporalAdjusters.lastDayOfMonth()));
}
/**
* 获取指定日期所在的月份的开始时间
* 例如:2023-09-30 00:00:00,000
@@ -195,6 +580,16 @@ public class LocalDateTimeUtils {
return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
}
/**
* 获取指定日期的开始时间
*
* @param date 日期
* @return 当日开始时间
*/
public static LocalDateTime beginOfDay(LocalDateTime date) {
return date.with(LocalTime.MIN);
}
/**
* 获取指定日期所在的月份的最后时间
* 例如:2023-09-30 23:59:59,999
@@ -206,6 +601,82 @@ public class LocalDateTimeUtils {
return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
}
/**
* 获得指定年月的月初时间
*
* @param year 年份
* @param month 月份
* @return 月初时间
*/
public static LocalDateTime getMonthBeginTime(int year, int month) {
return LocalDateTime.of(year, month, 1, 0, 0);
}
/**
* 获得指定年月的月初时间
*
* @param month 年月
* @return 月初时间
*/
public static LocalDateTime getMonthBeginTime(YearMonth month) {
return getMonthBeginTime(month.getYear(), month.getMonthValue());
}
/**
* 获得指定年月的下月月初时间
*
* @param year 年份
* @param month 月份
* @return 下月月初时间
*/
public static LocalDateTime getNextMonthBeginTime(int year, int month) {
return getMonthBeginTime(year, month).plusMonths(1);
}
/**
* 获得指定年月的下月月初时间
*
* @param month 年月
* @return 下月月初时间
*/
public static LocalDateTime getNextMonthBeginTime(YearMonth month) {
return getMonthBeginTime(month).plusMonths(1);
}
/**
* 判断两个时间段是否存在正时长的重叠
*
* @param beginTime1 第一个范围的开始时间
* @param endTime1 第一个范围的结束时间
* @param beginTime2 第二个范围的开始时间
* @param endTime2 第二个范围的结束时间
* @return 是否存在正时长的重叠
*/
public static boolean isOverlap(LocalDateTime beginTime1, LocalDateTime endTime1,
LocalDateTime beginTime2, LocalDateTime endTime2) {
if (beginTime1 == null || endTime1 == null || beginTime2 == null || endTime2 == null) {
return false;
}
return beginTime1.isBefore(endTime2) && endTime1.isAfter(beginTime2);
}
/**
* 判断两个左右闭合的时间范围是否重叠
*
* @param beginTime1 第一个范围的开始时间
* @param endTime1 第一个范围的结束时间
* @param beginTime2 第二个范围的开始时间
* @param endTime2 第二个范围的结束时间
* @return 是否重叠
*/
public static boolean isClosedRangeOverlap(LocalDateTime beginTime1, LocalDateTime endTime1,
LocalDateTime beginTime2, LocalDateTime endTime2) {
if (beginTime1 == null || endTime1 == null || beginTime2 == null || endTime2 == null) {
return false;
}
return isBeforeOrEqual(beginTime1, endTime2) && isAfterOrEqual(endTime1, beginTime2);
}
/**
* 获得指定日期所在季度
*
@@ -353,7 +824,11 @@ public class LocalDateTimeUtils {
* @return 日期列表,包含开始日期
*/
public static List<LocalDate> getDateList(LocalDate startDate, int days) {
return startDate.datesUntil(startDate.plusDays(days)).toList();
List<LocalDate> dateList = new ArrayList<>(days);
for (int i = 0; i < days; i++) {
dateList.add(startDate.plusDays(i));
}
return dateList;
}
/**
@@ -433,4 +908,23 @@ public class LocalDateTimeUtils {
return sourceDateTime.atZone(zoneId).toEpochSecond();
}
/**
* 时间范围
*/
@Getter
@AllArgsConstructor
@SuppressWarnings("ClassCanBeRecord")
public static class TimeRange {
/**
* 开始时间
*/
private final LocalDateTime startTime;
/**
* 结束时间
*/
private final LocalDateTime endTime;
}
}
@@ -96,6 +96,39 @@ public class MoneyUtils {
return new Money(0, fen).toString();
}
/**
* 格式化金额,默认进行四舍五入
*
* 位数:{@link #PRICE_SCALE}。金额为空时,按 0 处理。
*
* @param price 金额
* @return 格式化后的金额
*/
public static BigDecimal priceScale(BigDecimal price) {
return (price == null ? BigDecimal.ZERO : price).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
}
/**
* 金额相加,默认进行四舍五入
*
* 位数:{@link #PRICE_SCALE}。忽略空金额,没有有效金额时返回 0。
*
* @param prices 金额数组
* @return 金额相加结果
*/
public static BigDecimal priceAdd(BigDecimal... prices) {
BigDecimal result = BigDecimal.ZERO;
if (prices == null) {
return priceScale(result);
}
for (BigDecimal price : prices) {
if (price != null) {
result = result.add(price);
}
}
return priceScale(result);
}
/**
* 金额相乘,默认进行四舍五入
*
@@ -109,7 +142,7 @@ public class MoneyUtils {
if (price == null || count == null) {
return null;
}
return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
return priceScale(price.multiply(count));
}
/**
@@ -2,10 +2,12 @@ package cn.iocoder.yudao.framework.common.util.number;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import java.math.BigDecimal;
import java.util.List;
import java.util.regex.Pattern;
/**
* 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
@@ -14,6 +16,13 @@ import java.util.List;
*/
public class NumberUtils {
private static final Pattern FIRST_BIG_DECIMAL_PATTERN = Pattern.compile("-?\\d+(?:\\.\\d+)?");
/**
* 一百,用于百分数和比例计算
*/
public static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
public static Long parseLong(String str) {
return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
}
@@ -22,6 +31,30 @@ public class NumberUtils {
return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null;
}
/**
* 获得数字,为 null 时返回 {@link BigDecimal#ZERO}
*
* @param value 数字
* @return 数字
*/
public static BigDecimal zeroIfNull(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
/**
* 从文本中解析首个数字
*
* @param text 文本
* @return 数字;不存在时返回 {@link BigDecimal#ZERO}
*/
public static BigDecimal parseFirstBigDecimal(String text) {
if (StrUtil.isEmpty(text)) {
return BigDecimal.ZERO;
}
String number = ReUtil.getGroup0(FIRST_BIG_DECIMAL_PATTERN, text.replace(",", ""));
return NumberUtil.toBigDecimal(number);
}
public static boolean isAllNumber(List<String> values) {
if (CollUtil.isEmpty(values)) {
return false;
@@ -5,10 +5,13 @@ import cn.hutool.core.lang.func.Func1;
import cn.hutool.core.lang.func.LambdaUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField;
import org.springframework.util.Assert;
import java.util.List;
import static java.util.Collections.singletonList;
/**
@@ -24,6 +27,27 @@ public class PageUtils {
return (pageParam.getPageNo() - 1) * pageParam.getPageSize();
}
/**
* 对内存列表进行分页
*
* @param list 数据列表
* @param pageParam 分页参数
* @param <T> 数据类型
* @return 分页结果
*/
public static <T> PageResult<T> buildPageResult(List<T> list, PageParam pageParam) {
if (CollUtil.isEmpty(list)) {
return PageResult.empty();
}
long total = list.size();
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
return new PageResult<>(list, total);
}
int fromIndex = Math.toIntExact(Math.min(getStart(pageParam), total));
int toIndex = Math.toIntExact(Math.min((long) fromIndex + pageParam.getPageSize(), total));
return new PageResult<>(list.subList(fromIndex, toIndex), total);
}
/**
* 构建排序字段(默认倒序)
*
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.common.util.servlet;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
@@ -11,6 +12,7 @@ import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.IOException;
import java.util.Map;
/**
@@ -102,4 +104,11 @@ public class ServletUtils {
return JakartaServletUtil.getHeaderMap(request);
}
public static void writeAttachment(HttpServletResponse response, String filename, byte[] content)
throws IOException {
response.getOutputStream().write(content);
response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
}
}
@@ -0,0 +1,342 @@
package cn.iocoder.yudao.framework.common.util.date;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.TimeRange;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link LocalDateTimeUtils} 的单元测试
*
* @author 芋道源码
*/
public class LocalDateTimeUtilsTest {
@Test
public void testDateCompare() {
// 准备参数
LocalDate firstDate = LocalDate.of(2026, 7, 23);
LocalDate secondDate = LocalDate.of(2026, 7, 24);
// 调用,并断言
assertTrue(LocalDateTimeUtils.isBeforeOrEqual(firstDate, secondDate));
assertTrue(LocalDateTimeUtils.isBeforeOrEqual(secondDate, secondDate));
assertFalse(LocalDateTimeUtils.isBeforeOrEqual(secondDate, firstDate));
assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondDate, firstDate));
assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondDate, secondDate));
assertFalse(LocalDateTimeUtils.isAfterOrEqual(firstDate, secondDate));
}
@Test
public void testDateTimeCompare() {
// 准备参数
LocalDateTime firstTime = LocalDateTime.of(2026, 7, 24, 9, 0);
LocalDateTime secondTime = LocalDateTime.of(2026, 7, 24, 18, 0);
// 调用,并断言
assertTrue(LocalDateTimeUtils.isBeforeOrEqual(firstTime, secondTime));
assertTrue(LocalDateTimeUtils.isBeforeOrEqual(secondTime, secondTime));
assertFalse(LocalDateTimeUtils.isBeforeOrEqual(secondTime, firstTime));
assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondTime, firstTime));
assertTrue(LocalDateTimeUtils.isAfterOrEqual(secondTime, secondTime));
assertFalse(LocalDateTimeUtils.isAfterOrEqual(firstTime, secondTime));
}
@Test
public void testIsTimeRangeValid() {
// 准备参数
LocalDateTime beginTime = LocalDateTime.of(2026, 7, 24, 9, 0);
LocalDateTime endTime = LocalDateTime.of(2026, 7, 24, 18, 0);
// 调用,并断言
assertTrue(LocalDateTimeUtils.isTimeRangeValid(beginTime, endTime));
assertFalse(LocalDateTimeUtils.isTimeRangeValid(beginTime, beginTime));
assertFalse(LocalDateTimeUtils.isTimeRangeValid(endTime, beginTime));
assertFalse(LocalDateTimeUtils.isTimeRangeValid(null, endTime));
assertFalse(LocalDateTimeUtils.isTimeRangeValid(beginTime, null));
}
@Test
public void testIsTimeRangePresent() {
// 准备参数
LocalDateTime time = LocalDateTime.of(2026, 7, 24, 9, 0);
// 调用,并断言:空数组和双空边界都不是有效范围
assertFalse(LocalDateTimeUtils.isTimeRangePresent(null));
assertFalse(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[0]));
assertFalse(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{null, null}));
// 调用,并断言:单边或双边存在即为有效范围
assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{time}));
assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{null, time}));
assertTrue(LocalDateTimeUtils.isTimeRangePresent(new LocalDateTime[]{time, time.plusHours(1)}));
}
@Test
public void testGetDayTime() {
// 准备参数
LocalDate date = LocalDate.of(2026, 7, 24);
LocalDateTime time = date.atTime(15, 30);
// 调用,并断言
assertEquals(LocalDateTime.of(2026, 7, 24, 0, 0),
LocalDateTimeUtils.getDayBeginTime(date));
assertEquals(LocalDateTime.of(2026, 7, 24, 0, 0),
LocalDateTimeUtils.getDayBeginTime(time));
assertNull(LocalDateTimeUtils.getDayBeginTime((LocalDateTime) null));
assertEquals(LocalDateTime.of(date, LocalTime.MAX),
LocalDateTimeUtils.getDayEndTime(date));
}
@Test
public void testGetDateTimeRange() {
// 准备参数
LocalDate beginDate = LocalDate.of(2026, 7, 1);
LocalDate endDate = LocalDate.of(2026, 7, 31);
// 调用,并断言:左右两端均为闭区间边界
LocalDateTime[] result = LocalDateTimeUtils.getDateTimeRange(beginDate, endDate);
assertEquals(beginDate.atStartOfDay(), result[0]);
assertEquals(endDate.atTime(LocalTime.MAX), result[1]);
// 调用,并断言:支持单边范围
result = LocalDateTimeUtils.getDateTimeRange(beginDate, null);
assertEquals(beginDate.atStartOfDay(), result[0]);
assertNull(result[1]);
result = LocalDateTimeUtils.getDateTimeRange(null, endDate);
assertNull(result[0]);
assertEquals(endDate.atTime(LocalTime.MAX), result[1]);
// 调用,并断言:两个边界都为空时不生成范围
assertNull(LocalDateTimeUtils.getDateTimeRange(null, null));
}
@Test
public void testGetMonthDateTimeRange() {
// 调用:闰年二月
LocalDateTime[] result = LocalDateTimeUtils.getMonthDateTimeRange(2024, 2);
// 断言:完整包含闰年二月的首尾时刻
assertEquals(LocalDateTime.of(2024, 2, 1, 0, 0), result[0]);
assertEquals(LocalDate.of(2024, 2, 29).atTime(LocalTime.MAX), result[1]);
}
@Test
public void testGetMonthBeginTime() {
// 准备参数
YearMonth month = YearMonth.of(2026, 12);
// 调用,并断言
assertEquals(LocalDateTime.of(2026, 12, 1, 0, 0),
LocalDateTimeUtils.getMonthBeginTime(month));
assertEquals(LocalDateTime.of(2027, 1, 1, 0, 0),
LocalDateTimeUtils.getNextMonthBeginTime(month));
}
@Test
public void testIsBetween() {
// 准备参数
LocalDate beginDate = LocalDate.of(2026, 7, 23);
LocalDate endDate = LocalDate.of(2026, 7, 25);
LocalDateTime beginTime = beginDate.atTime(9, 0);
LocalDateTime endTime = beginDate.atTime(18, 0);
// 调用,并断言:日期闭区间
assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, beginDate));
assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, LocalDate.of(2026, 7, 24)));
assertTrue(LocalDateTimeUtils.isBetween(beginDate, endDate, endDate));
assertFalse(LocalDateTimeUtils.isBetween(beginDate, endDate, LocalDate.of(2026, 7, 26)));
assertFalse(LocalDateTimeUtils.isBetween(beginDate, endDate, null));
assertFalse(LocalDateTimeUtils.isNotBetween(beginDate, endDate, beginDate));
assertTrue(LocalDateTimeUtils.isNotBetween(beginDate, endDate, LocalDate.of(2026, 7, 26)));
assertTrue(LocalDateTimeUtils.isNotBetween(beginDate, endDate, null));
// 调用,并断言:时间闭区间
assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, beginTime));
assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, beginDate.atTime(12, 0)));
assertTrue(LocalDateTimeUtils.isBetween(beginTime, endTime, endTime));
assertFalse(LocalDateTimeUtils.isBetween(beginTime, endTime, beginDate.atTime(18, 1)));
assertFalse(LocalDateTimeUtils.isBetween(beginTime, endTime, (LocalDateTime) null));
assertFalse(LocalDateTimeUtils.isNotBetween(beginTime, endTime, endTime));
assertTrue(LocalDateTimeUtils.isNotBetween(beginTime, endTime, beginDate.atTime(18, 1)));
assertTrue(LocalDateTimeUtils.isNotBetween(beginTime, endTime, null));
}
@Test
public void testIsClosedRangeOverlap() {
// 准备参数
LocalDateTime firstBeginTime = LocalDateTime.of(2026, 7, 24, 9, 0);
LocalDateTime firstEndTime = LocalDateTime.of(2026, 7, 24, 12, 0);
// 调用,并断言:首尾相接属于闭区间重叠
assertTrue(LocalDateTimeUtils.isClosedRangeOverlap(
firstBeginTime, firstEndTime, firstEndTime, firstEndTime.plusHours(1)));
assertTrue(LocalDateTimeUtils.isClosedRangeOverlap(
firstBeginTime, firstEndTime, firstBeginTime.minusHours(1), firstBeginTime));
assertFalse(LocalDateTimeUtils.isClosedRangeOverlap(
firstBeginTime, firstEndTime, firstEndTime.plusNanos(1), firstEndTime.plusHours(1)));
assertFalse(LocalDateTimeUtils.isClosedRangeOverlap(
firstBeginTime, firstEndTime, null, firstEndTime));
}
@Test
public void testSubtractTimeRanges() {
// 准备参数
LocalDate date = LocalDate.of(2026, 7, 24);
TimeRange sourceRange = new TimeRange(date.atTime(9, 0), date.atTime(18, 0));
// 调用:扣除原时段中间的一段
List<TimeRange> result = LocalDateTimeUtils.subtractTimeRanges(
Collections.singletonList(sourceRange),
new TimeRange(date.atTime(12, 0), date.atTime(13, 0)));
// 断言:原时段被拆分为两段
assertEquals(2, result.size());
assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(12, 0));
assertTimeRange(result.get(1), date.atTime(13, 0), date.atTime(18, 0));
// 调用,并断言:扣除时段完整覆盖原时段
result = LocalDateTimeUtils.subtractTimeRanges(
Collections.singletonList(sourceRange),
new TimeRange(date.atTime(8, 0), date.atTime(19, 0)));
assertTrue(result.isEmpty());
// 调用:扣除时段与原时段首尾相接
result = LocalDateTimeUtils.subtractTimeRanges(
Collections.singletonList(sourceRange),
new TimeRange(date.atTime(18, 0), date.atTime(19, 0)));
// 断言:两个时段仅首尾相接,没有正时长交集,原时段保持不变
assertEquals(1, result.size());
assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(18, 0));
}
@Test
public void testMergeTimeRanges() {
// 准备参数
LocalDate date = LocalDate.of(2026, 7, 24);
List<TimeRange> timeRanges = Arrays.asList(
new TimeRange(date.atTime(13, 0), date.atTime(14, 0)),
new TimeRange(date.atTime(9, 0), date.atTime(11, 0)),
new TimeRange(date.atTime(10, 0), date.atTime(13, 0)),
new TimeRange(date.atTime(16, 0), date.atTime(17, 0)));
// 调用
List<TimeRange> result = LocalDateTimeUtils.mergeTimeRanges(timeRanges);
// 断言:重叠或首尾相接的时段被合并,独立时段保持不变
assertEquals(2, result.size());
assertTimeRange(CollUtil.getFirst(result), date.atTime(9, 0), date.atTime(14, 0));
assertTimeRange(result.get(1), date.atTime(16, 0), date.atTime(17, 0));
// 调用,并断言:空列表
assertTrue(LocalDateTimeUtils.mergeTimeRanges(Collections.emptyList()).isEmpty());
}
@Test
public void testIntersectTimeRanges() {
// 准备参数
LocalDate date = LocalDate.of(2026, 7, 24);
List<TimeRange> firstRanges = Arrays.asList(
new TimeRange(date.atTime(9, 0), date.atTime(12, 0)),
new TimeRange(date.atTime(13, 0), date.atTime(18, 0)));
List<TimeRange> secondRanges = Arrays.asList(
new TimeRange(date.atTime(10, 0), date.atTime(14, 0)),
new TimeRange(date.atTime(17, 0), date.atTime(19, 0)));
// 调用
List<TimeRange> result = LocalDateTimeUtils.intersectTimeRanges(firstRanges, secondRanges);
// 断言
assertEquals(3, result.size());
assertTimeRange(CollUtil.getFirst(result), date.atTime(10, 0), date.atTime(12, 0));
assertTimeRange(result.get(1), date.atTime(13, 0), date.atTime(14, 0));
assertTimeRange(result.get(2), date.atTime(17, 0), date.atTime(18, 0));
// 调用,并断言:两个时段仅首尾相接时不生成零时长交集
assertNull(LocalDateTimeUtils.intersectTimeRange(
new TimeRange(date.atTime(9, 0), date.atTime(10, 0)),
new TimeRange(date.atTime(10, 0), date.atTime(11, 0))));
}
@Test
public void testCalculateDurationMinutes() {
// 准备参数
LocalDate date = LocalDate.of(2026, 7, 24);
List<TimeRange> timeRanges = Arrays.asList(
new TimeRange(date.atTime(9, 0), date.atTime(10, 0)),
new TimeRange(date.atTime(13, 0), date.atTime(14, 30)));
// 调用,并断言
assertEquals(150, LocalDateTimeUtils.calculateDurationMinutes(timeRanges));
// 调用,并断言:空列表
assertEquals(0, LocalDateTimeUtils.calculateDurationMinutes(Collections.emptyList()));
}
@Test
public void testBuildDailyTimeRanges() {
// 准备参数
LocalDate beginDate = LocalDate.of(2026, 7, 24);
LocalDate endDate = LocalDate.of(2026, 7, 25);
// 调用:构建跨天时间范围
List<TimeRange> result = LocalDateTimeUtils.buildDailyTimeRanges(
beginDate, endDate, LocalTime.of(22, 0), LocalTime.of(6, 0));
// 断言
assertEquals(2, result.size());
assertTimeRange(CollUtil.getFirst(result), beginDate.atTime(22, 0), beginDate.plusDays(1).atTime(6, 0));
assertTimeRange(result.get(1), endDate.atTime(22, 0), endDate.plusDays(1).atTime(6, 0));
// 调用:每日开始和结束时刻相同
result = LocalDateTimeUtils.buildDailyTimeRanges(
beginDate, beginDate, LocalTime.of(9, 0), LocalTime.of(9, 0));
// 断言:按完整一天处理
assertEquals(1, result.size());
assertTimeRange(CollUtil.getFirst(result), beginDate.atTime(9, 0), beginDate.plusDays(1).atTime(9, 0));
// 调用,并断言:开始日期晚于结束日期
assertTrue(LocalDateTimeUtils.buildDailyTimeRanges(
endDate, beginDate, LocalTime.of(9, 0), LocalTime.of(18, 0)).isEmpty());
}
@Test
public void testFindDailyTimeRange() {
// 准备参数
LocalDate date = LocalDate.of(2026, 7, 8);
// 调用,并断言普通时间范围的左右边界
TimeRange result = LocalDateTimeUtils.findDailyTimeRange(
date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(8, 0));
assertEquals(date.atTime(8, 0), result.getStartTime());
assertEquals(date.atTime(10, 0), result.getEndTime());
assertNotNull(LocalDateTimeUtils.findDailyTimeRange(
date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(10, 0)));
// 调用,并断言跨日时间范围
result = LocalDateTimeUtils.findDailyTimeRange(
date, LocalTime.of(20, 0), LocalTime.of(6, 0), date.plusDays(1).atTime(5, 0));
assertEquals(date.atTime(20, 0), result.getStartTime());
assertEquals(date.plusDays(1).atTime(6, 0), result.getEndTime());
assertNull(LocalDateTimeUtils.findDailyTimeRange(
date, LocalTime.of(8, 0), LocalTime.of(10, 0), date.atTime(12, 0)));
}
private static void assertTimeRange(TimeRange timeRange,
LocalDateTime expectedStartTime,
LocalDateTime expectedEndTime) {
assertEquals(expectedStartTime, timeRange.getStartTime());
assertEquals(expectedEndTime, timeRange.getEndTime());
}
}
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.framework.excel.core.convert;
import cn.hutool.core.collection.CollUtil;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import java.util.List;
/**
* Excel 字符串列表转换器
*
* @author 芋道源码
*/
public class StringListConvert implements Converter<List<String>> {
@Override
public WriteCellData<String> convertToExcelData(List<String> value,
ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
return new WriteCellData<>(CollUtil.isEmpty(value) ? "" : String.join("/", value));
}
}
@@ -1,7 +1,9 @@
package cn.iocoder.yudao.framework.excel.core.util;
import cn.idev.excel.FastExcelFactory;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.converters.longconverter.LongStringConverter;
import cn.idev.excel.read.listener.ReadListener;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
@@ -10,7 +12,10 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Excel 工具类
@@ -44,7 +49,33 @@ public class ExcelUtils {
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
}
/**
* 将动态表头和数据以 Excel 响应给前端
*
* @param response 响应
* @param filename 文件名
* @param sheetName Excel sheet 名
* @param head Excel head 头
* @param data 数据列表
* @throws IOException 写入失败的情况
*/
public static void write(HttpServletResponse response, String filename, String sheetName,
List<List<String>> head, List<List<Object>> data) throws IOException {
// 输出 Excel
FastExcelFactory.write(response.getOutputStream())
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
.registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
.registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
.head(head).sheet(sheetName).doWrite(data);
// 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
}
public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
if (file == null || file.isEmpty()) {
return Collections.emptyList();
}
// 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景
try (InputStream inputStream = file.getInputStream()) {
return FastExcelFactory.read(inputStream, head, null)
@@ -53,4 +84,64 @@ public class ExcelUtils {
}
}
/**
* 读取指定数量以内的 Excel 数据,达到上限后停止继续解析。
*
* @param file Excel 文件,可为空
* @param head Excel 表头类型
* @param maxRowCount 最大读取行数
* @return Excel 数据
*/
public static <T> List<T> read(MultipartFile file, Class<T> head, int maxRowCount) throws IOException {
if (file == null || file.isEmpty()) {
return Collections.emptyList();
}
if (maxRowCount <= 0) {
throw new IllegalArgumentException("maxRowCount must be greater than 0");
}
List<T> rows = new ArrayList<>(Math.min(maxRowCount, 1024));
try (InputStream inputStream = file.getInputStream()) {
FastExcelFactory.read(inputStream, head, new ReadListener<T>() {
@Override
public void invoke(T data, AnalysisContext context) {
rows.add(data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 无需处理
}
@Override
public boolean hasNext(AnalysisContext context) {
return rows.size() < maxRowCount;
}
}).autoCloseStream(false).doReadAll();
}
return rows;
}
/**
* 读取动态表头 Excel 数据
*
* @param file Excel 文件,可为空
* @return 按列下标存储的行数据
* @throws IOException 读取失败
*/
public static List<Map<Integer, String>> read(MultipartFile file) throws IOException {
if (file == null || file.isEmpty()) {
return Collections.emptyList();
}
// 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景
try (InputStream inputStream = file.getInputStream()) {
return FastExcelFactory.read(inputStream)
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
.headRowNumber(1)
.sheet()
.doReadSync();
}
}
}
@@ -0,0 +1,226 @@
package cn.iocoder.yudao.framework.excel.core.util;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import lombok.experimental.UtilityClass;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.Comment;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import java.math.BigDecimal;
import java.util.Collection;
/**
* 原生 POI 的 Excel 工具类
*
* 全局 {@link ExcelUtils}(FastExcel)无法表达单元格锁定、公式、动态列等复杂模板时,各模块基于本类实现自定义 Excel 读写
*
* @author 芋道源码
*/
@UtilityClass
public class PoiExcelUtils {
/**
* 读取公式计算值必须 DataFormatter + FormulaEvaluator,hutool 没有更简的等价写法
*/
private static final DataFormatter DATA_FORMATTER = new DataFormatter();
// ==================== 读取 ====================
/**
* 读取单元格文本,公式单元格取公式计算值
*
* @param row 行
* @param column 列
* @param evaluator 公式计算器,不需要求值时传 null
* @return 文本,空单元格返回空字符串
*/
public static String getCellText(Row row, Integer column, FormulaEvaluator evaluator) {
if (row == null || column == null || row.getCell(column) == null) {
return "";
}
return evaluator == null
? DATA_FORMATTER.formatCellValue(row.getCell(column))
: DATA_FORMATTER.formatCellValue(row.getCell(column), evaluator);
}
/**
* 读取单元格数字,兼容千分位文本
*
* @param row 行
* @param column 列
* @param evaluator 公式计算器,不需要求值时传 null
* @return 数字,空单元格返回 null;格式不正确时抛出 {@link NumberFormatException}
*/
public static BigDecimal getDecimal(Row row, Integer column, FormulaEvaluator evaluator) {
String value = getCellText(row, column, evaluator).replace(",", "");
return StrUtil.isBlank(value) ? null : NumberUtil.toBigDecimal(value);
}
/**
* 判断指定列是否全部为空
*
* @param row 行
* @param evaluator 公式计算器,不需要求值时传 null
* @param columns 列数组
* @return 是否全部为空
*/
public static boolean isEmptyRow(Row row, FormulaEvaluator evaluator, Collection<Integer> columns) {
if (row == null) {
return true;
}
for (Integer column : columns) {
if (StrUtil.isNotBlank(getCellText(row, column, evaluator))) {
return false;
}
}
return true;
}
// ==================== 写入 ====================
/**
* 写入文本单元格,null 按空字符串处理
*/
public static void writeText(Row row, int column, String value, CellStyle style) {
Cell cell = row.createCell(column);
cell.setCellValue(StrUtil.nullToEmpty(value));
cell.setCellStyle(style);
}
/**
* 写入数字单元格,null 时留空
*/
public static void writeNumber(Row row, int column, BigDecimal value, CellStyle style) {
Cell cell = row.createCell(column);
if (value != null) {
cell.setCellValue(value.doubleValue());
}
cell.setCellStyle(style);
}
/**
* 写入公式单元格
*/
public static void writeFormula(Row row, int column, String formula, CellStyle style) {
Cell cell = row.createCell(column);
cell.setCellFormula(formula);
cell.setCellStyle(style);
}
/**
* 写入跨两行的合并表头
*/
public static void mergeHeader(Sheet sheet, Row firstRow, Row secondRow,
int beginColumn, int endColumn, String title, CellStyle style) {
sheet.addMergedRegion(new CellRangeAddress(firstRow.getRowNum(), secondRow.getRowNum(),
beginColumn, endColumn));
Cell cell = firstRow.createCell(beginColumn);
cell.setCellValue(title);
cell.setCellStyle(style);
secondRow.createCell(beginColumn).setCellStyle(style);
}
/**
* 添加单元格批注
*
* @param cell 单元格
* @param text 批注内容
*/
public static void addComment(Cell cell, String text) {
CreationHelper helper = cell.getSheet().getWorkbook().getCreationHelper();
ClientAnchor anchor = helper.createClientAnchor();
anchor.setCol1(cell.getColumnIndex());
anchor.setCol2(cell.getColumnIndex() + 3);
anchor.setRow1(cell.getRowIndex());
anchor.setRow2(cell.getRowIndex() + 3);
Comment comment = cell.getSheet().createDrawingPatriarch().createCellComment(anchor);
comment.setString(helper.createRichTextString(text));
cell.setCellComment(comment);
}
// ==================== 样式 ====================
/**
* 创建细边框样式
*/
public static CellStyle createBorderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
return style;
}
/**
* 创建数据单元格样式:细边框、垂直居中,可指定锁定和填充色
*
* @param workbook 工作簿
* @param locked 是否锁定,配合 protectSheet 使用
* @param fillColor 填充色,传 null 时不填充
*/
public static CellStyle createDataStyle(Workbook workbook, boolean locked, IndexedColors fillColor) {
CellStyle style = createBorderStyle(workbook);
style.setLocked(locked);
if (fillColor != null) {
style.setFillForegroundColor(fillColor.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
}
style.setVerticalAlignment(VerticalAlignment.CENTER);
return style;
}
/**
* 基于已有样式创建数字格式样式
*/
public static CellStyle createNumberStyle(Workbook workbook, CellStyle base, String format) {
CellStyle style = workbook.createCellStyle();
style.cloneStyleFrom(base);
style.setDataFormat(workbook.createDataFormat().getFormat(format));
return style;
}
/**
* 创建表头样式:细边框、指定填充色、加粗居中
*/
public static CellStyle createHeaderStyle(Workbook workbook, IndexedColors fillColor) {
CellStyle style = createBorderStyle(workbook);
style.setFillForegroundColor(fillColor.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
return style;
}
/**
* 创建标题样式:大号加粗居中
*/
public static CellStyle createTitleStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
Font font = workbook.createFont();
font.setBold(true);
font.setFontHeightInPoints((short) 20);
style.setFont(font);
return style;
}
}
@@ -0,0 +1,61 @@
package cn.iocoder.yudao.framework.excel.core.util;
import cn.idev.excel.FastExcelFactory;
import cn.idev.excel.annotation.ExcelProperty;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link ExcelUtils} 的单元测试
*
* @author 芋道源码
*/
public class ExcelUtilsTest {
@Test
public void testRead_maxRowCount() throws Exception {
// mock 数据
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
FastExcelFactory.write(outputStream, TestRow.class).sheet().doWrite(Arrays.asList(
new TestRow("员工一"), new TestRow("员工二"), new TestRow("员工三")));
MockMultipartFile file = new MockMultipartFile(
"file", "employees.xlsx", null, outputStream.toByteArray());
// 调用
List<TestRow> rows = ExcelUtils.read(file, TestRow.class, 2);
// 断言
assertEquals(2, rows.size());
assertEquals("员工一", rows.get(0).getName());
assertEquals("员工二", rows.get(1).getName());
}
public static class TestRow {
@ExcelProperty("姓名")
private String name;
public TestRow() {
}
public TestRow(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@@ -5,8 +5,6 @@ package cn.iocoder.yudao.framework.datasource.core.enums;
*
* 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。
* 注意,默认是 {@link #MASTER} 数据源
*
* 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html
*/
public interface DataSourceEnum {
@@ -144,6 +144,18 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
return selectOneForUpdate(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
}
/**
* 获得满足条件的第 1 条记录,并使用 FOR UPDATE 锁定。
*
* 注意:需要在事务中调用,否则锁会立即释放。
*
* @param queryWrapper 查询条件
* @return 实体
*/
default T selectFirstOneForUpdate(LambdaQueryWrapper<T> queryWrapper) {
return selectOne(queryWrapper.last("LIMIT 1 FOR UPDATE"));
}
/**
* 获取满足条件的第 1 条记录
*
@@ -7,7 +7,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.function.Consumer;
/**
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
@@ -107,6 +109,24 @@ public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
return betweenIfPresent(column, val1, val2);
}
/**
* 拼接两个闭区间的重叠条件
*
* <p>记录区间与查询区间重叠的条件为:记录开始值小于等于查询结束值,
* 且记录结束值大于等于查询开始值。不能使用单字段 {@code BETWEEN} 代替,
* 否则会漏掉完整覆盖查询区间的记录。</p>
*
* @param beginColumn 记录开始值字段
* @param endColumn 记录结束值字段
* @param times 查询闭区间
* @return 查询条件
*/
public LambdaQueryWrapperX<T> betweenIfPresent(
SFunction<T, ?> beginColumn, SFunction<T, ?> endColumn, LocalDateTime[] times) {
return leIfPresent(beginColumn, ArrayUtils.get(times, 1))
.geIfPresent(endColumn, ArrayUtils.get(times, 0));
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
@@ -139,4 +159,40 @@ public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
return this;
}
@Override
public LambdaQueryWrapperX<T> exists(boolean condition, String existsSql, Object... values) {
super.exists(condition, existsSql, values);
return this;
}
@Override
public LambdaQueryWrapperX<T> notExists(boolean condition, String existsSql, Object... values) {
super.notExists(condition, existsSql, values);
return this;
}
@Override
public LambdaQueryWrapperX<T> and(boolean condition, Consumer<LambdaQueryWrapper<T>> consumer) {
super.and(condition, consumer);
return this;
}
@Override
public LambdaQueryWrapperX<T> or(boolean condition, Consumer<LambdaQueryWrapper<T>> consumer) {
super.or(condition, consumer);
return this;
}
@Override
public LambdaQueryWrapperX<T> nested(boolean condition, Consumer<LambdaQueryWrapper<T>> consumer) {
super.nested(condition, consumer);
return this;
}
@Override
public LambdaQueryWrapperX<T> not(boolean condition, Consumer<LambdaQueryWrapper<T>> consumer) {
super.not(condition, consumer);
return this;
}
}
@@ -102,16 +102,9 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
}
public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
if (values != null && values.length != 0 && values[0] != null && values[1] != null) {
return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
}
if (values != null && values.length != 0 && values[0] != null) {
return (QueryWrapperX<T>) ge(column, values[0]);
}
if (values != null && values.length != 0 && values[1] != null) {
return (QueryWrapperX<T>) le(column, values[1]);
}
return this;
Object val1 = values != null && values.length > 0 ? values[0] : null;
Object val2 = values != null && values.length > 1 ? values[1] : null;
return betweenIfPresent(column, val1, val2);
}
// ========== 重写父类方法,方便链式调用 ==========
@@ -24,6 +24,8 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* MyBatis 工具类
@@ -179,6 +181,28 @@ public class MyBatisUtils {
return findInSet(columnName, paramIndex);
}
/**
* 跨数据库的 find_in_set 实现,适用于同一字段匹配多个参数的场景
*
* 每个参数生成一个 find_in_set 条件,并使用 OR 连接。
*
* @param columnName 字段名称
* @param values 参数集合
* @return sql
*/
public static String findInSet(String columnName, Collection<?> values) {
return findInSet(JdbcUtils.getDbType(), columnName, values);
}
static String findInSet(DbType dbType, String columnName, Collection<?> values) {
if (CollUtil.isEmpty(values)) {
throw new IllegalArgumentException("Values cannot be empty");
}
return IntStream.range(0, values.size())
.mapToObj(index -> findInSet(dbType, columnName, index))
.collect(Collectors.joining(" OR "));
}
private static String findInSet(String columnName, int paramIndex) {
DbType dbType = JdbcUtils.getDbType();
return findInSet(dbType, columnName, paramIndex);
@@ -0,0 +1,67 @@
package cn.iocoder.yudao.framework.mybatis.core.query;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import lombok.Data;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link LambdaQueryWrapperX} 的单元测试
*
* @author 芋道源码
*/
public class LambdaQueryWrapperXTest {
@BeforeAll
public static void initTableInfo() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TestDO.class);
}
@Test
public void testBetweenIfPresent_overlap() {
// 调用,并断言:空范围不追加条件
assertEquals("", new LambdaQueryWrapperX<TestDO>()
.betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime, null)
.getSqlSegment());
// 调用,并断言:双边范围使用闭区间重叠条件
LocalDateTime beginTime = LocalDateTime.of(2026, 8, 1, 0, 0);
LocalDateTime endTime = LocalDateTime.of(2026, 8, 31, 23, 59);
String sqlSegment = new LambdaQueryWrapperX<TestDO>()
.betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime,
new LocalDateTime[]{beginTime, endTime})
.getSqlSegment();
assertTrue(sqlSegment.contains("start_time <="));
assertTrue(sqlSegment.contains("end_time >="));
// 调用,并断言:单边范围只追加对应的重叠边界
sqlSegment = new LambdaQueryWrapperX<TestDO>()
.betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime,
new LocalDateTime[]{beginTime})
.getSqlSegment();
assertTrue(sqlSegment.contains("end_time >="));
sqlSegment = new LambdaQueryWrapperX<TestDO>()
.betweenIfPresent(TestDO::getStartTime, TestDO::getEndTime,
new LocalDateTime[]{null, endTime})
.getSqlSegment();
assertTrue(sqlSegment.contains("start_time <="));
}
@Data
@TableName("test_data")
private static class TestDO {
private LocalDateTime startTime;
private LocalDateTime endTime;
}
}
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.framework.mybatis.core.query;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link QueryWrapperX} 的单元测试
*
* @author 芋道源码
*/
public class QueryWrapperXTest {
@Test
public void testBetweenIfPresent_array() {
// 调用,并断言:空范围不追加条件
assertEquals("", new QueryWrapperX<>().betweenIfPresent("create_time", (Object[]) null).getSqlSegment());
assertEquals("", new QueryWrapperX<>().betweenIfPresent("create_time", new Object[0]).getSqlSegment());
// 调用,并断言:单边范围安全降级为大于等于或小于等于
assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{1})
.getSqlSegment().contains("create_time >="));
assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{null, 2})
.getSqlSegment().contains("create_time <="));
// 调用,并断言:双边范围使用左右闭合的 BETWEEN
assertTrue(new QueryWrapperX<>().betweenIfPresent("create_time", new Object[]{1, 2})
.getSqlSegment().contains("create_time BETWEEN"));
}
}
@@ -14,6 +14,7 @@ import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -73,6 +74,19 @@ public class YudaoSecurityAutoConfiguration {
return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi);
}
/**
* Token 过滤器由 Spring Security FilterChain 管理,禁止 Spring Boot 再注册为 Servlet 全局 Filter
*
* @see <a href="https://github.com/YunaiV/ruoyi-vue-pro/issues/1198">Issue #1198</a>
*/
@Bean
public FilterRegistrationBean<TokenAuthenticationFilter> tokenAuthenticationFilterRegistration(
TokenAuthenticationFilter filter) {
FilterRegistrationBean<TokenAuthenticationFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
@Bean("ss") // 使用 Spring Security 的缩写,方便使用
public SecurityFrameworkService securityFrameworkService(PermissionCommonApi permissionApi) {
return new SecurityFrameworkServiceImpl(permissionApi);
@@ -17,17 +17,19 @@ import jakarta.validation.constraints.NotNull;
public class WebProperties {
@NotNull(message = "APP API 不能为空")
@Valid
private Api appApi = new Api("/app-api", "**.controller.app.**");
@NotNull(message = "Admin API 不能为空")
@Valid
private Api adminApi = new Api("/admin-api", "**.controller.admin.**");
@NotNull(message = "Admin UI 不能为空")
@Valid
private Ui adminUi;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Valid
public static class Api {
/**
@@ -53,7 +55,6 @@ public class WebProperties {
}
@Data
@Valid
public static class Ui {
/**
@@ -313,6 +313,11 @@ public class GlobalExceptionHandler {
// 忽略日志,避免影响主流程
}
}
// ServiceException 的错误码可能为成功码,直接调用 CommonResult.error 会抛出异常,此处统一按系统异常处理。
// 详见 Issue #1196:https://github.com/YunaiV/ruoyi-vue-pro/issues/1196
if (CommonResult.isSuccess(ex.getCode())) {
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
}
return CommonResult.error(ex.getCode(), ex.getMessage());
}
+67
View File
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao</artifactId>
<version>${revision}</version>
</parent>
<artifactId>yudao-module-fms</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
fms 包下,财务管理(Financial Management System)
例如说:凭证、账簿、报表、结账等等
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-system</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-infra</artifactId>
<version>${revision}</version>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-excel</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<!-- 现金流量表公式计算 -->
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO;
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 结账期间")
@RestController
@RequestMapping("/fms/closing/period")
@Validated
public class FmsClosingPeriodController {
@Resource
private FmsClosingPeriodService closingPeriodService;
@GetMapping("/current-month")
@Operation(summary = "获得当前会计期间")
public CommonResult<String> getCurrentMonth(@RequestParam("accountSetId") @NotNull Long accountSetId) {
return success(closingPeriodService.getCurrentMonth(accountSetId, getLoginUserId()));
}
@GetMapping("/overview")
@Operation(summary = "获得结账概况")
@PreAuthorize("@ss.hasPermission('fms:closing:query')")
public CommonResult<FmsClosingOverviewRespVO> getClosingOverview(@Valid FmsClosingQueryReqVO queryReqVO) {
return success(closingPeriodService.getClosingOverview(queryReqVO, getLoginUserId()));
}
@PutMapping("/close")
@Operation(summary = "结账")
@PreAuthorize("@ss.hasPermission('fms:closing:close')")
public CommonResult<Boolean> closePeriod(@Valid @RequestBody FmsClosingQueryReqVO queryReqVO) {
closingPeriodService.closePeriod(queryReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/cancel")
@Operation(summary = "反结账")
@PreAuthorize("@ss.hasPermission('fms:closing:cancel')")
public CommonResult<Boolean> cancelClosePeriod(@Valid FmsClosingQueryReqVO queryReqVO) {
closingPeriodService.cancelClosePeriod(queryReqVO, getLoginUserId());
return success(true);
}
}
@@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeSaveReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsProfitLossSettingsSaveReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsSpecialClosingSettingsSaveReqVO;
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 结账方案")
@RestController
@RequestMapping("/fms/closing/scheme")
@Validated
public class FmsClosingSchemeController {
@Resource
private FmsClosingSchemeService closingSchemeService;
@GetMapping("/list")
@Operation(summary = "获得结账方案列表")
@PreAuthorize("@ss.hasPermission('fms:closing:query')")
public CommonResult<List<FmsClosingSchemeRespVO>> getClosingSchemeList(@Valid FmsClosingQueryReqVO queryReqVO) {
return success(closingSchemeService.getClosingSchemeList(queryReqVO, getLoginUserId()));
}
@PostMapping("/create")
@Operation(summary = "创建结账方案")
@PreAuthorize("@ss.hasPermission('fms:closing:update')")
public CommonResult<Long> createClosingScheme(@Valid @RequestBody FmsClosingSchemeSaveReqVO createReqVO) {
return success(closingSchemeService.createClosingScheme(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "修改结账方案")
@PreAuthorize("@ss.hasPermission('fms:closing:update')")
public CommonResult<Boolean> updateClosingScheme(@Valid @RequestBody FmsClosingSchemeSaveReqVO updateReqVO) {
closingSchemeService.updateClosingScheme(updateReqVO, getLoginUserId());
return success(true);
}
@PutMapping("/update-profit-loss-settings")
@Operation(summary = "保存结转损益设置")
@PreAuthorize("@ss.hasPermission('fms:closing:update')")
public CommonResult<Long> saveProfitLossSettings(@Valid @RequestBody FmsProfitLossSettingsSaveReqVO saveReqVO) {
return success(closingSchemeService.saveProfitLossSettings(saveReqVO, getLoginUserId()));
}
@PutMapping("/update-special-settings")
@Operation(summary = "保存专用结转设置")
@PreAuthorize("@ss.hasPermission('fms:closing:update')")
public CommonResult<Boolean> updateSpecialClosingSettings(@Valid @RequestBody FmsSpecialClosingSettingsSaveReqVO updateReqVO) {
closingSchemeService.updateSpecialClosingSettings(updateReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除结账方案")
@PreAuthorize("@ss.hasPermission('fms:closing:update')")
public CommonResult<Boolean> deleteClosingScheme(@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
closingSchemeService.deleteClosingScheme(accountSetId, id, getLoginUserId());
return success(true);
}
}
@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingTemplateRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingTemplateSaveReqVO;
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 结账模板")
@RestController
@RequestMapping("/fms/closing/template")
@Validated
public class FmsClosingTemplateController {
@Resource
private FmsClosingTemplateService closingTemplateService;
@GetMapping("/list")
@Operation(summary = "获得结账模板列表")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:closing:query')")
public CommonResult<List<FmsClosingTemplateRespVO>> getClosingTemplateList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
return success(closingTemplateService.getClosingTemplateList(accountSetId, getLoginUserId()));
}
@PostMapping("/create")
@Operation(summary = "创建结账模板")
@PreAuthorize("@ss.hasPermission('fms:closing:update')")
public CommonResult<Long> createClosingTemplate(
@Valid @RequestBody FmsClosingTemplateSaveReqVO createReqVO) {
return success(closingTemplateService.createClosingTemplate(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "更新结账模板")
@PreAuthorize("@ss.hasPermission('fms:closing:update')")
public CommonResult<Boolean> updateClosingTemplate(
@Valid @RequestBody FmsClosingTemplateSaveReqVO updateReqVO) {
closingTemplateService.updateClosingTemplate(updateReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除结账模板")
@Parameters({
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"),
@Parameter(name = "id", description = "结账模板编号", required = true, example = "1024")
})
@PreAuthorize("@ss.hasPermission('fms:closing:update')")
public CommonResult<Boolean> deleteClosingTemplate(@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
closingTemplateService.deleteClosingTemplate(accountSetId, id, getLoginUserId());
return success(true);
}
}
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingSchemeGenerateReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingVoucherGenerateReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsProfitLossGenerateReqVO;
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 结转凭证")
@RestController
@RequestMapping("/fms/closing/voucher")
@Validated
public class FmsClosingVoucherController {
@Resource
private FmsClosingVoucherService closingVoucherService;
@PostMapping("/generate-profit-loss")
@Operation(summary = "生成结转损益凭证")
@PreAuthorize("@ss.hasPermission('fms:closing:profit-loss')")
public CommonResult<Long> generateProfitLossVoucher(@Valid @RequestBody FmsProfitLossGenerateReqVO generateReqVO) {
return success(closingVoucherService.generateProfitLossVoucher(generateReqVO, getLoginUserId()));
}
@PostMapping("/generate-scheme")
@Operation(summary = "生成结账方案凭证")
@PreAuthorize("@ss.hasPermission('fms:closing:profit-loss')")
public CommonResult<Long> generateClosingSchemeVoucher(@Valid @RequestBody FmsClosingSchemeGenerateReqVO generateReqVO) {
return success(closingVoucherService.generateClosingSchemeVoucher(generateReqVO, getLoginUserId()));
}
@PostMapping("/generate-list")
@Operation(summary = "批量生成结转凭证")
@PreAuthorize("@ss.hasPermission('fms:closing:profit-loss')")
public CommonResult<List<Long>> generateClosingVoucherList(@Valid @RequestBody FmsClosingVoucherGenerateReqVO generateReqVO) {
return success(closingVoucherService.generateClosingVoucherList(generateReqVO, getLoginUserId()));
}
}
@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - FMS 结账概况 Response VO")
@Data
public class FmsClosingOverviewRespVO {
@Schema(description = "会计期间", example = "2026-08")
private String month;
@Schema(description = "是否已结账")
private Boolean closed;
@Schema(description = "是否要求凭证审核")
private Boolean voucherReviewRequired;
@Schema(description = "未审核凭证数量", example = "2")
private Long pendingVoucherCount;
@Schema(description = "凭证数量", example = "12")
private Long voucherCount;
@Schema(description = "损益类科目余额", example = "9100.00")
private BigDecimal profitLossBalance;
@Schema(description = "资产负债表差额", example = "0.00")
private BigDecimal balanceSheetDifference;
@Schema(description = "结转损益凭证编号", example = "1024")
private Long profitLossVoucherId;
@Schema(description = "初始余额是否试算平衡")
private Boolean initialBalanceBalanced;
@Schema(description = "凭证编号是否连续")
private Boolean voucherNumberContinuous;
@Schema(description = "需要结转损益时是否已生成凭证")
private Boolean profitLossVoucherGenerated;
@Schema(description = "利润表勾稽是否平衡")
private Boolean incomeStatementBalanced;
@Schema(description = "利润表未纳入公式的科目数量", example = "0")
private Integer incomeStatementUnmappedSubjectCount;
@Schema(description = "资产负债表损益是否已结转")
private Boolean balanceSheetProfitLossTransferred;
@Schema(description = "资产负债表是否平衡")
private Boolean balanceSheetBalanced;
@Schema(description = "资产负债表未纳入公式的科目数量", example = "0")
private Integer balanceSheetUnmappedSubjectCount;
@Schema(description = "是否满足全部结账条件")
private Boolean canClose;
}
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
@Schema(description = "管理后台 - FMS 结账查询 Request VO")
@Data
public class FmsClosingQueryReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套不能为空")
private Long accountSetId;
@Schema(description = "目标会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08")
@NotBlank(message = "会计期间不能为空")
@Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "会计期间格式不正确")
private String month;
}
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
@Schema(description = "管理后台 - FMS 结账方案生成凭证 Request VO")
@Data
public class FmsClosingSchemeGenerateReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "账套不能为空")
private Long accountSetId;
@Schema(description = "方案编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "方案编号不能为空")
private Long id;
@Schema(description = "会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08")
@NotBlank(message = "会计期间不能为空")
@Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "会计期间格式不正确")
private String month;
}
@@ -0,0 +1,90 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - FMS 结账方案 Response VO")
@Data
public class FmsClosingSchemeRespVO {
@Schema(description = "方案编号", example = "1024")
private Long id;
@Schema(description = "账套编号", example = "1")
private Long accountSetId;
@Schema(description = "方案名称", example = "结转本月房租")
private String name;
@Schema(description = "是否期末结转")
private Boolean periodEnd;
@Schema(description = "来源科目编号", example = "1024")
private Long subjectId;
@Schema(description = "取数规则", example = "0")
private Integer formulaRule;
@Schema(description = "取数时间类型", example = "1")
private Integer timeType;
@Schema(description = "凭证字编号", example = "11")
private Long voucherWordId;
@Schema(description = "凭证摘要", example = "结转损益")
private String digest;
@Schema(description = "结转凭证类型", example = "2")
private Integer voucherType;
@Schema(description = "以前年度损益调整科目编号", example = "1024")
private Long priorYearAdjustmentSubjectId;
@Schema(description = "以前年度损益调整结转科目编号", example = "1025")
private Long adjustmentClosingSubjectId;
@Schema(description = "其他损益结转科目编号", example = "1026")
private Long otherClosingSubjectId;
@Schema(description = "是否按余额反向结转")
private Boolean reverseBalance;
@Schema(description = "结转日期", example = "31")
private Integer closingDay;
@Schema(description = "类型", example = "1")
private Integer type;
@Schema(description = "结转科目规则数组")
private List<SubjectRule> subjects;
@Schema(description = "待结转金额", example = "12000.00")
private BigDecimal balance;
@Schema(description = "当前期间已生成凭证编号数组")
private List<Long> voucherIds;
@Schema(description = "管理后台 - FMS 结账方案科目规则")
@Data
public static class SubjectRule {
@Schema(description = "科目编号", example = "1024")
private Long subjectId;
@Schema(description = "科目编码快照", example = "6602")
private String subjectCode;
@Schema(description = "摘要", example = "结转本月房租")
private String digest;
@Schema(description = "借贷方向", example = "1")
private Integer direction;
@Schema(description = "金额比例", example = "100")
private BigDecimal amountRatio;
}
}
@@ -0,0 +1,89 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum;
import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTimeTypeEnum;
import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - FMS 结账方案保存 Request VO")
@Data
public class FmsClosingSchemeSaveReqVO {
@Schema(description = "方案编号", example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "账套不能为空")
private Long accountSetId;
@Schema(description = "方案名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "结转本月房租")
@NotBlank(message = "方案名称不能为空")
@Size(max = 255, message = "方案名称长度不能超过 255 个字符")
private String name;
@Schema(description = "是否期末结转", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "是否期末结转不能为空")
private Boolean periodEnd;
@Schema(description = "来源科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "来源科目不能为空")
private Long subjectId;
@Schema(description = "取数规则", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "取数规则不能为空")
@InEnum(FmsFormulaRuleEnum.class)
private Integer formulaRule;
@Schema(description = "取数时间类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "取数时间类型不能为空")
@InEnum(FmsClosingTimeTypeEnum.class)
private Integer timeType;
@Schema(description = "凭证字编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
@NotNull(message = "凭证字不能为空")
private Long voucherWordId;
@Schema(description = "结转科目规则数组", requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
@NotEmpty(message = "结转科目规则不能为空")
@Size(min = 2, message = "结转科目规则至少需要两条")
private List<SubjectRule> subjects;
@Schema(description = "管理后台 - FMS 结账方案科目规则")
@Data
public static class SubjectRule {
@Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "科目不能为空")
private Long subjectId;
@Schema(description = "摘要", requiredMode = Schema.RequiredMode.REQUIRED, example = "结转本月房租")
@NotBlank(message = "摘要不能为空")
@Size(max = 500, message = "摘要长度不能超过 500 个字符")
private String digest;
@Schema(description = "借贷方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "借贷方向不能为空")
@InEnum(FmsDebitCreditDirectionEnum.class)
private Integer direction;
@Schema(description = "金额比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "金额比例不能为空")
@DecimalMin(value = "0", inclusive = false, message = "金额比例必须大于 0")
@DecimalMax(value = "100", message = "金额比例不能超过 100")
private BigDecimal amountRatio;
}
}
@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - FMS 结账模板 Response VO")
@Data
public class FmsClosingTemplateRespVO {
@Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long accountSetId;
@Schema(description = "系统预置编码", example = "daily-travel-reimbursement")
private String presetCode;
@Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "报销差旅费")
private String name;
@Schema(description = "模板分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer category;
@Schema(description = "是否期末结转", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean periodEnd;
@Schema(description = "来源科目编号", example = "1024")
private Long subjectId;
@Schema(description = "取数规则", example = "0")
private Integer formulaRule;
@Schema(description = "取数时间类型", example = "1")
private Integer timeType;
@Schema(description = "结转科目规则数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<SubjectRule> subjects;
@Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer sort;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "管理后台 - FMS 结账模板科目规则")
@Data
public static class SubjectRule {
@Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long subjectId;
@Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "560107")
private String subjectCode;
@Schema(description = "摘要", requiredMode = Schema.RequiredMode.REQUIRED, example = "报销差旅费")
private String digest;
@Schema(description = "借贷方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer direction;
@Schema(description = "金额比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private BigDecimal amountRatio;
}
}
@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTemplateCategoryEnum;
import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingTimeTypeEnum;
import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum;
import cn.iocoder.yudao.module.fms.enums.report.FmsFormulaRuleEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - FMS 结账模板保存 Request VO")
@Data
public class FmsClosingTemplateSaveReqVO {
@Schema(description = "模板编号", example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "账套不能为空")
private Long accountSetId;
@Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "报销差旅费")
@NotBlank(message = "模板名称不能为空")
@Size(max = 255, message = "模板名称长度不能超过 255 个字符")
private String name;
@Schema(description = "模板分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "模板分类不能为空")
@InEnum(FmsClosingTemplateCategoryEnum.class)
private Integer category;
@Schema(description = "是否期末结转", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "是否期末结转不能为空")
private Boolean periodEnd;
@Schema(description = "来源科目编号", example = "1024")
private Long subjectId;
@Schema(description = "取数规则", example = "0")
@InEnum(FmsFormulaRuleEnum.class)
private Integer formulaRule;
@Schema(description = "取数时间类型", example = "1")
@InEnum(FmsClosingTimeTypeEnum.class)
private Integer timeType;
@Schema(description = "结转科目规则数组", requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
@NotEmpty(message = "结转科目规则不能为空")
@Size(min = 2, message = "结转科目规则至少需要两条")
private List<SubjectRule> subjects;
@Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "显示顺序不能为空")
private Integer sort;
@Schema(description = "管理后台 - FMS 结账模板科目规则")
@Data
public static class SubjectRule {
@Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "科目不能为空")
private Long subjectId;
@Schema(description = "摘要", requiredMode = Schema.RequiredMode.REQUIRED, example = "报销差旅费")
@NotBlank(message = "摘要不能为空")
@Size(max = 500, message = "摘要长度不能超过 500 个字符")
private String digest;
@Schema(description = "借贷方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "借贷方向不能为空")
@InEnum(FmsDebitCreditDirectionEnum.class)
private Integer direction;
@Schema(description = "金额比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "金额比例不能为空")
@DecimalMin(value = "0", inclusive = false, message = "金额比例必须大于 0")
@DecimalMax(value = "100", message = "金额比例不能超过 100")
private BigDecimal amountRatio;
}
}
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - FMS 批量生成结转凭证 Request VO")
@Data
public class FmsClosingVoucherGenerateReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "账套不能为空")
private Long accountSetId;
@Schema(description = "方案编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "方案编号不能为空")
private List<@NotNull(message = "方案编号不能为空") Long> ids;
@Schema(description = "会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08")
@NotBlank(message = "会计期间不能为空")
@Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "会计期间格式不正确")
private String month;
}
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
@Schema(description = "管理后台 - FMS 生成结转损益凭证 Request VO")
@Data
public class FmsProfitLossGenerateReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套不能为空")
private Long accountSetId;
@Schema(description = "会计期间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-08")
@NotBlank(message = "会计期间不能为空")
@Pattern(regexp = "\\d{4}-(0[1-9]|1[0-2])", message = "会计期间格式不正确")
private String month;
}
@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.fms.enums.closing.FmsClosingVoucherTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - FMS 结转损益设置保存 Request VO")
@Data
public class FmsProfitLossSettingsSaveReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套不能为空")
private Long accountSetId;
@Schema(description = "凭证字编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
@NotNull(message = "凭证字不能为空")
private Long voucherWordId;
@Schema(description = "凭证摘要", requiredMode = Schema.RequiredMode.REQUIRED, example = "结转损益")
@NotBlank(message = "凭证摘要不能为空")
@Size(max = 500, message = "凭证摘要长度不能超过 500 个字符")
private String digest;
@Schema(description = "结转凭证类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotNull(message = "结转凭证类型不能为空")
@InEnum(FmsClosingVoucherTypeEnum.class)
private Integer voucherType;
@Schema(description = "以前年度损益调整科目编号", requiredMode = Schema.RequiredMode.REQUIRED,
example = "1024")
@NotNull(message = "以前年度损益调整科目不能为空")
private Long priorYearAdjustmentSubjectId;
@Schema(description = "以前年度损益调整结转科目编号", requiredMode = Schema.RequiredMode.REQUIRED,
example = "1025")
@NotNull(message = "以前年度损益调整结转科目不能为空")
private Long adjustmentClosingSubjectId;
@Schema(description = "其他损益结转科目编号", requiredMode = Schema.RequiredMode.REQUIRED,
example = "1026")
@NotNull(message = "其他损益结转科目不能为空")
private Long otherClosingSubjectId;
@Schema(description = "是否按余额反向结转", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "结转方式不能为空")
private Boolean reverseBalance;
@Schema(description = "结转日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "31")
@NotNull(message = "结转日期不能为空")
@Min(value = 1, message = "结转日期不能小于 1")
@Max(value = 31, message = "结转日期不能大于 31")
private Integer closingDay;
}
@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.fms.controller.admin.closing.vo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - FMS 专用结转设置保存 Request VO")
@Data
public class FmsSpecialClosingSettingsSaveReqVO {
@Schema(description = "方案编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "方案编号不能为空")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "账套不能为空")
private Long accountSetId;
@Schema(description = "凭证字编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
@NotNull(message = "凭证字不能为空")
private Long voucherWordId;
@Schema(description = "结转科目规则数组", requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
@NotEmpty(message = "结转科目规则不能为空")
@Size(min = 2, message = "结转科目规则至少需要两条")
private List<SubjectRule> subjects;
@Schema(description = "管理后台 - FMS 专用结转科目规则")
@Data
public static class SubjectRule {
@Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "科目不能为空")
private Long subjectId;
@Schema(description = "摘要", requiredMode = Schema.RequiredMode.REQUIRED, example = "计提所得税")
@NotBlank(message = "摘要不能为空")
@Size(max = 500, message = "摘要长度不能超过 500 个字符")
private String digest;
@Schema(description = "借贷方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "借贷方向不能为空")
@InEnum(FmsDebitCreditDirectionEnum.class)
private Integer direction;
@Schema(description = "金额比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "25")
@NotNull(message = "金额比例不能为空")
@DecimalMin(value = "0", inclusive = false, message = "金额比例必须大于 0")
@DecimalMax(value = "100", message = "金额比例不能超过 100")
private BigDecimal amountRatio;
}
}
@@ -0,0 +1,105 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetInitializeReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset.FmsAccountSetSaveReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO;
import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService;
import cn.iocoder.yudao.module.fms.service.config.FmsAccountUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 账套")
@RestController
@RequestMapping("/fms/config/account-set")
@Validated
public class FmsAccountSetController {
@Resource
private FmsAccountSetService accountSetService;
@Resource
private FmsAccountUserService accountUserService;
@PostMapping("/create")
@Operation(summary = "创建账套")
@PreAuthorize("@ss.hasPermission('fms:config:account-set:create')")
public CommonResult<Long> createAccountSet(@Valid @RequestBody FmsAccountSetSaveReqVO createReqVO) {
return success(accountSetService.createAccountSet(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "更新账套")
@PreAuthorize("@ss.hasPermission('fms:config:account-set:update')")
public CommonResult<Boolean> updateAccountSet(@Valid @RequestBody FmsAccountSetSaveReqVO updateReqVO) {
accountSetService.updateAccountSet(updateReqVO, getLoginUserId());
return success(true);
}
@PutMapping("/initialize")
@Operation(summary = "初始化账套")
@PreAuthorize("@ss.hasPermission('fms:config:account-set:initialize')")
public CommonResult<Boolean> initializeAccountSet(
@Valid @RequestBody FmsAccountSetInitializeReqVO initializeReqVO) {
accountSetService.initializeAccountSet(initializeReqVO, getLoginUserId());
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得账套")
@Parameter(name = "id", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:account-set:query')")
public CommonResult<FmsAccountSetRespVO> getAccountSet(@RequestParam("id") Long id) {
FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission(id, getLoginUserId());
FmsAccountUserDO accountUser = accountUserService.getAccountUser(id, getLoginUserId());
return success(buildAccountSetRespVO(accountSet, accountUser));
}
@GetMapping("/list")
@Operation(summary = "获得当前用户账套列表")
@PreAuthorize("@ss.hasPermission('fms:config:account-set:query')")
public CommonResult<List<FmsAccountSetRespVO>> getAccountSetList() {
List<FmsAccountUserDO> accountUsers = accountUserService.getAccountUserList(getLoginUserId());
Map<Long, FmsAccountSetDO> accountSetMap = accountSetService.getAccountSetMap(
convertSet(accountUsers, FmsAccountUserDO::getAccountSetId));
return success(buildAccountSetRespVOList(accountSetMap, accountUsers));
}
// ==================== 拼接 VO ====================
private FmsAccountSetRespVO buildAccountSetRespVO(
FmsAccountSetDO accountSet, FmsAccountUserDO accountUser) {
return BeanUtils.toBean(accountSet, FmsAccountSetRespVO.class)
.setDefaultStatus(accountUser.getDefaultStatus()).setFounder(accountUser.getFounder())
.setLevel(accountUser.getLevel());
}
private List<FmsAccountSetRespVO> buildAccountSetRespVOList(
Map<Long, FmsAccountSetDO> accountSetMap, List<FmsAccountUserDO> accountUsers) {
return convertList(accountUsers, accountUser -> buildAccountSetRespVO(
accountSetMap.get(accountUser.getAccountSetId()), accountUser));
}
}
@@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser.FmsAccountUserRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser.FmsAccountUserUpdateReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountUserDO;
import cn.iocoder.yudao.module.fms.service.config.FmsAccountUserService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 账套用户")
@RestController
@RequestMapping("/fms/config/account-user")
@Validated
public class FmsAccountUserController {
@Resource
private FmsAccountUserService accountUserService;
@Resource
private AdminUserApi adminUserApi;
@Resource
private DeptApi deptApi;
@GetMapping("/list")
@Operation(summary = "获得账套用户列表")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:account-set:authorize')")
public CommonResult<List<FmsAccountUserRespVO>> getAccountUserList(
@RequestParam("accountSetId") Long accountSetId) {
List<FmsAccountUserDO> accountUsers = accountUserService.getAccountUserList(
accountSetId, getLoginUserId());
return success(buildAccountUserRespVOList(accountUsers));
}
@PutMapping("/update")
@Operation(summary = "更新账套用户列表")
@PreAuthorize("@ss.hasPermission('fms:config:account-set:authorize')")
public CommonResult<Boolean> updateAccountUserList(
@Valid @RequestBody FmsAccountUserUpdateReqVO updateReqVO) {
accountUserService.updateAccountUserList(updateReqVO, getLoginUserId());
return success(true);
}
@PutMapping("/update-default-status")
@Operation(summary = "更新默认账套状态")
@PreAuthorize("@ss.hasPermission('fms:config:account-set:query')")
public CommonResult<Boolean> updateAccountSetDefaultStatus(@RequestParam("accountSetId") Long accountSetId) {
accountUserService.updateAccountSetDefaultStatus(accountSetId, getLoginUserId());
return success(true);
}
// ==================== 拼接 VO ====================
private List<FmsAccountUserRespVO> buildAccountUserRespVOList(List<FmsAccountUserDO> accountUsers) {
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSet(accountUsers, FmsAccountUserDO::getUserId));
Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
return convertList(accountUsers, accountUser -> {
FmsAccountUserRespVO respVO = BeanUtils.toBean(accountUser, FmsAccountUserRespVO.class);
findAndThen(userMap, accountUser.getUserId(), user -> {
respVO.setNickname(user.getNickname()).setMobile(user.getMobile())
.setEmail(user.getEmail()).setStatus(user.getStatus());
findAndThen(deptMap, user.getDeptId(), dept -> respVO.setDeptName(dept.getName()));
});
return respVO;
});
}
}
@@ -0,0 +1,228 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemImportExcelVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemImportRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemPageReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemSaveReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem.FmsAuxiliaryItemStatusReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryItemDO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO;
import cn.iocoder.yudao.module.fms.enums.config.FmsAuxiliaryTypeEnum;
import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryItemService;
import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 辅助核算项目")
@RestController
@RequestMapping("/fms/config/auxiliary-item")
@Validated
public class FmsAuxiliaryItemController {
@Resource
private FmsAuxiliaryItemService auxiliaryItemService;
@Resource
private FmsAuxiliaryTypeService auxiliaryTypeService;
@PostMapping("/create")
@Operation(summary = "创建辅助核算项目")
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:create')")
public CommonResult<Long> createAuxiliaryItem(@Valid @RequestBody FmsAuxiliaryItemSaveReqVO createReqVO) {
return success(auxiliaryItemService.createAuxiliaryItem(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "更新辅助核算项目")
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:update')")
public CommonResult<Boolean> updateAuxiliaryItem(
@Valid @RequestBody FmsAuxiliaryItemSaveReqVO updateReqVO) {
auxiliaryItemService.updateAuxiliaryItem(updateReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete-list")
@Operation(summary = "删除辅助核算项目")
@Parameters({
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"),
@Parameter(name = "ids", description = "辅助核算项目编号数组", required = true, example = "[1024]")
})
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:delete')")
public CommonResult<Boolean> deleteAuxiliaryItemList(
@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("ids") @NotEmpty List<Long> ids) {
auxiliaryItemService.deleteAuxiliaryItemList(accountSetId, ids, getLoginUserId());
return success(true);
}
@PutMapping("/update-status")
@Operation(summary = "更新辅助核算项目状态")
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:update')")
public CommonResult<Boolean> updateAuxiliaryItemStatus(
@Valid @RequestBody FmsAuxiliaryItemStatusReqVO reqVO) {
auxiliaryItemService.updateAuxiliaryItemStatus(
reqVO.getAccountSetId(), reqVO.getId(), reqVO.getStatus(), getLoginUserId());
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得辅助核算项目")
@Parameters({
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"),
@Parameter(name = "id", description = "辅助核算项目编号", required = true, example = "1024")
})
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:query')")
public CommonResult<FmsAuxiliaryItemRespVO> getAuxiliaryItem(@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
FmsAuxiliaryItemDO item = auxiliaryItemService.getAuxiliaryItem(accountSetId, id, getLoginUserId());
return success(BeanUtils.toBean(item, FmsAuxiliaryItemRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得辅助核算项目分页")
@PreAuthorize("@ss.hasAnyPermissions('fms:config:auxiliary:query', 'fms:voucher:create', "
+ "'fms:voucher:update')")
public CommonResult<PageResult<FmsAuxiliaryItemRespVO>> getAuxiliaryItemPage(
@Valid FmsAuxiliaryItemPageReqVO pageReqVO) {
PageResult<FmsAuxiliaryItemDO> pageResult = auxiliaryItemService
.getAuxiliaryItemPage(pageReqVO, getLoginUserId());
return success(BeanUtils.toBean(pageResult, FmsAuxiliaryItemRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获得辅助核算项目精简列表", description = "只包含启用的项目,主要用于前端下拉选项")
@Parameters({
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"),
@Parameter(name = "auxiliaryTypeId", description = "辅助核算类别编号", required = true,
example = "1024")
})
public CommonResult<List<FmsAuxiliaryItemRespVO>> getAuxiliaryItemSimpleList(
@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("auxiliaryTypeId") @NotNull Long auxiliaryTypeId) {
List<FmsAuxiliaryItemDO> list = auxiliaryItemService
.getAuxiliaryItemListByAccountSetIdAndAuxiliaryTypeIdAndStatus(
accountSetId, auxiliaryTypeId, CommonStatusEnum.ENABLE.getStatus(), getLoginUserId());
return success(convertList(list, item -> new FmsAuxiliaryItemRespVO()
.setId(item.getId()).setCode(item.getCode()).setName(item.getName())));
}
@GetMapping("/export-excel")
@Operation(summary = "导出辅助核算项目")
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:export')")
@ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
public void exportAuxiliaryItemExcel(
@Valid FmsAuxiliaryItemPageReqVO pageReqVO, HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<FmsAuxiliaryItemDO> list = auxiliaryItemService
.getAuxiliaryItemPage(pageReqVO, getLoginUserId()).getList();
FmsAuxiliaryTypeDO auxiliaryType = auxiliaryTypeService.getAuxiliaryType(
pageReqVO.getAccountSetId(), pageReqVO.getAuxiliaryTypeId(), getLoginUserId());
ExcelUtils.write(response, auxiliaryType.getName() + ".xlsx", "辅助核算项目",
buildAuxiliaryItemExcelHead(auxiliaryType.getType()), buildAuxiliaryItemExcelData(list,
auxiliaryType.getType()));
}
@GetMapping("/get-import-template")
@Operation(summary = "获得辅助核算项目导入模板")
@Parameter(name = "type", description = "辅助核算类别类型", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:import')")
@ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
public void getAuxiliaryItemImportTemplate(
@RequestParam("type") @InEnum(FmsAuxiliaryTypeEnum.class) Integer type,
HttpServletResponse response) throws IOException {
FmsAuxiliaryItemImportExcelVO example = new FmsAuxiliaryItemImportExcelVO()
.setCode("001").setName("示例项目").setRemark("示例备注");
if (FmsAuxiliaryTypeEnum.INVENTORY.getType().equals(type)) {
example.setSpecification("标准版").setUnit("个");
}
ExcelUtils.write(response, "辅助核算项目导入模板.xlsx", "辅助核算项目",
FmsAuxiliaryItemImportExcelVO.class, Collections.singletonList(example));
}
@PostMapping("/import")
@Operation(summary = "导入辅助核算项目")
@Parameters({
@Parameter(name = "accountSetId", description = "账套编号", required = true),
@Parameter(name = "auxiliaryTypeId", description = "辅助核算类别编号", required = true),
@Parameter(name = "file", description = "Excel 文件", required = true)
})
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:import')")
@ApiAccessLog(operateType = OperateTypeEnum.IMPORT)
public CommonResult<FmsAuxiliaryItemImportRespVO> importAuxiliaryItem(
@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("auxiliaryTypeId") @NotNull Long auxiliaryTypeId,
@RequestParam("file") MultipartFile file) throws Exception {
List<FmsAuxiliaryItemImportExcelVO> importItems = ExcelUtils.read(
file, FmsAuxiliaryItemImportExcelVO.class);
return success(auxiliaryItemService.importAuxiliaryItemList(
accountSetId, auxiliaryTypeId, importItems, getLoginUserId()));
}
/** 构建辅助核算项目导出的动态表头 */
private List<List<String>> buildAuxiliaryItemExcelHead(Integer type) {
if (FmsAuxiliaryTypeEnum.CUSTOMER.getType().equals(type)
|| FmsAuxiliaryTypeEnum.SUPPLIER.getType().equals(type)) {
return Arrays.asList(Collections.singletonList("编码"), Collections.singletonList("名称"),
Collections.singletonList("备注"));
}
if (FmsAuxiliaryTypeEnum.INVENTORY.getType().equals(type)) {
return Arrays.asList(Collections.singletonList("编码"), Collections.singletonList("名称"),
Collections.singletonList("规格"), Collections.singletonList("单位"));
}
return Arrays.asList(Collections.singletonList("编码"), Collections.singletonList("名称"));
}
/** 构建辅助核算项目导出的动态数据 */
private List<List<Object>> buildAuxiliaryItemExcelData(List<FmsAuxiliaryItemDO> items, Integer type) {
List<List<Object>> rows = new ArrayList<>(items.size());
for (FmsAuxiliaryItemDO item : items) {
if (FmsAuxiliaryTypeEnum.CUSTOMER.getType().equals(type)
|| FmsAuxiliaryTypeEnum.SUPPLIER.getType().equals(type)) {
rows.add(Arrays.asList(item.getCode(), item.getName(), item.getRemark()));
} else if (FmsAuxiliaryTypeEnum.INVENTORY.getType().equals(type)) {
rows.add(Arrays.asList(item.getCode(), item.getName(), item.getSpecification(), item.getUnit()));
} else {
rows.add(Arrays.asList(item.getCode(), item.getName()));
}
}
return rows;
}
}
@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype.FmsAuxiliaryTypeRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype.FmsAuxiliaryTypeSaveReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO;
import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 辅助核算类别")
@RestController
@RequestMapping("/fms/config/auxiliary-type")
@Validated
public class FmsAuxiliaryTypeController {
@Resource
private FmsAuxiliaryTypeService auxiliaryTypeService;
@PostMapping("/create")
@Operation(summary = "创建辅助核算类别")
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:create')")
public CommonResult<Long> createAuxiliaryType(@Valid @RequestBody FmsAuxiliaryTypeSaveReqVO createReqVO) {
return success(auxiliaryTypeService.createAuxiliaryType(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "更新辅助核算类别")
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:update')")
public CommonResult<Boolean> updateAuxiliaryType(@Valid @RequestBody FmsAuxiliaryTypeSaveReqVO updateReqVO) {
auxiliaryTypeService.updateAuxiliaryType(updateReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除辅助核算类别")
@Parameters({
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"),
@Parameter(name = "id", description = "辅助核算类别编号", required = true, example = "1024")
})
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:delete')")
public CommonResult<Boolean> deleteAuxiliaryType(
@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
auxiliaryTypeService.deleteAuxiliaryType(accountSetId, id, getLoginUserId());
return success(true);
}
@GetMapping("/list")
@Operation(summary = "获得辅助核算类别列表")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:auxiliary:query')")
public CommonResult<List<FmsAuxiliaryTypeRespVO>> getAuxiliaryTypeList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
return success(BeanUtils.toBean(auxiliaryTypeService.getAuxiliaryTypeList(
accountSetId, getLoginUserId()), FmsAuxiliaryTypeRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获得辅助核算类别精简列表", description = "主要用于前端的下拉选项")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
public CommonResult<List<FmsAuxiliaryTypeRespVO>> getAuxiliaryTypeSimpleList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
List<FmsAuxiliaryTypeDO> list = auxiliaryTypeService.getAuxiliaryTypeList(
accountSetId, getLoginUserId());
return success(convertList(list, auxiliaryType -> new FmsAuxiliaryTypeRespVO()
.setId(auxiliaryType.getId()).setName(auxiliaryType.getName()).setType(auxiliaryType.getType())));
}
}
@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency.FmsCurrencyRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency.FmsCurrencySaveReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsCurrencyDO;
import cn.iocoder.yudao.module.fms.service.config.FmsCurrencyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 币别")
@RestController
@RequestMapping("/fms/config/currency")
@Validated
public class FmsCurrencyController {
@Resource
private FmsCurrencyService currencyService;
@PostMapping("/create")
@Operation(summary = "创建币别")
@PreAuthorize("@ss.hasPermission('fms:config:currency:create')")
public CommonResult<Long> createCurrency(@Valid @RequestBody FmsCurrencySaveReqVO createReqVO) {
return success(currencyService.createCurrency(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "更新币别")
@PreAuthorize("@ss.hasPermission('fms:config:currency:update')")
public CommonResult<Boolean> updateCurrency(@Valid @RequestBody FmsCurrencySaveReqVO updateReqVO) {
currencyService.updateCurrency(updateReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除币别")
@Parameters({
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"),
@Parameter(name = "id", description = "币别编号", required = true, example = "1024")
})
@PreAuthorize("@ss.hasPermission('fms:config:currency:delete')")
public CommonResult<Boolean> deleteCurrency(
@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
currencyService.deleteCurrency(accountSetId, id, getLoginUserId());
return success(true);
}
@GetMapping("/list")
@Operation(summary = "获得币别列表")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:currency:query')")
public CommonResult<List<FmsCurrencyRespVO>> getCurrencyList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
List<FmsCurrencyDO> list = currencyService.getCurrencyList(accountSetId, getLoginUserId());
return success(BeanUtils.toBean(list, FmsCurrencyRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获得币别精简列表", description = "主要用于前端的下拉选项")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
public CommonResult<List<FmsCurrencyRespVO>> getCurrencySimpleList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
List<FmsCurrencyDO> list = currencyService.getCurrencyList(accountSetId, getLoginUserId());
return success(convertList(list, currency -> new FmsCurrencyRespVO()
.setId(currency.getId()).setCode(currency.getCode()).setName(currency.getName())
.setExchangeRate(currency.getExchangeRate()).setStandard(currency.getStandard())));
}
}
@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest.FmsDigestRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest.FmsDigestSaveReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsDigestDO;
import cn.iocoder.yudao.module.fms.service.config.FmsDigestService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
* FMS 常用摘要 Controller
*
* @author 芋道源码
*/
@Tag(name = "管理后台 - FMS 常用摘要")
@RestController
@RequestMapping("/fms/config/digest")
@Validated
public class FmsDigestController {
@Resource
private FmsDigestService digestService;
@PostMapping("/create")
@Operation(summary = "创建常用摘要")
@PreAuthorize("@ss.hasPermission('fms:config:digest:create')")
public CommonResult<Long> createDigest(@Valid @RequestBody FmsDigestSaveReqVO createReqVO) {
return success(digestService.createDigest(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "修改常用摘要")
@PreAuthorize("@ss.hasPermission('fms:config:digest:update')")
public CommonResult<Boolean> updateDigest(@Valid @RequestBody FmsDigestSaveReqVO updateReqVO) {
digestService.updateDigest(updateReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除常用摘要")
@Parameter(name = "id", description = "常用摘要编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:digest:delete')")
public CommonResult<Boolean> deleteDigest(@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
digestService.deleteDigest(accountSetId, id, getLoginUserId());
return success(true);
}
@GetMapping("/list")
@Operation(summary = "获得常用摘要管理列表")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:digest:query')")
public CommonResult<List<FmsDigestRespVO>> getDigestList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
List<FmsDigestDO> list = digestService.getDigestList(accountSetId, getLoginUserId());
return success(BeanUtils.toBean(list, FmsDigestRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获得常用摘要精简列表")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
public CommonResult<List<FmsDigestRespVO>> getDigestSimpleList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
List<FmsDigestDO> list = digestService.getDigestList(accountSetId, getLoginUserId());
return success(convertList(list, digest -> new FmsDigestRespVO()
.setId(digest.getId()).setAccountSetId(digest.getAccountSetId()).setContent(digest.getContent())));
}
}
@@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeindicator.FmsFinanceIndicatorRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeindicator.FmsFinanceIndicatorSaveReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceIndicatorDO;
import cn.iocoder.yudao.module.fms.service.config.FmsFinanceIndicatorService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 财务指标")
@RestController
@RequestMapping("/fms/config/finance-indicator")
@Validated
public class FmsFinanceIndicatorController {
@Resource
private FmsFinanceIndicatorService financeIndicatorService;
@PostMapping("/create")
@Operation(summary = "创建首页财务指标")
@PreAuthorize("@ss.hasPermission('fms:config:finance-indicator:create')")
public CommonResult<Long> create(@Valid @RequestBody FmsFinanceIndicatorSaveReqVO reqVO) {
return success(financeIndicatorService.createFinanceIndicator(reqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "修改首页财务指标")
@PreAuthorize("@ss.hasPermission('fms:config:finance-indicator:update')")
public CommonResult<Boolean> update(@Valid @RequestBody FmsFinanceIndicatorSaveReqVO reqVO) {
financeIndicatorService.updateFinanceIndicator(reqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除首页财务指标")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@Parameter(name = "id", description = "指标编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:finance-indicator:delete')")
public CommonResult<Boolean> delete(@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
financeIndicatorService.deleteFinanceIndicator(accountSetId, id, getLoginUserId());
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得首页财务指标")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@Parameter(name = "id", description = "指标编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:finance-indicator:query')")
public CommonResult<FmsFinanceIndicatorRespVO> get(@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
FmsFinanceIndicatorDO indicator = financeIndicatorService.getFinanceIndicator(accountSetId, id, getLoginUserId());
return success(BeanUtils.toBean(indicator, FmsFinanceIndicatorRespVO.class));
}
@GetMapping("/list")
@Operation(summary = "获得首页财务指标列表")
@PreAuthorize("@ss.hasPermission('fms:config:finance-indicator:query')")
public CommonResult<List<FmsFinanceIndicatorRespVO>> getList(@RequestParam("accountSetId") @NotNull Long accountSetId) {
List<FmsFinanceIndicatorDO> list = financeIndicatorService.getFinanceIndicatorList(accountSetId,
getLoginUserId());
return success(BeanUtils.toBean(list, FmsFinanceIndicatorRespVO.class));
}
}
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter.FmsFinanceParameterRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter.FmsFinanceParameterUpdateReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsFinanceParameterDO;
import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 财务参数")
@RestController
@RequestMapping("/fms/config/finance-parameter")
@Validated
public class FmsFinanceParameterController {
@Resource
private FmsFinanceParameterService financeParameterService;
@GetMapping("/get")
@Operation(summary = "获得财务参数")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:finance-parameter:query')")
public CommonResult<FmsFinanceParameterRespVO> getFinanceParameter(
@RequestParam("accountSetId") Long accountSetId) {
FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(
accountSetId, getLoginUserId());
return success(BeanUtils.toBean(financeParameter, FmsFinanceParameterRespVO.class));
}
@PutMapping("/update")
@Operation(summary = "更新财务参数")
@PreAuthorize("@ss.hasPermission('fms:config:finance-parameter:update')")
public CommonResult<Boolean> updateFinanceParameter(
@Valid @RequestBody FmsFinanceParameterUpdateReqVO updateReqVO) {
financeParameterService.updateFinanceParameter(updateReqVO, getLoginUserId());
return success(true);
}
}
@@ -0,0 +1,136 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceSaveReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsTrialBalanceRespVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO;
import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceExcelVO;
import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.INITIAL_BALANCE_IMPORT_TEMPLATE_INVALID;
@Tag(name = "管理后台 - FMS 初始余额")
@RestController
@RequestMapping("/fms/config/initial-balance")
@Validated
public class FmsInitialBalanceController {
@Resource
private FmsInitialBalanceService initialBalanceService;
@Resource
private FmsAccountSetService accountSetService;
@GetMapping("/list")
@Operation(summary = "获得初始余额列表")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@Parameter(name = "subjectType", description = "科目类型", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('fms:config:initial-balance:query')")
public CommonResult<List<FmsInitialBalanceRespVO>> getInitialBalanceList(
@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("subjectType") @NotNull Integer subjectType) {
return success(initialBalanceService.getInitialBalanceList(accountSetId, subjectType, getLoginUserId()));
}
@PutMapping("/save")
@Operation(summary = "保存初始余额")
@PreAuthorize("@ss.hasPermission('fms:config:initial-balance:update')")
public CommonResult<Boolean> saveInitialBalance(@Valid @RequestBody FmsInitialBalanceSaveReqVO saveReqVO) {
initialBalanceService.saveInitialBalance(saveReqVO, getLoginUserId());
return success(true);
}
@GetMapping("/trial-balance")
@Operation(summary = "获得试算平衡结果")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:initial-balance:query')")
public CommonResult<FmsTrialBalanceRespVO> getTrialBalance(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
return success(initialBalanceService.getTrialBalance(accountSetId, getLoginUserId()));
}
@GetMapping("/export-excel")
@Operation(summary = "导出初始余额")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:initial-balance:export')")
@ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
public void exportInitialBalance(
@RequestParam("accountSetId") @NotNull Long accountSetId,
HttpServletResponse response) throws IOException {
// 1. 读取初始余额列表,并展开为 Excel 行
List<FmsInitialBalanceExcelVO> rows = FmsInitialBalanceExcelHelper.toRows(
initialBalanceService.getInitialBalanceList(accountSetId, null, getLoginUserId()), true);
// 2. 输出 Excel
ExcelUtils.write(response, "财务初始余额.xlsx", "财务初始余额",
FmsInitialBalanceExcelVO.class, rows);
}
@GetMapping("/get-import-template")
@Operation(summary = "获得初始余额导入模板")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:initial-balance:import')")
@ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
public void getInitialBalanceImportTemplate(@RequestParam("accountSetId") @NotNull Long accountSetId,
HttpServletResponse response)
throws IOException {
// 1. 读取初始余额列表,并展开为 Excel 行
Long userId = getLoginUserId();
List<FmsInitialBalanceRespVO> initialBalances = initialBalanceService.getInitialBalanceList(accountSetId, null, userId);
List<FmsInitialBalanceExcelVO> rows = FmsInitialBalanceExcelHelper.toRows(initialBalances, false);
// 2. 生成导入模板,一月启用时只需要录入期初余额
FmsAccountSetDO accountSet = accountSetService.validateAccountSetReadPermission(accountSetId, userId);
byte[] content = FmsInitialBalanceExcelHelper.writeTemplate(
rows, LocalDateTimeUtils.isJanuary(accountSet.getStartTime()));
// 3. 写入响应
ServletUtils.writeAttachment(response, "财务初始余额导入模板.xlsx", content);
}
@PostMapping("/import")
@Operation(summary = "导入初始余额")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:initial-balance:import')")
@ApiAccessLog(operateType = OperateTypeEnum.IMPORT)
public CommonResult<Integer> importInitialBalance(
@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("file") MultipartFile file) throws IOException {
// 1. 读取 Excel
List<FmsInitialBalanceExcelVO> rows;
try {
rows = FmsInitialBalanceExcelHelper.read(file);
} catch (IllegalArgumentException | IOException ex) {
throw exception(INITIAL_BALANCE_IMPORT_TEMPLATE_INVALID);
}
// 2. 导入数据
return success(initialBalanceService.importInitialBalance(accountSetId, rows, getLoginUserId()));
}
}
@@ -0,0 +1,343 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceRespVO;
import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum;
import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance.FmsInitialBalanceExcelVO;
import lombok.experimental.UtilityClass;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.number.NumberUtils.zeroIfNull;
import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.createDataStyle;
import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.createHeaderStyle;
import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.createNumberStyle;
import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.getCellText;
import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.getDecimal;
import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.isEmptyRow;
import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.mergeHeader;
import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.writeFormula;
import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.writeNumber;
import static cn.iocoder.yudao.framework.excel.core.util.PoiExcelUtils.writeText;
/**
* FMS 初始余额 Excel 工具
*
* @author 芋道源码
*/
@UtilityClass
public final class FmsInitialBalanceExcelHelper {
private static final String TEMPLATE_SHEET_NAME = "初始余额模板";
/**
* 展开初始余额平铺列表为 Excel 行
*
* 父级科目仅在 includeParent 时输出行;启用辅助核算且存在明细的科目按明细逐行输出
*/
public static List<FmsInitialBalanceExcelVO> toRows(List<FmsInitialBalanceRespVO> results,
boolean includeParent) {
Set<Long> parentIds = convertSet(results, FmsInitialBalanceRespVO::getParentId);
List<FmsInitialBalanceExcelVO> rows = new ArrayList<>();
results.forEach(result -> {
if (parentIds.contains(result.getSubjectId())) {
if (includeParent) {
rows.add(buildRow(result, null));
}
return;
}
if (Boolean.FALSE.equals(result.getAuxiliaryAccounting())
|| CollUtil.isEmpty(result.getAssistBalances())) {
rows.add(buildRow(result, null));
return;
}
result.getAssistBalances().forEach(assist -> rows.add(buildRow(result, assist)));
});
return rows;
}
/**
* 构建单个科目或辅助核算明细对应的 Excel 行
*/
private static FmsInitialBalanceExcelVO buildRow(FmsInitialBalanceRespVO result,
FmsInitialBalanceRespVO.AssistBalance assist) {
FmsInitialBalanceExcelVO row = new FmsInitialBalanceExcelVO()
.setSubjectCode(result.getSubjectCode()).setSubjectName(result.getSubjectName())
.setDirectionName(FmsDebitCreditDirectionEnum.valueOf(result.getBalanceDirection()).getName())
.setSubjectType(result.getType()).setQuantityAccounting(result.getQuantityAccounting())
.setAuxiliaryAccounting(result.getAuxiliaryAccounting());
if (assist == null) {
copyAmounts(result, row);
} else {
// 辅助核算项目拼接为“类别:名称/类别:名称”,与导入解析的格式对应
Map<Long, String> auxiliaryTypeNameMap = convertMap(result.getAuxiliaryConfigs(),
FmsInitialBalanceRespVO.AuxiliaryConfig::getAuxiliaryTypeId,
FmsInitialBalanceRespVO.AuxiliaryConfig::getName);
row.setAuxiliaryItems(String.join("/", convertList(assist.getAuxiliaries(),
item -> auxiliaryTypeNameMap.get(item.getTypeId()) + ":" + item.getName())));
copyAmounts(assist, row);
}
return row;
}
private static void copyAmounts(FmsInitialBalanceRespVO source, FmsInitialBalanceExcelVO target) {
target.setOpeningAmount(zeroIfNull(source.getOpeningAmount()))
.setOpeningQuantity(zeroIfNull(source.getOpeningQuantity()))
.setYearDebitAmount(zeroIfNull(source.getYearDebitAmount()))
.setYearDebitQuantity(zeroIfNull(source.getYearDebitQuantity()))
.setYearCreditAmount(zeroIfNull(source.getYearCreditAmount()))
.setYearCreditQuantity(zeroIfNull(source.getYearCreditQuantity()))
.setYearOpeningAmount(zeroIfNull(source.getYearOpeningAmount()))
.setYearOpeningQuantity(zeroIfNull(source.getYearOpeningQuantity()))
.setProfitLossAmount(zeroIfNull(source.getProfitLossAmount()))
.setProfitLossQuantity(zeroIfNull(source.getProfitLossQuantity()));
}
private static void copyAmounts(FmsInitialBalanceRespVO.AssistBalance source,
FmsInitialBalanceExcelVO target) {
target.setOpeningAmount(zeroIfNull(source.getOpeningAmount()))
.setOpeningQuantity(zeroIfNull(source.getOpeningQuantity()))
.setYearDebitAmount(zeroIfNull(source.getYearDebitAmount()))
.setYearDebitQuantity(zeroIfNull(source.getYearDebitQuantity()))
.setYearCreditAmount(zeroIfNull(source.getYearCreditAmount()))
.setYearCreditQuantity(zeroIfNull(source.getYearCreditQuantity()))
.setYearOpeningAmount(zeroIfNull(source.getYearOpeningAmount()))
.setYearOpeningQuantity(zeroIfNull(source.getYearOpeningQuantity()))
.setProfitLossAmount(zeroIfNull(source.getProfitLossAmount()))
.setProfitLossQuantity(zeroIfNull(source.getProfitLossQuantity()));
}
/**
* 生成导入模板
*
* 模板锁定系统生成列,年初余额列写入公式,并通过 protectSheet 防止误改
*/
public static byte[] writeTemplate(List<FmsInitialBalanceExcelVO> rows,
boolean january) throws IOException {
try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
// 1. 写入表头,一月启用时只保留期初余额
Sheet sheet = workbook.createSheet(TEMPLATE_SHEET_NAME);
writeHeaders(workbook, sheet, 0, january);
// 2. 写入科目数据,系统生成列锁定,可填写列解锁
CellStyle lockedStyle = createDataStyle(workbook, true, IndexedColors.GREY_25_PERCENT);
CellStyle editableStyle = createDataStyle(workbook, false, IndexedColors.WHITE);
CellStyle numberLockedStyle = createNumberStyle(workbook, lockedStyle, "#,##0.0000");
CellStyle amountLockedStyle = createNumberStyle(workbook, lockedStyle, "#,##0.00");
CellStyle numberEditableStyle = createNumberStyle(workbook, editableStyle, "#,##0.0000");
CellStyle amountEditableStyle = createNumberStyle(workbook, editableStyle, "#,##0.00");
for (int index = 0; index < rows.size(); index++) {
writeDataRow(sheet.createRow(index + 2), rows.get(index), january, true,
lockedStyle, editableStyle, numberLockedStyle, amountLockedStyle,
numberEditableStyle, amountEditableStyle);
}
// 3. 保护工作表并补充填写说明
sheet.protectSheet("yudao-fms");
sheet.createFreezePane(4, 2);
writeInstructions(workbook);
workbook.write(output);
return output.toByteArray();
}
}
/**
* 读取导入文件
*
* 按模板表头校验文件合法性,年初余额等公式单元格取公式计算值;不限制导入行数
*/
public static List<FmsInitialBalanceExcelVO> read(MultipartFile file) throws IOException {
try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) {
// 1. 校验模板 sheet 和表头
Sheet sheet = workbook.getSheet(TEMPLATE_SHEET_NAME);
if (sheet == null || !hasValidHeaders(sheet)) {
throw new IllegalArgumentException("初始余额模板表头不正确");
}
// 2. 逐行解析,跳过空行
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
boolean january = sheet.getRow(1).getLastCellNum() <= 6;
List<FmsInitialBalanceExcelVO> rows = new ArrayList<>();
for (int rowIndex = 2; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
Row row = sheet.getRow(rowIndex);
if (isEmptyRow(row, evaluator, Arrays.asList(0, 1, 3, 4, 5))) {
continue;
}
rows.add(parseRow(row, january, evaluator));
}
return rows;
}
}
/**
* 写入双行表头,数量和金额成组;一月启用时只保留期初余额组
*/
private static void writeHeaders(Workbook workbook, Sheet sheet, int rowIndex, boolean january) {
CellStyle style = createHeaderStyle(workbook, IndexedColors.GREY_25_PERCENT);
int lastColumn = january ? 5 : 13;
Row firstRow = sheet.createRow(rowIndex);
Row secondRow = sheet.createRow(rowIndex + 1);
mergeHeader(sheet, firstRow, secondRow, 0, 0, "科目编码", style);
mergeHeader(sheet, firstRow, secondRow, 1, 1, "科目名称", style);
mergeHeader(sheet, firstRow, secondRow, 2, 2, "方向", style);
mergeHeader(sheet, firstRow, secondRow, 3, 3, "辅助核算项目", style);
writeGroupHeader(sheet, firstRow, secondRow, 4, "期初余额", style);
if (!january) {
writeGroupHeader(sheet, firstRow, secondRow, 6, "本年累计借方", style);
writeGroupHeader(sheet, firstRow, secondRow, 8, "本年累计贷方", style);
writeGroupHeader(sheet, firstRow, secondRow, 10, "年初余额", style);
writeGroupHeader(sheet, firstRow, secondRow, 12, "实际损益发生额", style);
}
for (int column = 0; column <= lastColumn; column++) {
sheet.setColumnWidth(column, column == 1 || column == 3 ? 7_000 : 4_000);
}
firstRow.setHeightInPoints(28);
secondRow.setHeightInPoints(24);
}
/**
* 写入“数量/金额”成组表头,第一行合并组名,第二行拆出数量和金额
*/
private static void writeGroupHeader(Sheet sheet, Row firstRow, Row secondRow,
int beginColumn, String title, CellStyle style) {
sheet.addMergedRegion(new CellRangeAddress(firstRow.getRowNum(), firstRow.getRowNum(), beginColumn, beginColumn + 1));
Cell groupCell = firstRow.createCell(beginColumn);
groupCell.setCellValue(title);
groupCell.setCellStyle(style);
firstRow.createCell(beginColumn + 1).setCellStyle(style);
Cell quantityCell = secondRow.createCell(beginColumn);
quantityCell.setCellValue("数量");
quantityCell.setCellStyle(style);
Cell amountCell = secondRow.createCell(beginColumn + 1);
amountCell.setCellValue("金额");
amountCell.setCellStyle(style);
}
/**
* 写入一行数据
*
* 模板模式下按科目配置解锁可填写单元格(数量列仅数量核算科目可填,损益列仅损益类科目可填),
* 年初余额列写入“期初 ± 累计借贷”公式
*/
private static void writeDataRow(Row excelRow, FmsInitialBalanceExcelVO data,
boolean january, boolean template, CellStyle lockedStyle, CellStyle editableStyle,
CellStyle numberLockedStyle, CellStyle amountLockedStyle,
CellStyle numberEditableStyle, CellStyle amountEditableStyle) {
writeText(excelRow, 0, data.getSubjectCode(), lockedStyle);
writeText(excelRow, 1, data.getSubjectName(), lockedStyle);
writeText(excelRow, 2, data.getDirectionName(), lockedStyle);
writeText(excelRow, 3, data.getAuxiliaryItems(),
template && Boolean.TRUE.equals(data.getAuxiliaryAccounting()) ? editableStyle : lockedStyle);
writeNumber(excelRow, 4, data.getOpeningQuantity(),
template && Boolean.TRUE.equals(data.getQuantityAccounting())
? numberEditableStyle : numberLockedStyle);
writeNumber(excelRow, 5, data.getOpeningAmount(), template ? amountEditableStyle : amountLockedStyle);
if (january) {
return;
}
writeNumber(excelRow, 6, data.getYearDebitQuantity(),
template && Boolean.TRUE.equals(data.getQuantityAccounting())
? numberEditableStyle : numberLockedStyle);
writeNumber(excelRow, 7, data.getYearDebitAmount(), template ? amountEditableStyle : amountLockedStyle);
writeNumber(excelRow, 8, data.getYearCreditQuantity(),
template && Boolean.TRUE.equals(data.getQuantityAccounting())
? numberEditableStyle : numberLockedStyle);
writeNumber(excelRow, 9, data.getYearCreditAmount(), template ? amountEditableStyle : amountLockedStyle);
if (template) {
String quantityFormula = "借".equals(data.getDirectionName())
? "E" + (excelRow.getRowNum() + 1) + "-G" + (excelRow.getRowNum() + 1)
+ "+I" + (excelRow.getRowNum() + 1)
: "E" + (excelRow.getRowNum() + 1) + "+G" + (excelRow.getRowNum() + 1)
+ "-I" + (excelRow.getRowNum() + 1);
String amountFormula = "借".equals(data.getDirectionName())
? "F" + (excelRow.getRowNum() + 1) + "-H" + (excelRow.getRowNum() + 1)
+ "+J" + (excelRow.getRowNum() + 1)
: "F" + (excelRow.getRowNum() + 1) + "+H" + (excelRow.getRowNum() + 1)
+ "-J" + (excelRow.getRowNum() + 1);
writeFormula(excelRow, 10, quantityFormula, numberLockedStyle);
writeFormula(excelRow, 11, amountFormula, amountLockedStyle);
} else {
writeNumber(excelRow, 10, data.getYearOpeningQuantity(), numberLockedStyle);
writeNumber(excelRow, 11, data.getYearOpeningAmount(), amountLockedStyle);
}
boolean profitLoss = FmsSubjectTypeEnum.PROFIT_LOSS.getType().equals(data.getSubjectType());
writeNumber(excelRow, 12, profitLoss ? data.getProfitLossQuantity() : null,
template && profitLoss && Boolean.TRUE.equals(data.getQuantityAccounting())
? numberEditableStyle : numberLockedStyle);
writeNumber(excelRow, 13, profitLoss ? data.getProfitLossAmount() : null,
template && profitLoss ? amountEditableStyle : amountLockedStyle);
}
/**
* 解析一行数据,公式单元格通过 evaluator 取计算值
*/
private static FmsInitialBalanceExcelVO parseRow(Row row, boolean january, FormulaEvaluator evaluator) {
FmsInitialBalanceExcelVO result = new FmsInitialBalanceExcelVO()
.setRowNumber(row.getRowNum() + 1)
.setSubjectCode(getCellText(row, 0, evaluator))
.setSubjectName(getCellText(row, 1, evaluator))
.setDirectionName(getCellText(row, 2, evaluator))
.setAuxiliaryItems(getCellText(row, 3, evaluator))
.setOpeningQuantity(getDecimal(row, 4, evaluator))
.setOpeningAmount(getDecimal(row, 5, evaluator));
if (!january) {
result.setYearDebitQuantity(getDecimal(row, 6, evaluator))
.setYearDebitAmount(getDecimal(row, 7, evaluator))
.setYearCreditQuantity(getDecimal(row, 8, evaluator))
.setYearCreditAmount(getDecimal(row, 9, evaluator))
.setYearOpeningQuantity(getDecimal(row, 10, evaluator))
.setYearOpeningAmount(getDecimal(row, 11, evaluator))
.setProfitLossQuantity(getDecimal(row, 12, evaluator))
.setProfitLossAmount(getDecimal(row, 13, evaluator));
}
return result;
}
private static boolean hasValidHeaders(Sheet sheet) {
Row firstRow = sheet.getRow(0);
Row secondRow = sheet.getRow(1);
return firstRow != null && secondRow != null
&& "科目编码".equals(getCellText(firstRow, 0, null))
&& "科目名称".equals(getCellText(firstRow, 1, null))
&& "方向".equals(getCellText(firstRow, 2, null))
&& "辅助核算项目".equals(getCellText(firstRow, 3, null))
&& "期初余额".equals(getCellText(firstRow, 4, null))
&& "数量".equals(getCellText(secondRow, 4, null))
&& "金额".equals(getCellText(secondRow, 5, null));
}
private static void writeInstructions(Workbook workbook) {
Sheet sheet = workbook.createSheet("填写说明");
String[] instructions = {
"1. 灰色单元格由系统生成,请勿修改",
"2. 仅末级科目可以导入初始余额",
"3. 辅助核算项目使用“类别:名称/类别:名称”格式,例如“客户:北京客户/部门:销售部”",
"4. 数量核算科目可填写数量,金额和数量不能小于 0",
"5. 年初余额由期初余额和本年累计发生额自动计算"
};
for (int index = 0; index < instructions.length; index++) {
sheet.createRow(index).createCell(0).setCellValue(instructions[index]);
}
sheet.setColumnWidth(0, 20_000);
}
}
@@ -0,0 +1,195 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectDeleteReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectImportExcelVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectImportRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectListReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectSaveReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectStatusReqVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject.FmsSubjectUsageRespVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAuxiliaryTypeDO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO;
import cn.iocoder.yudao.module.fms.service.config.FmsAuxiliaryTypeService;
import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 会计科目")
@RestController
@RequestMapping("/fms/config/subject")
@Validated
public class FmsSubjectController {
@Resource
private FmsSubjectService subjectService;
@Resource
private FmsAuxiliaryTypeService auxiliaryTypeService;
@PostMapping("/create")
@Operation(summary = "创建科目")
@PreAuthorize("@ss.hasPermission('fms:config:subject:create')")
public CommonResult<Long> createSubject(@Valid @RequestBody FmsSubjectSaveReqVO createReqVO) {
return success(subjectService.createSubject(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "更新科目")
@PreAuthorize("@ss.hasPermission('fms:config:subject:update')")
public CommonResult<Boolean> updateSubject(@Valid @RequestBody FmsSubjectSaveReqVO updateReqVO) {
subjectService.updateSubject(updateReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete-list")
@Operation(summary = "删除科目")
@PreAuthorize("@ss.hasPermission('fms:config:subject:delete')")
public CommonResult<Boolean> deleteSubjectList(@Valid @RequestBody FmsSubjectDeleteReqVO deleteReqVO) {
subjectService.deleteSubjectList(deleteReqVO, getLoginUserId());
return success(true);
}
@PutMapping("/update-status")
@Operation(summary = "更新科目状态")
@PreAuthorize("@ss.hasPermission('fms:config:subject:update')")
public CommonResult<Boolean> updateSubjectStatus(@Valid @RequestBody FmsSubjectStatusReqVO statusReqVO) {
subjectService.updateSubjectStatus(statusReqVO, getLoginUserId());
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得科目")
@Parameters({
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"),
@Parameter(name = "id", description = "科目编号", required = true, example = "1024")
})
@PreAuthorize("@ss.hasPermission('fms:config:subject:query')")
public CommonResult<FmsSubjectRespVO> getSubject(@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
FmsSubjectDO subject = subjectService.getSubject(accountSetId, id, getLoginUserId());
return success(buildSubjectRespVO(subject));
}
@GetMapping("/get-usage")
@Operation(summary = "获得科目使用情况")
@Parameters({
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"),
@Parameter(name = "id", description = "科目编号", required = true, example = "1024")
})
@PreAuthorize("@ss.hasPermission('fms:config:subject:query')")
public CommonResult<FmsSubjectUsageRespVO> getSubjectUsage(
@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
return success(subjectService.getSubjectUsage(accountSetId, id, getLoginUserId()));
}
@GetMapping("/list")
@Operation(summary = "获得科目列表")
@PreAuthorize("@ss.hasPermission('fms:config:subject:query')")
public CommonResult<List<FmsSubjectRespVO>> getSubjectList(@Valid FmsSubjectListReqVO listReqVO) {
List<FmsSubjectDO> subjects = subjectService.getSubjectList(listReqVO, getLoginUserId());
return success(buildSubjectRespVOList(subjects));
}
@GetMapping("/simple-list")
@Operation(summary = "获得科目精简列表")
public CommonResult<List<FmsSubjectRespVO>> getSubjectSimpleList(@Valid FmsSubjectListReqVO listReqVO) {
List<FmsSubjectDO> subjects = subjectService.getSubjectList(listReqVO, getLoginUserId());
return success(convertList(subjects, subject -> new FmsSubjectRespVO()
.setId(subject.getId()).setParentId(subject.getParentId())
.setCode(subject.getCode()).setName(subject.getName()).setType(subject.getType())
.setBalanceDirection(subject.getBalanceDirection()).setStatus(subject.getStatus())
.setLevel(subject.getLevel()).setAuxiliaryTypeIds(subject.getAuxiliaryTypeIds())
.setQuantityAccounting(subject.getQuantityAccounting()).setQuantityUnit(subject.getQuantityUnit())));
}
@GetMapping("/export-excel")
@Operation(summary = "导出科目")
@PreAuthorize("@ss.hasPermission('fms:config:subject:export')")
@ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
public void exportSubject(@Valid FmsSubjectListReqVO listReqVO,
HttpServletResponse response) throws IOException {
List<FmsSubjectDO> subjects = subjectService.getSubjectList(listReqVO, getLoginUserId());
ExcelUtils.write(response, "科目设置.xlsx", "科目设置", FmsSubjectRespVO.class,
buildSubjectRespVOList(subjects));
}
@GetMapping("/get-import-template")
@Operation(summary = "获得科目导入模板")
@PreAuthorize("@ss.hasPermission('fms:config:subject:import')")
@ApiAccessLog(operateType = OperateTypeEnum.EXPORT)
public void getSubjectImportTemplate(HttpServletResponse response) throws IOException {
List<FmsSubjectImportExcelVO> list = Arrays.asList(
FmsSubjectImportExcelVO.builder().code("1001").name("库存现金")
.parentSubjectCode("0").balanceDirection("借").categoryName("流动资产").build(),
FmsSubjectImportExcelVO.builder().code("100101").name("人民币现金")
.parentSubjectCode("1001").balanceDirection("借").categoryName("流动资产")
.auxiliaryNames("部门/职员").build());
ExcelUtils.write(response, "科目导入模板.xlsx", "科目列表", FmsSubjectImportExcelVO.class, list);
}
@PostMapping("/import")
@Operation(summary = "导入科目")
@Parameters({
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024"),
@Parameter(name = "file", description = "Excel 文件", required = true)
})
@PreAuthorize("@ss.hasPermission('fms:config:subject:import')")
@ApiAccessLog(operateType = OperateTypeEnum.IMPORT)
public CommonResult<FmsSubjectImportRespVO> importSubject(
@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("file") MultipartFile file) throws IOException {
List<FmsSubjectImportExcelVO> importSubjects = ExcelUtils.read(file, FmsSubjectImportExcelVO.class);
return success(subjectService.importSubjectList(accountSetId, importSubjects, getLoginUserId()));
}
// ==================== 拼接 VO ====================
private List<FmsSubjectRespVO> buildSubjectRespVOList(List<FmsSubjectDO> subjects) {
Map<Long, FmsAuxiliaryTypeDO> auxiliaryTypeMap = auxiliaryTypeService.getAuxiliaryTypeMap(
convertSetByFlatMap(subjects, FmsSubjectDO::getAuxiliaryTypeIds, List::stream));
return convertList(subjects, subject -> {
FmsSubjectRespVO subjectVO = BeanUtils.toBean(subject, FmsSubjectRespVO.class)
.setCategoryDictValue(subject.getType() + "-" + subject.getCategory());
subjectVO.setAuxiliaryTypeNames(convertList(subject.getAuxiliaryTypeIds(),
auxiliaryTypeId -> auxiliaryTypeMap.get(auxiliaryTypeId).getName()));
if (Boolean.FALSE.equals(subject.getQuantityAccounting())) {
subjectVO.setQuantityUnit("");
}
return subjectVO;
});
}
private FmsSubjectRespVO buildSubjectRespVO(FmsSubjectDO subject) {
return subject == null ? null
: CollUtil.getFirst(buildSubjectRespVOList(Collections.singletonList(subject)));
}
}
@@ -0,0 +1,89 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplatecategory.FmsVoucherTemplateCategoryRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplatecategory.FmsVoucherTemplateCategorySaveReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateCategoryDO;
import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService;
import cn.iocoder.yudao.module.fms.service.config.FmsVoucherTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 凭证模板分类")
@RestController
@RequestMapping("/fms/config/voucher-template-category")
@Validated
public class FmsVoucherTemplateCategoryController {
@Resource
private FmsVoucherTemplateService voucherTemplateService;
@Resource
private FmsAccountSetService accountSetService;
@PostMapping("/create")
@Operation(summary = "创建凭证模板分类")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-template-category:create')")
public CommonResult<Long> createTemplateCategory(
@Valid @RequestBody FmsVoucherTemplateCategorySaveReqVO createReqVO) {
return success(voucherTemplateService.createTemplateCategory(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "修改凭证模板分类")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-template-category:update')")
public CommonResult<Boolean> updateTemplateCategory(
@Valid @RequestBody FmsVoucherTemplateCategorySaveReqVO updateReqVO) {
voucherTemplateService.updateTemplateCategory(updateReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除凭证模板分类")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-template-category:delete')")
public CommonResult<Boolean> deleteTemplateCategory(@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
voucherTemplateService.deleteTemplateCategory(accountSetId, id, getLoginUserId());
return success(true);
}
@GetMapping("/list")
@Operation(summary = "获得凭证模板分类列表")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-template-category:query')")
public CommonResult<List<FmsVoucherTemplateCategoryRespVO>> getTemplateCategoryList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
accountSetService.validateAccountSetReadPermission(accountSetId, getLoginUserId());
List<FmsVoucherTemplateCategoryDO> categories = voucherTemplateService.getTemplateCategoryList(accountSetId);
return success(BeanUtils.toBean(categories, FmsVoucherTemplateCategoryRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获得凭证模板分类精简列表", description = "主要用于前端的下拉选项")
public CommonResult<List<FmsVoucherTemplateCategoryRespVO>> getTemplateCategorySimpleList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
accountSetService.validateAccountSetReadPermission(accountSetId, getLoginUserId());
List<FmsVoucherTemplateCategoryDO> categories = voucherTemplateService.getTemplateCategoryList(accountSetId);
return success(convertList(categories, category -> new FmsVoucherTemplateCategoryRespVO()
.setId(category.getId()).setName(category.getName())));
}
}
@@ -0,0 +1,100 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate.FmsVoucherTemplateRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate.FmsVoucherTemplateSaveReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateCategoryDO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherTemplateDO;
import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService;
import cn.iocoder.yudao.module.fms.service.config.FmsVoucherTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - FMS 凭证模板")
@RestController
@RequestMapping("/fms/config/voucher-template")
@Validated
public class FmsVoucherTemplateController {
@Resource
private FmsVoucherTemplateService voucherTemplateService;
@Resource
private FmsAccountSetService accountSetService;
@PostMapping("/create")
@Operation(summary = "创建凭证模板")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-template:create')")
public CommonResult<Long> createVoucherTemplate(
@Valid @RequestBody FmsVoucherTemplateSaveReqVO createReqVO) {
return success(voucherTemplateService.createVoucherTemplate(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "修改凭证模板")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-template:update')")
public CommonResult<Boolean> updateVoucherTemplate(
@Valid @RequestBody FmsVoucherTemplateSaveReqVO updateReqVO) {
voucherTemplateService.updateVoucherTemplate(updateReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除凭证模板")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-template:delete')")
public CommonResult<Boolean> deleteVoucherTemplate(@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
voucherTemplateService.deleteVoucherTemplate(accountSetId, id, getLoginUserId());
return success(true);
}
@GetMapping("/list")
@Operation(summary = "获得凭证模板列表")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-template:query')")
public CommonResult<List<FmsVoucherTemplateRespVO>> getVoucherTemplateList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
accountSetService.validateAccountSetReadPermission(accountSetId, getLoginUserId());
return success(buildVoucherTemplateRespVOList(accountSetId));
}
@GetMapping("/simple-list")
@Operation(summary = "获得凭证模板精简列表", description = "主要用于录凭证时套用凭证模板")
public CommonResult<List<FmsVoucherTemplateRespVO>> getVoucherTemplateSimpleList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
accountSetService.validateAccountSetReadPermission(accountSetId, getLoginUserId());
return success(buildVoucherTemplateRespVOList(accountSetId));
}
// ==================== 拼接 VO ====================
private List<FmsVoucherTemplateRespVO> buildVoucherTemplateRespVOList(Long accountSetId) {
Map<Long, FmsVoucherTemplateCategoryDO> categoryMap =
voucherTemplateService.getTemplateCategoryMap(accountSetId);
List<FmsVoucherTemplateDO> templates = voucherTemplateService.getVoucherTemplateList(accountSetId);
return BeanUtils.toBean(templates, FmsVoucherTemplateRespVO.class, template -> {
findAndThen(categoryMap, template.getCategoryId(),
category -> template.setCategoryName(category.getName()));
});
}
}
@@ -0,0 +1,93 @@
package cn.iocoder.yudao.module.fms.controller.admin.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.voucherword.FmsVoucherWordRespVO;
import cn.iocoder.yudao.module.fms.controller.admin.config.vo.voucherword.FmsVoucherWordSaveReqVO;
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsVoucherWordDO;
import cn.iocoder.yudao.module.fms.service.config.FmsVoucherWordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
* FMS 凭证字 Controller
*
* @author 芋道源码
*/
@Tag(name = "管理后台 - FMS 凭证字")
@RestController
@RequestMapping("/fms/config/voucher-word")
@Validated
public class FmsVoucherWordController {
@Resource
private FmsVoucherWordService voucherWordService;
@PostMapping("/create")
@Operation(summary = "创建凭证字")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-word:create')")
public CommonResult<Long> createVoucherWord(@Valid @RequestBody FmsVoucherWordSaveReqVO createReqVO) {
return success(voucherWordService.createVoucherWord(createReqVO, getLoginUserId()));
}
@PutMapping("/update")
@Operation(summary = "修改凭证字")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-word:update')")
public CommonResult<Boolean> updateVoucherWord(@Valid @RequestBody FmsVoucherWordSaveReqVO updateReqVO) {
voucherWordService.updateVoucherWord(updateReqVO, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除凭证字")
@Parameter(name = "id", description = "凭证字编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-word:delete')")
public CommonResult<Boolean> deleteVoucherWord(
@RequestParam("accountSetId") @NotNull Long accountSetId,
@RequestParam("id") @NotNull Long id) {
voucherWordService.deleteVoucherWord(accountSetId, id, getLoginUserId());
return success(true);
}
@GetMapping("/list")
@Operation(summary = "获得凭证字管理列表")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('fms:config:voucher-word:query')")
public CommonResult<List<FmsVoucherWordRespVO>> getVoucherWordList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
List<FmsVoucherWordDO> list = voucherWordService.getVoucherWordList(accountSetId, getLoginUserId());
return success(BeanUtils.toBean(list, FmsVoucherWordRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获得凭证字精简列表")
@Parameter(name = "accountSetId", description = "账套编号", required = true, example = "1024")
public CommonResult<List<FmsVoucherWordRespVO>> getVoucherWordSimpleList(
@RequestParam("accountSetId") @NotNull Long accountSetId) {
return success(convertList(voucherWordService.getVoucherWordList(
accountSetId, getLoginUserId()), voucherWord -> new FmsVoucherWordRespVO()
.setId(voucherWord.getId()).setName(voucherWord.getName())
.setDefaultStatus(voucherWord.getDefaultStatus())));
}
}
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.fms.enums.config.FmsAccountingStandardEnum;
import cn.iocoder.yudao.module.fms.enums.config.FmsCurrencyPresetEnum;
import cn.iocoder.yudao.module.fms.enums.ledger.FmsLedgerBalanceModeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - FMS 账套初始化 Request VO")
@Data
public class FmsAccountSetInitializeReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "本位币编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "RMB")
@NotBlank(message = "本位币不能为空")
@InEnum(value = FmsCurrencyPresetEnum.class, message = "本位币必须是 {value}")
private String currencyCode;
@Schema(description = "启用期间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "启用期间不能为空")
private LocalDateTime startTime;
@Schema(description = "会计制度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "会计制度不能为空")
@InEnum(value = FmsAccountingStandardEnum.class, message = "会计制度必须是 {value}")
private Integer standard;
@Schema(description = "科目层级", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
@NotNull(message = "科目层级不能为空")
@Min(value = 1, message = "科目层级不能小于 1")
@Max(value = 8, message = "科目层级不能大于 8")
private Integer level;
@Schema(description = "科目编码规则", requiredMode = Schema.RequiredMode.REQUIRED, example = "4-2-2-2")
@NotBlank(message = "科目编码规则不能为空")
@Size(max = 64, message = "科目编码规则不能超过 64 个字符")
private String subjectCodeRule;
@Schema(description = "账簿余额方向模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "账簿余额方向模式不能为空")
@InEnum(FmsLedgerBalanceModeEnum.class)
private Integer ledgerBalanceMode;
}
@@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - FMS 账套 Response VO")
@Data
public class FmsAccountSetRespVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "公司编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "WK001")
private String companyCode;
@Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试科技有限公司")
private String companyName;
@Schema(description = "公司简介", example = "专注企业数字化服务")
private String companyProfile;
@Schema(description = "所在行业", example = "软件和信息技术服务业")
private String industry;
@Schema(description = "所在地", example = "上海市浦东新区")
private String location;
@Schema(description = "法人代表", example = "张三")
private String legalRepresentative;
@Schema(description = "法人身份证号", example = "310101199001011234")
private String legalRepresentativeIdNumber;
@Schema(description = "营业执照号", example = "91310000MA1K123456")
private String businessLicenseNumber;
@Schema(description = "组织机构代码", example = "MA1K12345")
private String organizationCode;
@Schema(description = "备注", example = "一般纳税人")
private String remark;
@Schema(description = "联系人", example = "李四")
private String contactName;
@Schema(description = "办公电话", example = "021-12345678")
private String officeTelephone;
@Schema(description = "手机号码", example = "15601691399")
private String mobile;
@Schema(description = "传真号码", example = "021-87654321")
private String faxNumber;
@Schema(description = "QQ 号码", example = "12345678")
private String qqNumber;
@Schema(description = "邮箱", example = "finance@example.com")
private String email;
@Schema(description = "其他联系方式", example = "企业微信:wk-finance")
private String otherContact;
@Schema(description = "详细地址", example = "张江高科技园区 1 号楼")
private String address;
@Schema(description = "币种编号", example = "1024")
private Long currencyId;
@Schema(description = "启用期间")
private LocalDateTime startTime;
@Schema(description = "会计制度", example = "1")
private Integer standard;
@Schema(description = "是否已初始化", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean initialized;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
// ==================== 来自 FmsAccountUserDO ====================
@Schema(description = "是否默认账套", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean defaultStatus;
@Schema(description = "是否账套创建人", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean founder;
@Schema(description = "当前用户的成员权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
private Integer level;
}
@@ -0,0 +1,110 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountset;
import com.mzt.logapi.starter.annotation.DiffLogField;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - FMS 账套新增/修改 Request VO")
@Data
public class FmsAccountSetSaveReqVO {
@Schema(description = "账套编号", example = "1024")
private Long id;
@Schema(description = "公司编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "WK001")
@NotBlank(message = "公司编码不能为空")
@Size(max = 64, message = "公司编码不能超过 64 个字符")
@DiffLogField(name = "公司编码")
private String companyCode;
@Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试科技有限公司")
@NotBlank(message = "公司名称不能为空")
@Size(max = 255, message = "公司名称不能超过 255 个字符")
@DiffLogField(name = "公司名称")
private String companyName;
@Schema(description = "公司简介", example = "专注企业数字化服务")
@Size(max = 500, message = "公司简介不能超过 500 个字符")
@DiffLogField(name = "公司简介")
private String companyProfile;
@Schema(description = "所在行业", example = "软件和信息技术服务业")
@Size(max = 255, message = "所在行业不能超过 255 个字符")
@DiffLogField(name = "所在行业")
private String industry;
@Schema(description = "所在地", example = "上海市浦东新区")
@Size(max = 255, message = "所在地不能超过 255 个字符")
@DiffLogField(name = "所在地")
private String location;
@Schema(description = "法人代表", example = "张三")
@Size(max = 255, message = "法人代表不能超过 255 个字符")
@DiffLogField(name = "法人代表")
private String legalRepresentative;
@Schema(description = "法人身份证号", example = "310101199001011234")
@Size(max = 255, message = "法人身份证号不能超过 255 个字符")
@DiffLogField(name = "法人身份证号")
private String legalRepresentativeIdNumber;
@Schema(description = "营业执照号", example = "91310000MA1K123456")
@Size(max = 255, message = "营业执照号不能超过 255 个字符")
@DiffLogField(name = "营业执照号")
private String businessLicenseNumber;
@Schema(description = "组织机构代码", example = "MA1K12345")
@Size(max = 255, message = "组织机构代码不能超过 255 个字符")
@DiffLogField(name = "组织机构代码")
private String organizationCode;
@Schema(description = "备注", example = "一般纳税人")
@Size(max = 500, message = "备注不能超过 500 个字符")
@DiffLogField(name = "备注")
private String remark;
@Schema(description = "联系人", example = "李四")
@Size(max = 255, message = "联系人不能超过 255 个字符")
@DiffLogField(name = "联系人")
private String contactName;
@Schema(description = "办公电话", example = "021-12345678")
@Size(max = 32, message = "办公电话不能超过 32 个字符")
@DiffLogField(name = "办公电话")
private String officeTelephone;
@Schema(description = "手机号码", example = "15601691399")
@Size(max = 32, message = "手机号码不能超过 32 个字符")
@DiffLogField(name = "手机号码")
private String mobile;
@Schema(description = "传真号码", example = "021-87654321")
@Size(max = 32, message = "传真号码不能超过 32 个字符")
@DiffLogField(name = "传真号码")
private String faxNumber;
@Schema(description = "QQ 号码", example = "12345678")
@Size(max = 255, message = "QQ 号码不能超过 255 个字符")
@DiffLogField(name = "QQ 号码")
private String qqNumber;
@Schema(description = "邮箱", example = "finance@example.com")
@Email(message = "邮箱格式不正确")
@Size(max = 255, message = "邮箱不能超过 255 个字符")
@DiffLogField(name = "邮箱")
private String email;
@Schema(description = "其他联系方式", example = "企业微信:wk-finance")
@Size(max = 255, message = "其他联系方式不能超过 255 个字符")
@DiffLogField(name = "其他联系方式")
private String otherContact;
@Schema(description = "详细地址", example = "张江高科技园区 1 号楼")
@Size(max = 255, message = "详细地址不能超过 255 个字符")
@DiffLogField(name = "详细地址")
private String address;
}
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - FMS 账套用户 Response VO")
@Data
public class FmsAccountUserRespVO {
@Schema(description = "后台用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long userId;
@Schema(description = "用户昵称", example = "芋道")
private String nickname;
@Schema(description = "部门名称", example = "财务部")
private String deptName;
@Schema(description = "手机号码", example = "15601691399")
private String mobile;
@Schema(description = "用户邮箱", example = "finance@example.com")
private String email;
@Schema(description = "用户状态", example = "0")
private Integer status;
@Schema(description = "是否默认账套", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean defaultStatus;
@Schema(description = "是否账套创建人", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean founder;
@Schema(description = "成员权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer level;
}
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.accountuser;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.fms.enums.config.FmsAccountUserLevelEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - FMS 账套用户更新 Request VO")
@Data
public class FmsAccountUserUpdateReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "账套成员数组", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "账套成员数组不能为空")
@Size(max = 100, message = "单个账套最多授权 100 名成员")
@Valid
private List<Member> members;
@Schema(description = "管理后台 - FMS 账套成员")
@Data
public static class Member {
@Schema(description = "后台用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "成员用户编号不能为空")
private Long userId;
@Schema(description = "成员权限级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotNull(message = "成员权限级别不能为空")
@InEnum(FmsAccountUserLevelEnum.class)
private Integer level;
}
}
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem;
import cn.idev.excel.annotation.ExcelProperty;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* FMS 辅助核算项目 Excel 导入 VO
*
* @author 芋道源码
*/
@Data
public class FmsAuxiliaryItemImportExcelVO {
@ExcelProperty("编码")
@NotBlank(message = "编码不能为空")
@Size(max = 64, message = "编码长度不能超过 64 个字符")
private String code;
@ExcelProperty("名称")
@NotBlank(message = "名称不能为空")
@Size(max = 255, message = "名称长度不能超过 255 个字符")
private String name;
@ExcelProperty("备注")
@Size(max = 500, message = "备注长度不能超过 500 个字符")
private String remark;
@ExcelProperty("规格")
@Size(max = 255, message = "规格长度不能超过 255 个字符")
private String specification;
@ExcelProperty("单位")
@Size(max = 255, message = "单位长度不能超过 255 个字符")
private String unit;
}
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* FMS 辅助核算项目导入结果 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 辅助核算项目导入结果 Response VO")
@Data
public class FmsAuxiliaryItemImportRespVO {
@Schema(description = "总数量", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer totalCount;
@Schema(description = "成功编码数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> successItemCodes;
@Schema(description = "失败原因 Map", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, String> failureReasons;
}
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* FMS 辅助核算项目分页 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 辅助核算项目分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class FmsAuxiliaryItemPageReqVO extends PageParam {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "辅助核算类别编号不能为空")
private Long auxiliaryTypeId;
@Schema(description = "关键词,匹配编码或名称", example = "测试")
private String search;
}
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* FMS 辅助核算项目 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 辅助核算项目 Response VO")
@Data
@ExcelIgnoreUnannotated
public class FmsAuxiliaryItemRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long accountSetId;
@Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long auxiliaryTypeId;
@Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "KH001")
@ExcelProperty("编码")
private String code;
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海测试数字科技有限公司")
@ExcelProperty("名称")
private String name;
@Schema(description = "备注", example = "重点客户")
@ExcelProperty("备注")
private String remark;
@Schema(description = "规格", example = "标准版")
@ExcelProperty("规格")
private String specification;
@Schema(description = "单位", example = "台")
@ExcelProperty("单位")
private String unit;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* FMS 辅助核算项目创建或修改 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 辅助核算项目创建或修改 Request VO")
@Data
public class FmsAuxiliaryItemSaveReqVO {
@Schema(description = "编号", example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "辅助核算类别编号不能为空")
private Long auxiliaryTypeId;
@Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "KH001")
@NotBlank(message = "编码不能为空")
@Size(max = 64, message = "编码长度不能超过 64 个字符")
private String code;
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海测试数字科技有限公司")
@NotBlank(message = "名称不能为空")
@Size(max = 255, message = "名称长度不能超过 255 个字符")
private String name;
@Schema(description = "备注", example = "重点客户")
@Size(max = 500, message = "备注长度不能超过 500 个字符")
private String remark;
@Schema(description = "规格", example = "标准版")
@Size(max = 255, message = "规格长度不能超过 255 个字符")
private String specification;
@Schema(description = "单位", example = "台")
@Size(max = 255, message = "单位长度不能超过 255 个字符")
private String unit;
}
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* FMS 辅助核算项目状态更新 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 辅助核算项目状态更新 Request VO")
@Data
public class FmsAuxiliaryItemStatusReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "编号不能为空")
private Long id;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
@InEnum(CommonStatusEnum.class)
private Integer status;
}
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* FMS 辅助核算类别 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 辅助核算类别 Response VO")
@Data
public class FmsAuxiliaryTypeRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户")
private String name;
@Schema(description = "是否系统预置", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean systemPreset;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long accountSetId;
@Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer type;
}
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliarytype;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* FMS 辅助核算类别创建或修改 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 辅助核算类别创建或修改 Request VO")
@Data
public class FmsAuxiliaryTypeSaveReqVO {
@Schema(description = "编号", example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "门店")
@NotBlank(message = "名称不能为空")
@Size(max = 255, message = "名称长度不能超过 255 个字符")
private String name;
}
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* FMS 币别 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 币别 Response VO")
@Data
public class FmsCurrencyRespVO {
@Schema(description = "币别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long accountSetId;
@Schema(description = "币别编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "USD")
private String code;
@Schema(description = "币别名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "美元")
private String name;
@Schema(description = "汇率", requiredMode = Schema.RequiredMode.REQUIRED, example = "7.120000")
private BigDecimal exchangeRate;
@Schema(description = "是否本位币", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean standard;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.currency;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
/**
* FMS 币别保存 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 币别保存 Request VO")
@Data
public class FmsCurrencySaveReqVO {
@Schema(description = "币别编号", example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套不能为空")
private Long accountSetId;
@Schema(description = "币别编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "USD")
@NotBlank(message = "币别编码不能为空")
@Size(max = 64, message = "币别编码长度不能超过 64 个字符")
@Pattern(regexp = "^[A-Za-z][A-Za-z0-9_]*$", message = "币别编码必须以字母开头,只能包含字母、数字和下划线")
private String code;
@Schema(description = "币别名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "美元")
@NotBlank(message = "币别名称不能为空")
@Size(max = 255, message = "币别名称长度不能超过 255 个字符")
private String name;
@Schema(description = "汇率", requiredMode = Schema.RequiredMode.REQUIRED, example = "7.120000")
@NotNull(message = "汇率不能为空")
@DecimalMin(value = "0", inclusive = false, message = "汇率必须大于 0")
@Digits(integer = 12, fraction = 6, message = "汇率最多 12 位整数和 6 位小数")
private BigDecimal exchangeRate;
}
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* FMS 常用摘要 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 常用摘要 Response VO")
@Data
public class FmsDigestRespVO {
@Schema(description = "常用摘要编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long accountSetId;
@Schema(description = "摘要内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "购买办公用品")
private String content;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.digest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* FMS 常用摘要保存 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 常用摘要保存 Request VO")
@Data
public class FmsDigestSaveReqVO {
@Schema(description = "常用摘要编号", example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "摘要内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "购买办公用品")
@NotBlank(message = "摘要内容不能为空")
@Size(max = 500, message = "摘要内容长度不能超过 500 个字符")
private String content;
}
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeindicator;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
// TODO DONE @AI:字段 Swagger 注解和空行已补齐。
@Schema(description = "管理后台 - FMS 财务指标 Response VO")
@Data
public class FmsFinanceIndicatorRespVO {
@Schema(description = "指标编号", example = "1024")
private Long id;
@Schema(description = "账套编号", example = "1")
private Long accountSetId;
@Schema(description = "指标名称", example = "现金")
private String name;
@Schema(description = "指标编码", example = "cash")
private String code;
@Schema(description = "取数报表类型", example = "1")
private Integer type;
@Schema(description = "指标公式")
private String formula;
@Schema(description = "展示顺序", example = "10")
private Integer sort;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}
@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeindicator;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.fms.enums.config.FmsFinanceIndicatorTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
// TODO DONE @AI:字段空行已对齐现有 SaveReqVO。
@Schema(description = "管理后台 - FMS 财务指标保存 Request VO")
@Data
public class FmsFinanceIndicatorSaveReqVO {
@Schema(description = "指标编号", example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "指标名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "现金")
@NotBlank(message = "指标名称不能为空")
@Size(max = 100, message = "指标名称长度不能超过 100 个字符")
private String name;
@Schema(description = "指标编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "cash")
@NotBlank(message = "指标编码不能为空")
@Size(max = 64, message = "指标编码长度不能超过 64 个字符")
private String code;
@Schema(description = "取数报表类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotNull(message = "取数报表类型不能为空")
@InEnum(FmsFinanceIndicatorTypeEnum.class)
private Integer type;
@Schema(description = "指标公式,支持 L1+L2 或科目公式 JSON", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "指标公式不能为空")
@Size(max = 2000, message = "指标公式长度不能超过 2000 个字符")
private String formula;
@Schema(description = "展示顺序", example = "10")
@NotNull(message = "展示顺序不能为空")
private Integer sort;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
@InEnum(CommonStatusEnum.class)
private Integer status;
}
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - FMS 财务参数 Response VO")
@Data
public class FmsFinanceParameterRespVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long accountSetId;
@Schema(description = "科目层级", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
private Integer level;
@Schema(description = "科目编码规则", requiredMode = Schema.RequiredMode.REQUIRED, example = "4-2-2-2")
private String subjectCodeRule;
@Schema(description = "账簿余额方向模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer ledgerBalanceMode;
@Schema(description = "结账前是否要求凭证审核", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean voucherReviewRequired;
}
@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.financeparameter;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.fms.enums.ledger.FmsLedgerBalanceModeEnum;
import cn.iocoder.yudao.module.fms.enums.config.FmsAccountingStandardEnum;
import com.mzt.logapi.starter.annotation.DiffLogField;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - FMS 财务参数更新 Request VO")
@Data
public class FmsFinanceParameterUpdateReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "会计制度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "会计制度不能为空")
@InEnum(FmsAccountingStandardEnum.class)
@DiffLogField(name = "会计制度")
private Integer standard;
@Schema(description = "科目层级", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
@NotNull(message = "科目层级不能为空")
@Min(value = 1, message = "科目层级不能小于 1")
@Max(value = 8, message = "科目层级不能大于 8")
@DiffLogField(name = "科目层级")
private Integer level;
@Schema(description = "科目编码规则", requiredMode = Schema.RequiredMode.REQUIRED, example = "4-2-2-2")
@NotBlank(message = "科目编码规则不能为空")
@Size(max = 64, message = "科目编码规则不能超过 64 个字符")
@DiffLogField(name = "科目编码规则")
private String subjectCodeRule;
@Schema(description = "账簿余额方向模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "账簿余额方向模式不能为空")
@InEnum(FmsLedgerBalanceModeEnum.class)
@DiffLogField(name = "账簿余额方向")
private Integer ledgerBalanceMode;
@Schema(description = "结账前是否要求凭证审核", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "结账前凭证审核设置不能为空")
@DiffLogField(name = "凭证审核后才允许结账")
private Boolean voucherReviewRequired;
}
@@ -0,0 +1,105 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
* FMS 初始余额 Excel VO
*
* @author 芋道源码
*/
@Data
@ExcelIgnoreUnannotated
public class FmsInitialBalanceExcelVO {
/**
* Excel 行号
*/
private Integer rowNumber;
/**
* 科目编码
*/
@ExcelProperty("科目编码")
private String subjectCode;
/**
* 科目名称
*/
@ExcelProperty("科目名称")
private String subjectName;
/**
* 余额方向名称
*/
@ExcelProperty("方向")
private String directionName;
/**
* 辅助核算项目,格式为“类别:名称/类别:名称”
*/
@ExcelProperty("辅助核算项目")
private String auxiliaryItems;
/**
* 科目类型
*/
private Integer subjectType;
/**
* 是否启用数量核算
*/
private Boolean quantityAccounting;
/**
* 是否启用辅助核算
*/
private Boolean auxiliaryAccounting;
/**
* 期初数量
*/
@ExcelProperty("期初数量")
private BigDecimal openingQuantity;
/**
* 期初金额
*/
@ExcelProperty("期初金额")
private BigDecimal openingAmount;
/**
* 本年累计借方数量
*/
@ExcelProperty("本年累计借方数量")
private BigDecimal yearDebitQuantity;
/**
* 本年累计借方金额
*/
@ExcelProperty("本年累计借方金额")
private BigDecimal yearDebitAmount;
/**
* 本年累计贷方数量
*/
@ExcelProperty("本年累计贷方数量")
private BigDecimal yearCreditQuantity;
/**
* 本年累计贷方金额
*/
@ExcelProperty("本年累计贷方金额")
private BigDecimal yearCreditAmount;
/**
* 年初数量
*/
@ExcelProperty("年初数量")
private BigDecimal yearOpeningQuantity;
/**
* 年初金额
*/
@ExcelProperty("年初金额")
private BigDecimal yearOpeningAmount;
/**
* 实际损益发生数量
*/
@ExcelProperty("实际损益发生数量")
private BigDecimal profitLossQuantity;
/**
* 实际损益发生金额
*/
@ExcelProperty("实际损益发生金额")
private BigDecimal profitLossAmount;
}
@@ -0,0 +1,154 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - FMS 初始余额 Response VO")
@Data
public class FmsInitialBalanceRespVO {
@Schema(description = "初始余额编号", example = "1024")
private Long id;
@Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long subjectId;
@Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED)
private String subjectCode;
@Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String subjectName;
@Schema(description = "上级科目编号", example = "1024")
private Long parentId;
@Schema(description = "科目类型", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer type;
@Schema(description = "余额方向", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer balanceDirection;
@Schema(description = "是否启用数量核算", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean quantityAccounting;
@Schema(description = "数量单位")
private String quantityUnit;
@Schema(description = "是否启用辅助核算", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean auxiliaryAccounting;
@Schema(description = "期初金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00")
private BigDecimal openingAmount;
@Schema(description = "期初数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.0000")
private BigDecimal openingQuantity;
@Schema(description = "本年累计借方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
private BigDecimal yearDebitAmount;
@Schema(description = "本年累计借方数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000")
private BigDecimal yearDebitQuantity;
@Schema(description = "本年累计贷方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "50.00")
private BigDecimal yearCreditAmount;
@Schema(description = "本年累计贷方数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000")
private BigDecimal yearCreditQuantity;
@Schema(description = "年初金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "950.00")
private BigDecimal yearOpeningAmount;
@Schema(description = "年初数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.0000")
private BigDecimal yearOpeningQuantity;
@Schema(description = "实际损益发生额", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.00")
private BigDecimal profitLossAmount;
@Schema(description = "实际损益发生数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000")
private BigDecimal profitLossQuantity;
@Schema(description = "辅助核算配置数组")
private List<AuxiliaryConfig> auxiliaryConfigs;
@Schema(description = "辅助核算余额数组")
private List<AssistBalance> assistBalances;
@Schema(description = "管理后台 - FMS 初始余额辅助核算配置")
@Data
public static class AuxiliaryConfig {
@Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long auxiliaryTypeId;
@Schema(description = "辅助核算类型", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer type;
@Schema(description = "辅助核算类别名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String name;
}
@Schema(description = "管理后台 - FMS 初始余额辅助核算余额")
@Data
public static class AssistBalance {
@Schema(description = "辅助核算组合编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long assistCombinationId;
@Schema(description = "辅助核算项目数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<AuxiliaryItem> auxiliaries;
@Schema(description = "期初金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00")
private BigDecimal openingAmount;
@Schema(description = "期初数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.0000")
private BigDecimal openingQuantity;
@Schema(description = "本年累计借方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
private BigDecimal yearDebitAmount;
@Schema(description = "本年累计借方数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000")
private BigDecimal yearDebitQuantity;
@Schema(description = "本年累计贷方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "50.00")
private BigDecimal yearCreditAmount;
@Schema(description = "本年累计贷方数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000")
private BigDecimal yearCreditQuantity;
@Schema(description = "年初金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "950.00")
private BigDecimal yearOpeningAmount;
@Schema(description = "年初数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.0000")
private BigDecimal yearOpeningQuantity;
@Schema(description = "实际损益发生额", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.00")
private BigDecimal profitLossAmount;
@Schema(description = "实际损益发生数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0000")
private BigDecimal profitLossQuantity;
}
@Schema(description = "管理后台 - FMS 初始余额辅助核算项目")
@Data
public static class AuxiliaryItem {
@Schema(description = "辅助核算类型", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer type;
@Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long typeId;
@Schema(description = "辅助核算项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long itemId;
@Schema(description = "辅助核算项目名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String name;
}
}
@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - FMS 初始余额保存 Request VO")
@Data
public class FmsInitialBalanceSaveReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "科目余额数组", requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
@NotEmpty(message = "科目余额不能为空")
private List<Balance> balances;
@Data
public static class Balance extends Amounts {
@Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "科目编号不能为空")
private Long subjectId;
@Schema(description = "辅助核算余额数组")
@Valid
private List<AssistBalance> assistBalances;
}
@Data
public static class AssistBalance extends Amounts {
@Schema(description = "辅助核算项目编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "辅助核算项目不能为空")
private List<Long> auxiliaryItemIds;
}
@Data
public static class Amounts {
@Schema(description = "期初金额", example = "1000.00")
@DecimalMin(value = "0", message = "期初金额不能小于 0")
private BigDecimal openingAmount;
@Schema(description = "期初数量", example = "10.0000")
@DecimalMin(value = "0", message = "期初数量不能小于 0")
private BigDecimal openingQuantity;
@Schema(description = "本年累计借方金额", example = "100.00")
@DecimalMin(value = "0", message = "本年累计借方金额不能小于 0")
private BigDecimal yearDebitAmount;
@Schema(description = "本年累计借方数量", example = "1.0000")
@DecimalMin(value = "0", message = "本年累计借方数量不能小于 0")
private BigDecimal yearDebitQuantity;
@Schema(description = "本年累计贷方金额", example = "50.00")
@DecimalMin(value = "0", message = "本年累计贷方金额不能小于 0")
private BigDecimal yearCreditAmount;
@Schema(description = "本年累计贷方数量", example = "1.0000")
@DecimalMin(value = "0", message = "本年累计贷方数量不能小于 0")
private BigDecimal yearCreditQuantity;
@Schema(description = "年初金额", example = "950.00")
@DecimalMin(value = "0", message = "年初金额不能小于 0")
private BigDecimal yearOpeningAmount;
@Schema(description = "年初数量", example = "10.0000")
@DecimalMin(value = "0", message = "年初数量不能小于 0")
private BigDecimal yearOpeningQuantity;
@Schema(description = "实际损益发生额", example = "0.00")
@DecimalMin(value = "0", message = "实际损益发生额不能小于 0")
private BigDecimal profitLossAmount;
@Schema(description = "实际损益发生数量", example = "1.0000")
@DecimalMin(value = "0", message = "实际损益发生数量不能小于 0")
private BigDecimal profitLossQuantity;
}
}
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - FMS 试算平衡 Response VO")
@Data
public class FmsTrialBalanceRespVO {
@Schema(description = "期初借方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00")
private BigDecimal openingDebitAmount;
@Schema(description = "期初贷方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00")
private BigDecimal openingCreditAmount;
@Schema(description = "期初差额", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.00")
private BigDecimal openingDifferenceAmount;
@Schema(description = "本年累计借方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
private BigDecimal yearDebitAmount;
@Schema(description = "本年累计贷方金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
private BigDecimal yearCreditAmount;
@Schema(description = "本年累计差额", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.00")
private BigDecimal yearDifferenceAmount;
@Schema(description = "是否平衡", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean balanced;
}
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* FMS 科目删除 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 科目删除 Request VO")
@Data
public class FmsSubjectDeleteReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "科目编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "科目编号数组不能为空")
private List<Long> ids;
}
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject;
import cn.idev.excel.annotation.ExcelProperty;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* FMS 科目 Excel 导入 VO
*
* @author 芋道源码
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class FmsSubjectImportExcelVO {
@ExcelProperty("科目编码")
@NotBlank(message = "科目编码不能为空")
private String code;
@ExcelProperty("科目名称")
@NotBlank(message = "科目名称不能为空")
private String name;
@ExcelProperty("上级科目编码")
private String parentSubjectCode;
@ExcelProperty("余额方向")
@NotBlank(message = "余额方向不能为空")
private String balanceDirection;
@ExcelProperty("类别")
@NotBlank(message = "类别不能为空")
private String categoryName;
@ExcelProperty("辅助核算")
private String auxiliaryNames;
}
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - FMS 科目导入结果 Response VO")
@Data
public class FmsSubjectImportRespVO {
@Schema(description = "总数量", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer totalCount;
@Schema(description = "成功科目编码数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> successSubjectCodes;
@Schema(description = "失败原因 Map", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, String> failureReasons;
}
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* FMS 科目列表 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 科目列表 Request VO")
@Data
public class FmsSubjectListReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "科目类型", example = "1")
@InEnum(FmsSubjectTypeEnum.class)
private Integer type;
}
@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.framework.excel.core.convert.StringListConvert;
import cn.iocoder.yudao.module.fms.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.module.infra.enums.DictTypeConstants.BOOLEAN_STRING;
/**
* FMS 科目 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 科目 Response VO")
@Data
@ExcelIgnoreUnannotated
public class FmsSubjectRespVO {
@Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long accountSetId;
@Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
@ExcelProperty(value = "编码", index = 0)
private String code;
@Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "库存现金")
@ExcelProperty(value = "名称", index = 1)
private String name;
@Schema(description = "上级科目编号", example = "1024")
private Long parentId;
@Schema(description = "科目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer type;
@Schema(description = "科目类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer category;
@JsonIgnore // 仅 excel 导出使用
@ExcelProperty(value = "类别", index = 2, converter = DictConvert.class)
@DictFormat(DictTypeConstants.SUBJECT_CATEGORY)
private String categoryDictValue;
@Schema(description = "余额方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty(value = "余额方向", index = 3, converter = DictConvert.class)
@DictFormat(DictTypeConstants.DEBIT_CREDIT_DIRECTION)
private Integer balanceDirection;
@Schema(description = "辅助核算类别编号数组")
private List<Long> auxiliaryTypeIds;
@Schema(description = "外币核算币别编号数组")
private List<Long> currencyIds;
@Schema(description = "是否启用数量核算", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean quantityAccounting;
@Schema(description = "数量单位", example = "件")
@ExcelProperty(value = "数量", index = 5)
private String quantityUnit;
@Schema(description = "是否现金及现金等价物", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty(value = "是否现金项", index = 6, converter = DictConvert.class)
@DictFormat(BOOLEAN_STRING)
private Boolean cash;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
@Schema(description = "层级", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer level;
@Schema(description = "辅助核算类别名称数组")
@ExcelProperty(value = "辅助核算", index = 4, converter = StringListConvert.class)
private List<String> auxiliaryTypeNames;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}
@@ -0,0 +1,113 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.fms.enums.common.FmsDebitCreditDirectionEnum;
import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectCategoryEnum;
import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* FMS 科目保存 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 科目保存 Request VO")
@Data
public class FmsSubjectSaveReqVO {
@Schema(description = "科目编号", example = "1024")
private Long id;
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "科目编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
@NotBlank(message = "科目编码不能为空")
@Size(max = 64, message = "科目编码长度不能超过 64 个字符")
private String code;
@Schema(description = "科目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "库存现金")
@NotBlank(message = "科目名称不能为空")
@Size(max = 255, message = "科目名称长度不能超过 255 个字符")
private String name;
@Schema(description = "上级科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "上级科目编号不能为空")
private Long parentId;
@Schema(description = "科目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "科目类型不能为空")
@InEnum(FmsSubjectTypeEnum.class)
private Integer type;
@Schema(description = "科目类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "科目类别不能为空")
private Integer category;
@Schema(description = "余额方向", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "余额方向不能为空")
@InEnum(FmsDebitCreditDirectionEnum.class)
private Integer balanceDirection;
@Schema(description = "辅助核算类别编号数组")
@NotNull(message = "辅助核算类别编号数组不能为空")
private List<Long> auxiliaryTypeIds;
@Schema(description = "外币核算币别编号数组")
@NotNull(message = "外币核算币别编号数组不能为空")
private List<Long> currencyIds;
@Schema(description = "是否启用数量核算", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "是否启用数量核算不能为空")
private Boolean quantityAccounting;
@Schema(description = "数量单位", example = "件")
@Size(max = 255, message = "数量单位长度不能超过 255 个字符")
private String quantityUnit;
@Schema(description = "是否现金及现金等价物", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "是否现金及现金等价物不能为空")
private Boolean cash;
@Schema(description = "是否迁移上级科目历史数据")
private Boolean migrateParentData;
@Schema(description = "辅助核算历史数据迁移项目数组")
private List<AuxiliaryMapping> auxiliaryMappings;
@AssertTrue(message = "科目类别与科目类型不匹配")
public boolean isCategoryValid() {
return type == null || category == null || FmsSubjectCategoryEnum.valueOf(type, category) != null;
}
@AssertTrue(message = "启用数量核算时数量单位不能为空")
public boolean isQuantityUnitValid() {
return !Boolean.TRUE.equals(quantityAccounting) || StrUtil.isNotBlank(quantityUnit);
}
/**
* 辅助核算历史数据迁移项目
*/
@Data
public static class AuxiliaryMapping {
@Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "辅助核算类别编号不能为空")
private Long typeId;
@Schema(description = "辅助核算项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "辅助核算项目编号不能为空")
private Long itemId;
}
}
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* FMS 科目状态更新 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 科目状态更新 Request VO")
@Data
public class FmsSubjectStatusReqVO {
@Schema(description = "账套编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "账套编号不能为空")
private Long accountSetId;
@Schema(description = "科目编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "科目编号数组不能为空")
private List<Long> ids;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "状态不能为空")
@InEnum(CommonStatusEnum.class)
private Integer status;
}
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* FMS 科目使用情况 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - FMS 科目使用情况 Response VO")
@Data
public class FmsSubjectUsageRespVO {
@Schema(description = "下级科目数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Long childCount;
@Schema(description = "凭证分录数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Long voucherEntryCount;
@Schema(description = "初始余额数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long initialBalanceCount;
@Schema(description = "辅助核算组合数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
private Long auxiliaryCombinationCount;
@Schema(description = "包含数量数据的记录数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long quantityDataCount;
@Schema(description = "是否已被业务使用", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean used;
}
@@ -0,0 +1,69 @@
package cn.iocoder.yudao.module.fms.controller.admin.config.vo.vouchertemplate;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - FMS 凭证模板分录 VO")
@Data
public class FmsVoucherTemplateEntryVO {
@Schema(description = "摘要内容", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "摘要不能为空")
@Size(max = 500, message = "摘要长度不能超过 500 个字符")
private String digest;
@Schema(description = "科目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "科目不能为空")
private Long subjectId;
@Schema(description = "数量")
@DecimalMin(value = "0", message = "数量不能小于 0")
@Digits(integer = 14, fraction = 4, message = "数量最多保留 4 位小数")
private BigDecimal quantity;
@Schema(description = "单价")
@DecimalMin(value = "0", message = "单价不能小于 0")
@Digits(integer = 12, fraction = 6, message = "单价最多保留 6 位小数")
private BigDecimal unitPrice;
@Schema(description = "借方金额")
@Digits(integer = 16, fraction = 2, message = "借方金额最多保留 2 位小数")
private BigDecimal debitAmount;
@Schema(description = "贷方金额")
@Digits(integer = 16, fraction = 2, message = "贷方金额最多保留 2 位小数")
private BigDecimal creditAmount;
@Schema(description = "辅助核算项目数组")
@Valid
private List<AuxiliaryItem> auxiliaries;
@Schema(description = "管理后台 - FMS 凭证模板分录辅助核算项目")
@Data
public static class AuxiliaryItem {
@Schema(description = "辅助核算类型")
private Integer type;
@Schema(description = "辅助核算类别编号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "辅助核算类别不能为空")
private Long typeId;
@Schema(description = "辅助核算项目编号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "辅助核算项目不能为空")
private Long itemId;
@Schema(description = "辅助核算项目名称")
private String name;
}
}

Some files were not shown because too many files have changed in this diff Show More