# Conflicts:
#	yudao-module-ai/pom.xml
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java
#	yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java
#	yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java
#	yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java
#	yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java
#	yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java
#	yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java
#	yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java
#	yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java
#	yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java
#	yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/DictTypeConstants.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmCommentTypeEnum.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsFunnelController.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsPerformanceController.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsFunnelReqVO.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsCustomerMapper.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsFunnelMapper.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsPerformanceMapper.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerServiceImpl.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelService.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelServiceImpl.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceService.java
#	yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java
#	yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml
#	yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
#	yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImChannelMessageManagerController.java
#	yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/rtc/core/LiveKitClient.java
#	yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageService.java
#	yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageServiceImpl.java
#	yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java
#	yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineUniappTest.java
#	yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index
#	yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index
#	yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index
#	yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/search-form
#	yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/coupon/AppCouponTemplateController.java
#	yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java
#	yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/checkrecord/MesDvCheckRecordLineController.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/maintenrecord/MesDvMaintenRecordLineController.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/repair/MesDvRepairLineController.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/result/MesWmStockTakingTaskResultRespVO.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/repair/MesDvRepairLineService.java
#	yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/repair/MesDvRepairLineServiceImpl.java
#	yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceTest.java
#	yudao-module-report/src/main/java/cn/iocoder/yudao/module/report/framework/jmreport/core/service/JmOnlDragExternalServiceImpl.java
#	yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java
#	yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java
This commit is contained in:
YunaiV
2026-07-31 21:24:08 +08:00
193 changed files with 14603 additions and 455 deletions
+1 -1
View File
@@ -36,7 +36,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.06-jdk8-SNAPSHOT</revision>
<revision>2026.07-jdk8-SNAPSHOT</revision>
<!-- Maven 相关 -->
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
+1 -1
View File
@@ -15,7 +15,7 @@ bash verify.sh
- 7880:HTTP / WebSocket 信令;
- 7881:WebRTC TCP fallback;
- 7882/UDP:WebRTC 媒体;
- macOS / Windows:当前 `docker-compose.yml` 走端口映射模式,webhook URL 用 `host.docker.internal:48080` 让容器访问到宿主机 yudao 后端;
- macOS / Windows:当前 `docker-compose.yml` 走端口映射模式,`rtc.node_ip` 使用 `127.0.0.1` 让浏览器访问映射后的媒体端口,webhook URL 用 `host.docker.internal:48080` 让容器访问到宿主机 yudao 后端;
- macOS 上 host network(`network_mode: host`)需要 Docker Desktop 4.34+ 并在 Settings → Resources → Network 勾选「Enable host networking」,老版本静默失败(容器跑得起来但端口完全不通);
- Linux:可以把 `docker-compose.yml` 改成 `network_mode: host` + 删 `ports:` 段,并把 `livekit.yaml` 的 webhook URL 改为 `http://127.0.0.1:48080/admin-api/im/livekit/webhook`。
+2
View File
@@ -10,6 +10,8 @@ rtc:
tcp_port: 7881
udp_port: 7882
use_external_ip: false
# macOS / Windows 端口映射模式下显式广播宿主机回环地址,避免把容器 172.x 地址发给浏览器
node_ip: 127.0.0.1
# Webhook:成员离开 / 房间结束等事件回调到 yudao 后端做业务态兜底清理
# host.docker.internal 让容器访问宿主机 macOS / Windows 上的 yudao 后端
+13 -2
View File
@@ -462,7 +462,7 @@ CREATE TABLE `system_dict_data` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1061137 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
) ENGINE = InnoDB AUTO_INCREMENT = 1061147 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
-- ----------------------------
-- Records of system_dict_data
@@ -814,6 +814,16 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
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 (1512, 6, '委派中', '6', 'bpm_task_status', 0, 'primary', '', '', '1', '2024-03-17 10:06:22', '1', '2024-03-08 22:41:40', b'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 (1513, 7, '审批通过中', '7', 'bpm_task_status', 0, 'success', '', '', '1', '2024-03-17 10:06:47', '1', '2024-03-08 22:41:41', b'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 (1514, 0, '待审批', '0', 'bpm_task_status', 0, 'info', '', '', '1', '2024-03-17 10:07:11', '1', '2024-03-08 22:41:42', b'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 (1061137, 0, '评论', '0', 'bpm_comment_type', 0, 'primary', '', '流程评论类型 - 评论', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'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 (1061138, 1, '审批通过', '1', 'bpm_comment_type', 0, 'success', '', '流程评论类型 - 审批通过', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'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 (1061139, 2, '不通过', '2', 'bpm_comment_type', 0, 'danger', '', '流程评论类型 - 不通过', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'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 (1061140, 3, '已取消', '3', 'bpm_comment_type', 0, 'info', '', '流程评论类型 - 已取消', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'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 (1061141, 4, '退回', '4', 'bpm_comment_type', 0, 'warning', '', '流程评论类型 - 退回', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'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 (1061142, 5, '委派发起', '5', 'bpm_comment_type', 0, 'primary', '', '流程评论类型 - 委派发起', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'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 (1061143, 6, '委派完成', '6', 'bpm_comment_type', 0, 'primary', '', '流程评论类型 - 委派完成', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'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 (1061144, 7, '转派', '7', 'bpm_comment_type', 0, 'warning', '', '流程评论类型 - 转派', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'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 (1061145, 8, '加签', '8', 'bpm_comment_type', 0, 'primary', '', '流程评论类型 - 加签', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'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 (1061146, 9, '减签', '9', 'bpm_comment_type', 0, 'info', '', '流程评论类型 - 减签', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'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 (1515, 35, '发起人自选', '35', 'bpm_task_candidate_strategy', 0, '', '', '', '1', '2024-03-22 19:45:16', '1', '2024-03-22 19:45:16', b'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 (1516, 1, '执行监听器', 'execution', 'bpm_process_listener_type', 0, 'primary', '', '', '1', '2024-03-23 12:54:03', '1', '2024-03-23 19:14:19', b'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 (1517, 1, '任务监听器', 'task', 'bpm_process_listener_type', 0, 'success', '', '', '1', '2024-03-23 12:54:13', '1', '2024-03-23 19:14:24', b'0');
@@ -1513,7 +1523,7 @@ CREATE TABLE `system_dict_type` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1061099 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
) ENGINE = InnoDB AUTO_INCREMENT = 1061100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
-- ----------------------------
-- Records of system_dict_type
@@ -1726,6 +1736,7 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061096, 'IM 通话参与角色', 'im_rtc_participant_role', 0, '1=发起人;2=被邀请者;3=主动加入者', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061097, 'IM 通话参与状态', 'im_rtc_participant_status', 0, '10=邀请中;20=已加入;30=已拒绝;40=未应答;50=已离开', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061098, 'IM 频道素材内容类型', 'im_channel_material_type', 0, '1=站内富文本 / 2=外链', '1', '2026-05-19 14:09:25', '1', '2026-05-19 14:09:25', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061099, '流程评论类型', 'bpm_comment_type', 0, 'Flowable 评论类型', '1', '2026-07-02 09:20:00', '1', '2026-07-02 09:20:00', b'0', NULL);
COMMIT;
-- ----------------------------
+53 -42
View File
@@ -14,7 +14,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.06-jdk8-SNAPSHOT</revision>
<revision>2026.07-jdk8-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 -->
<spring.framework.version>5.3.39</spring.framework.version>
@@ -41,9 +41,9 @@
<!-- 服务保障相关 -->
<lock4j.version>2.2.7</lock4j.version>
<!-- 监控相关 -->
<skywalking.version>8.12.0</skywalking.version>
<skywalking.version>9.6.0</skywalking.version>
<spring-boot-admin.version>2.7.15</spring-boot-admin.version>
<opentracing.version>0.33.0</opentracing.version>
<opentelemetry.version>1.39.0</opentelemetry.version>
<!-- Test 测试相关 -->
<podam.version>7.2.11.RELEASE</podam.version> <!-- Spring Boot 2.X 最多使用 7.2.11 版本 -->
<jedis-mock.version>1.1.15</jedis-mock.version>
@@ -60,8 +60,7 @@
<hutool-5.version>5.8.46</hutool-5.version>
<fastexcel.version>1.3.0</fastexcel.version>
<velocity.version>2.4</velocity.version> <!-- JDK8 不能从 2.4 升级到 2.4.1,会报包不存在!!!! -->
<fastjson.version>1.2.83</fastjson.version>
<fastjson2.version>2.0.62</fastjson2.version>
<fastjson2.version>2.0.63</fastjson2.version>
<guava.version>33.6.0-jre</guava.version>
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<commons-net.version>3.13.0</commons-net.version>
@@ -76,8 +75,8 @@
<okhttp.version>4.12.0</okhttp.version>
<californium.version>3.14.0</californium.version>
<j2mod.version>3.3.0</j2mod.version>
<httpclient5.version>5.5.2</httpclient5.version> <!-- WxJava 4.8.x 需要 HttpClient5 5.4+,Spring Boot 2.7 默认 5.1.4 不兼容 -->
<httpcore5.version>5.3.6</httpcore5.version> <!-- 配套 httpclient5 5.5.2,Spring Boot 2.7 默认 5.1.5 不兼容 -->
<httpclient5.version>5.5.1</httpclient5.version> <!-- WxJava 4.8.x 需要 HttpClient5 5.4+,Spring Boot 2.7 默认 5.1.4 不兼容;JDK8 兼容不能升级到 5.5.2+ -->
<httpcore5.version>5.3.6</httpcore5.version> <!-- 配套 httpclient5 5.5.1,Spring Boot 2.7 默认 5.1.5 不兼容 -->
<!-- 三方云服务相关 -->
<awssdk.version>2.46.17</awssdk.version>
<justauth.version>1.16.7</justauth.version>
@@ -257,6 +256,12 @@
<groupId>org.dromara</groupId>
<artifactId>easy-trans-anno</artifactId>
<version>${easy-trans.version}</version>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
@@ -328,6 +333,12 @@
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq-spring.version}</version>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 服务保障相关 -->
@@ -357,45 +368,25 @@
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>${skywalking.version}</version>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>${skywalking.version}</version>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-opentracing</artifactId>
<version>${skywalking.version}</version>
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <artifactId>opentracing-api</artifactId>-->
<!-- <groupId>io.opentracing</groupId>-->
<!-- </exclusion>-->
<!-- <exclusion>-->
<!-- <artifactId>opentracing-util</artifactId>-->
<!-- <groupId>io.opentracing</groupId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
</dependency>
<dependency>
<groupId>io.opentracing</groupId>
<artifactId>opentracing-api</artifactId>
<version>${opentracing.version}</version>
</dependency>
<dependency>
<groupId>io.opentracing</groupId>
<artifactId>opentracing-util</artifactId>
<version>${opentracing.version}</version>
</dependency>
<dependency>
<groupId>io.opentracing</groupId>
<artifactId>opentracing-noop</artifactId>
<version>${opentracing.version}</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
@@ -523,7 +514,7 @@
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
<version>${fastjson2.version}</version> <!-- fastjson2 官方提供的 1.x API 兼容实现 -->
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
@@ -641,7 +632,7 @@
<version>${j2mod.version}</version>
</dependency>
<!-- WxJava 4.8.x 需要 HttpClient5 5.4+,覆盖 Spring Boot 2.7 默认的 5.1.4 -->
<!-- WxJava 4.8.x 需要 HttpClient5 5.4+,覆盖 Spring Boot 2.7 默认的 5.1.4;JDK8 兼容不能升级到 5.5.2+ -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
@@ -669,6 +660,12 @@
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
<version>${justauth.version}</version>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.xkcoding.justauth</groupId>
@@ -692,6 +689,10 @@
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
@@ -734,6 +735,12 @@
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-spring-boot-starter</artifactId>
<version>${jimureport.version}</version>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
@@ -748,6 +755,10 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
+7 -2
View File
@@ -66,8 +66,8 @@
<!-- 监控相关 -->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
<!-- 工具类相关 -->
@@ -133,6 +133,11 @@
<artifactId>transmittable-thread-local</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId> <!-- JustAuth、Jimu 等三方库仍使用 1.x 包名,由项目统一提供 fastjson2 兼容实现 -->
</dependency>
<dependency>
<groupId>org.dromara</groupId> <!-- VO 数据翻译 -->
<artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 -->
@@ -37,6 +37,15 @@ public interface OAuth2TokenCommonApi {
*/
OAuth2AccessTokenRespDTO removeAccessToken(String accessToken);
/**
* 移除用户的所有访问令牌
* 注意:该流程中,会移除相关的刷新令牌
*
* @param userId 用户编号
* @param userType 用户类型
*/
void removeAccessToken(Long userId, Integer userType);
/**
* 刷新访问令牌
*
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.common.util.monitor;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
/**
* 链路追踪工具类
@@ -18,13 +19,14 @@ public class TracerUtils {
}
/**
* 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。
* 获得链路追踪编号,直接返回 OpenTelemetry 的 TraceId。
* 如果不存在的话为空字符串!!!
*
* @return 链路追踪编号
*/
public static String getTraceId() {
return TraceContext.traceId();
SpanContext context = Span.current().getSpanContext();
return context.isValid() ? context.getTraceId() : "";
}
}
@@ -0,0 +1,207 @@
package cn.iocoder.yudao.framework.excel.core.convert;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Excel 多值数据字典转换器
*
* 数据库存储值使用半角逗号分隔,例如 {@code 1,2}
* Excel 展示使用顿号分隔,例如 {@code 男、女}
* 使用时,需要在字段上同时配置
* {@code @ExcelProperty(converter = MultiDictConvert.class)} 和 {@link DictFormat}
*
* @author NaCl
*/
@Slf4j
public class MultiDictConvert implements Converter<Object> {
private static final String EXCEL_SEPARATOR = "、";
private static final String DB_SEPARATOR = ",";
private static final String EXCEL_SEPARATOR_REGEX = "[、,,]";
@Override
public Class<?> supportJavaTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
throw new UnsupportedOperationException("暂不支持,也不需要");
}
@Override
public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 空时,返回空值
Field field = contentProperty.getField();
String labels = readCellData.getStringValue();
if (StrUtil.isBlank(labels)) {
return convertToFieldValue(field, new ArrayList<>());
}
// 使用字典解析
String type = getType(contentProperty);
String[] labelArray = labels.split(EXCEL_SEPARATOR_REGEX);
List<String> values = new ArrayList<>(labelArray.length);
for (String item : labelArray) {
String label = item.trim();
if (StrUtil.isBlank(label)) {
continue;
}
String value = DictFrameworkUtils.parseDictDataValue(type, label);
if (value == null) {
log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label);
return null;
}
values.add(value);
}
// 将 String 的 value 转换成对应的属性
return convertToFieldValue(field, values);
}
@Override
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 空时,返回空
if (object == null) {
return new WriteCellData<>("");
}
// 使用字典格式化
String type = getType(contentProperty);
List<String> values = convertToStringList(object);
List<String> labels = new ArrayList<>(values.size());
for (String value : values) {
String label = DictFrameworkUtils.parseDictDataLabel(type, value);
if (label == null) {
log.error("[convertToExcelData][type({}) 转换不了 value({})]", type, value);
return new WriteCellData<>("");
}
labels.add(label);
}
// 生成 Excel 小表格
return new WriteCellData<>(String.join(EXCEL_SEPARATOR, labels));
}
private static Object convertToFieldValue(Field field, List<String> values) {
Class<?> fieldClazz = field.getType();
if (String.class == fieldClazz) {
return String.join(DB_SEPARATOR, values);
}
if (fieldClazz.isArray()) {
return convertToArray(fieldClazz.getComponentType(), values);
}
if (Collection.class.isAssignableFrom(fieldClazz)) {
return convertToCollection(field, values);
}
return Convert.convert(fieldClazz, String.join(DB_SEPARATOR, values));
}
private static Object convertToArray(Class<?> componentType, List<String> values) {
Object array = Array.newInstance(componentType, values.size());
for (int i = 0; i < values.size(); i++) {
Array.set(array, i, Convert.convert(componentType, values.get(i)));
}
return array;
}
private static Collection<?> convertToCollection(Field field, List<String> values) {
Class<?> elementClazz = getCollectionElementClazz(field);
Collection<Object> result = createCollection(field.getType());
for (String value : values) {
result.add(Convert.convert(elementClazz, value));
}
return result;
}
@SuppressWarnings("unchecked")
private static Collection<Object> createCollection(Class<?> fieldClazz) {
if (!fieldClazz.isInterface() && !Modifier.isAbstract(fieldClazz.getModifiers())) {
try {
return (Collection<Object>) fieldClazz.getDeclaredConstructor().newInstance();
} catch (Exception ignored) {
// 使用默认集合实现
}
}
return Set.class.isAssignableFrom(fieldClazz) ? new LinkedHashSet<>() : new ArrayList<>();
}
private static Class<?> getCollectionElementClazz(Field field) {
Type genericType = field.getGenericType();
if (!(genericType instanceof ParameterizedType)) {
return String.class;
}
Type actualType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
if (actualType instanceof Class<?>) {
return (Class<?>) actualType;
}
if (actualType instanceof ParameterizedType
&& ((ParameterizedType) actualType).getRawType() instanceof Class<?>) {
return (Class<?>) ((ParameterizedType) actualType).getRawType();
}
return String.class;
}
private static List<String> convertToStringList(Object object) {
List<String> values = new ArrayList<>();
if (object instanceof String) {
String[] valueArray = ((String) object).split(DB_SEPARATOR);
for (String value : valueArray) {
addStringValue(values, value);
}
return values;
}
if (object instanceof Collection<?>) {
for (Object item : (Collection<?>) object) {
addStringValue(values, item);
}
return values;
}
if (object.getClass().isArray()) {
int length = Array.getLength(object);
for (int i = 0; i < length; i++) {
addStringValue(values, Array.get(object, i));
}
return values;
}
addStringValue(values, object);
return values;
}
private static void addStringValue(List<String> values, Object value) {
if (value == null) {
return;
}
String str = String.valueOf(value).trim();
if (StrUtil.isBlank(str)) {
return;
}
values.add(str);
}
private static String getType(ExcelContentProperty contentProperty) {
return contentProperty.getField().getAnnotation(DictFormat.class).value();
}
}
@@ -0,0 +1,295 @@
package cn.iocoder.yudao.framework.excel.core.convert;
import cn.hutool.core.collection.ListUtil;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* {@link MultiDictConvert} 的单元测试
*
* @author NaCl
*/
public class MultiDictConvertTest extends BaseMockitoUnitTest {
private static final String DICT_TYPE = "test_dict";
@Mock
private DictDataCommonApi dictDataApi;
private MultiDictConvert convert;
@BeforeEach
public void setUp() {
convert = new MultiDictConvert();
DictFrameworkUtils.init(dictDataApi);
DictFrameworkUtils.clearCache();
}
@Test
public void testConvertToExcelData_string() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData("1,2", mockContentProperty("stringValues"), null);
// 断言
assertEquals("男、女", result.getStringValue());
}
@Test
public void testConvertToExcelData_single() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData("1", mockContentProperty("stringValues"), null);
// 断言
assertEquals("男", result.getStringValue());
}
@Test
public void testConvertToExcelData_order() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData("2,1", mockContentProperty("stringValues"), null);
// 断言
assertEquals("女、男", result.getStringValue());
}
@Test
public void testConvertToExcelData_null() {
// 调用
WriteCellData<String> result = convert.convertToExcelData(null, null, null);
// 断言
assertEquals("", result.getStringValue());
}
@Test
public void testConvertToExcelData_collection() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData(ListUtil.of(1, 2),
mockContentProperty("listValues"), null);
// 断言
assertEquals("男、女", result.getStringValue());
}
@Test
public void testConvertToExcelData_array() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData(new int[]{1, 2},
mockContentProperty("stringValues"), null);
// 断言
assertEquals("男、女", result.getStringValue());
}
@Test
public void testConvertToExcelData_unknownValue() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
WriteCellData<String> result = convert.convertToExcelData("1,3", mockContentProperty("stringValues"), null);
// 断言
assertEquals("", result.getStringValue());
}
@Test
public void testConvertToJavaData_string() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男、女"),
mockContentProperty("stringValues"), null);
// 断言
assertEquals("1,2", result);
}
@Test
public void testConvertToJavaData_single() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男"),
mockContentProperty("stringValues"), null);
// 断言
assertEquals("1", result);
}
@Test
public void testConvertToJavaData_blank() throws NoSuchFieldException {
// 调用
Object result = convert.convertToJavaData(mockReadCellData(""),
mockContentProperty("stringValues"), null);
// 断言
assertEquals("", result);
}
@Test
public void testConvertToJavaData_blankList() throws NoSuchFieldException {
// 调用
Object result = convert.convertToJavaData(mockReadCellData(""),
mockContentProperty("listValues"), null);
// 断言
assertTrue(result instanceof List);
assertTrue(((List<?>) result).isEmpty());
}
@Test
public void testConvertToJavaData_blankSet() throws NoSuchFieldException {
// 调用
Object result = convert.convertToJavaData(mockReadCellData(""),
mockContentProperty("setValues"), null);
// 断言
assertTrue(result instanceof LinkedHashSet);
assertTrue(((Set<?>) result).isEmpty());
}
@Test
public void testConvertToJavaData_blankArray() throws NoSuchFieldException {
// 调用
Object result = convert.convertToJavaData(mockReadCellData(""),
mockContentProperty("arrayValues"), null);
// 断言
assertArrayEquals(new Integer[0], (Integer[]) result);
}
@Test
@SuppressWarnings("unchecked")
public void testConvertToJavaData_list() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男、女"),
mockContentProperty("listValues"), null);
// 断言
assertEquals(ListUtil.of(1, 2), (List<Integer>) result);
}
@Test
@SuppressWarnings("unchecked")
public void testConvertToJavaData_set() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
mockContentProperty("setValues"), null);
// 断言
assertEquals(new LinkedHashSet<>(Arrays.asList(1L, 2L)), (Set<Long>) result);
}
@Test
@SuppressWarnings("SimplifiableAssertion")
public void testConvertToJavaData_hashSet() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
mockContentProperty("hashSetValues"), null);
// 断言
assertTrue(result instanceof HashSet);
assertEquals(new HashSet<>(Arrays.asList(1L, 2L)), result);
}
@Test
public void testConvertToJavaData_array() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男,女"),
mockContentProperty("arrayValues"), null);
// 断言
assertArrayEquals(new Integer[]{1, 2}, (Integer[]) result);
}
@Test
public void testConvertToJavaData_unknownLabel() throws NoSuchFieldException {
// mock 数据
mockDictDatas();
// 调用
Object result = convert.convertToJavaData(mockReadCellData("男、未知"),
mockContentProperty("stringValues"), null);
// 断言
assertNull(result);
}
private void mockDictDatas() {
List<DictDataRespDTO> dictDatas = ListUtil.of(
randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("1").setLabel("男")),
randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("2").setLabel("女"))
);
when(dictDataApi.getDictDataList(eq(DICT_TYPE))).thenReturn(dictDatas);
}
private static ReadCellData<?> mockReadCellData(String value) {
ReadCellData<?> readCellData = mock(ReadCellData.class);
when(readCellData.getStringValue()).thenReturn(value);
return readCellData;
}
private static ExcelContentProperty mockContentProperty(String fieldName) throws NoSuchFieldException {
Field field = TestExcelVO.class.getDeclaredField(fieldName);
ExcelContentProperty contentProperty = mock(ExcelContentProperty.class);
when(contentProperty.getField()).thenReturn(field);
return contentProperty;
}
static class TestExcelVO {
@DictFormat(DICT_TYPE)
private String stringValues;
@DictFormat(DICT_TYPE)
private List<Integer> listValues;
@DictFormat(DICT_TYPE)
private Set<Long> setValues;
@DictFormat(DICT_TYPE)
private HashSet<Long> hashSetValues;
@DictFormat(DICT_TYPE)
private Integer[] arrayValues;
}
}
@@ -35,20 +35,25 @@
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided,只有 TraceFilter 使用 -->
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>io.opentracing</groupId>
<artifactId>opentracing-util</artifactId>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<optional>true</optional>
</dependency>
<dependency>
@@ -56,11 +61,6 @@
<artifactId>apm-toolkit-logback-1.x</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-opentracing</artifactId>
<optional>true</optional>
</dependency>
<!-- Micrometer 对 Prometheus 的支持 -->
<dependency>
@@ -1,9 +1,14 @@
package cn.iocoder.yudao.framework.tracer.config;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.tracer.core.aop.BizTraceAspect;
import cn.iocoder.yudao.framework.tracer.core.filter.TraceFilter;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@@ -16,28 +21,27 @@ import org.springframework.context.annotation.Bean;
*/
@AutoConfiguration
@ConditionalOnClass(name = {
"org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer", // 来自 apm-toolkit-opentracing.jar
// "io.opentracing.Tracer", // 来自 opentracing-api.jar
"io.opentelemetry.api.trace.Tracer", // 来自 opentelemetry-api.jar
"javax.servlet.Filter"
})
@EnableConfigurationProperties(TracerProperties.class)
@ConditionalOnProperty(prefix = "yudao.tracer", value = "enable", matchIfMissing = true)
public class YudaoTracerAutoConfiguration {
// TODO @芋艿:skywalking 不兼容最新的 opentracing 版本。同时,opentracing 也停止了维护,尬住了!后续换 opentelemetry 即可!
// @Bean
// public BizTraceAspect bizTracingAop() {
// return new BizTraceAspect(tracer());
// }
//
// @Bean
// public Tracer tracer() {
// // 创建 SkywalkingTracer 对象
// SkywalkingTracer tracer = new SkywalkingTracer();
// // 设置为 GlobalTracer 的追踪器
// GlobalTracer.registerIfAbsent(tracer);
// return tracer;
// }
@Value("${spring.application.name:application}")
private String applicationName;
@Bean
@ConditionalOnMissingBean
public Tracer tracer() {
return GlobalOpenTelemetry.getTracer(applicationName);
}
@Bean
@ConditionalOnMissingBean
public BizTraceAspect bizTracingAop(Tracer tracer) {
return new BizTraceAspect(tracer);
}
/**
* 创建 TraceFilter 过滤器,响应 header 设置 traceId
@@ -5,9 +5,9 @@ import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.tracer.core.annotation.BizTrace;
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
import cn.iocoder.yudao.framework.tracer.core.util.TracerFrameworkUtils;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.tag.Tags;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
@@ -36,10 +36,10 @@ public class BizTraceAspect {
public Object around(ProceedingJoinPoint joinPoint, BizTrace trace) throws Throwable {
// 创建 span
String operationName = getOperationName(joinPoint, trace);
Span span = tracer.buildSpan(operationName)
.withTag(Tags.COMPONENT.getKey(), "biz")
.start();
try {
Span span = tracer.spanBuilder(operationName)
.setAttribute("component", "biz")
.startSpan();
try (Scope ignored = span.makeCurrent()) {
// 执行原有方法
return joinPoint.proceed();
} catch (Throwable throwable) {
@@ -49,7 +49,7 @@ public class BizTraceAspect {
// 设置 Span 的 biz 属性
setBizTag(span, joinPoint, trace);
// 完成 Span
span.finish();
span.end();
}
}
@@ -67,8 +67,8 @@ public class BizTraceAspect {
private void setBizTag(Span span, ProceedingJoinPoint joinPoint, BizTrace trace) {
try {
Map<String, Object> result = SpringExpressionUtils.parseExpressions(joinPoint, asList(trace.type(), trace.id()));
span.setTag(BizTrace.TYPE_TAG, MapUtil.getStr(result, trace.type()));
span.setTag(BizTrace.ID_TAG, MapUtil.getStr(result, trace.id()));
span.setAttribute(BizTrace.TYPE_TAG, MapUtil.getStr(result, trace.type()));
span.setAttribute(BizTrace.ID_TAG, MapUtil.getStr(result, trace.id()));
} catch (Exception ex) {
log.error("[setBizTag][解析 bizType 与 bizId 发生异常]", ex);
}
@@ -1,12 +1,10 @@
package cn.iocoder.yudao.framework.tracer.core.util;
import io.opentracing.Span;
import io.opentracing.tag.Tags;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
/**
* 链路追踪 Util
@@ -22,25 +20,24 @@ public class TracerFrameworkUtils {
* @param span Span
*/
public static void onError(Throwable throwable, Span span) {
Tags.ERROR.set(span, Boolean.TRUE);
if (throwable != null) {
span.log(errorLogs(throwable));
// 忽略无效 Span
if (span == null || !span.getSpanContext().isValid()) {
return;
}
// 标记异常状态
if (throwable == null) {
span.setStatus(StatusCode.ERROR);
return;
}
}
private static Map<String, Object> errorLogs(Throwable throwable) {
Map<String, Object> errorLogs = new HashMap<String, Object>(10);
errorLogs.put("event", Tags.ERROR.getKey());
errorLogs.put("error.object", throwable);
errorLogs.put("error.kind", throwable.getClass().getName());
// 记录异常事件
span.recordException(throwable);
String message = throwable.getCause() != null ? throwable.getCause().getMessage() : throwable.getMessage();
if (message != null) {
errorLogs.put("message", message);
}
span.setStatus(StatusCode.ERROR, message == null ? "" : message);
// 记录异常堆栈
StringWriter sw = new StringWriter();
throwable.printStackTrace(new PrintWriter(sw));
errorLogs.put("stack", sw.toString());
return errorLogs;
span.setAttribute("error.stack", sw.toString());
}
}
@@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.google.common.util.concurrent.UncheckedExecutionException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.util.Assert;
@@ -52,6 +53,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
* @author 芋道源码
*/
@RestControllerAdvice
@Order(0) // 优先于三方库默认的全局异常处理器,例如 JimuReport
@AllArgsConstructor
@Slf4j
public class GlobalExceptionHandler {
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* MiniMax {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class MiniMaxChatModel implements ChatModel {
public static final String BASE_URL = "https://api.minimaxi.com/v1";
public static final String MODEL_DEFAULT = "MiniMax-M3";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 月之暗面 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class MoonshotChatModel implements ChatModel {
public static final String BASE_URL = "https://api.moonshot.cn";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "kimi-k2.6";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 阶跃星辰 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class StepFunChatModel implements ChatModel {
public static final String BASE_URL = "https://api.stepfun.com";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "step-3.7-flash";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 文心一言 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class YiYanChatModel implements ChatModel {
public static final String BASE_URL = "https://qianfan.baidubce.com/v2";
public static final String MODEL_DEFAULT = "ernie-5.1";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 智谱 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class ZhiPuChatModel implements ChatModel {
public static final String BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
public static final String MODEL_DEFAULT = "glm-5.2";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link GrokChatModel} 集成测试
*
* @author 芋道源码
*/
public class GrokChatModelTests {
private static final String API_KEY = SystemUtil.get("GROK_API_KEY",
"sk-xxxx");
private static final String MODEL = SystemUtil.get("GROK_MODEL",
GrokChatModel.MODEL_DEFAULT);
private final GrokChatModel chatModel = new GrokChatModel(OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(GrokChatModel.BASE_URL)
.completionsPath(GrokChatModel.COMPLETE_PATH)
.apiKey(API_KEY)
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(MODEL)
.temperature(0.7)
.build())
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
}
@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link StepFunChatModel} 集成测试
*
* @author 芋道源码
*/
public class StepFunChatModelTests {
private static final String API_KEY = SystemUtil.get("STEPFUN_API_KEY",
"sk-xxxx");
private static final String MODEL = SystemUtil.get("STEPFUN_MODEL",
StepFunChatModel.MODEL_DEFAULT);
private final StepFunChatModel chatModel = new StepFunChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(StepFunChatModel.BASE_URL)
.completionsPath(StepFunChatModel.COMPLETE_PATH)
.apiKey(API_KEY)
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model(MODEL)
.temperature(0.7)
.build())
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
// System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
System.out.println(response.getResult() != null ? response.getResult().getOutput() : null);
}).then().block();
}
}
@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.bpm.controller.admin.comment;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.comment.vo.BpmCommentCreateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.comment.vo.BpmCommentRespVO;
import cn.iocoder.yudao.module.bpm.service.comment.BpmCommentService;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
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 javax.annotation.Resource;
import javax.validation.Valid;
import org.flowable.engine.task.Comment;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
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.convertSet;
@Tag(name = "管理后台 - 流程评论")
@RestController
@RequestMapping("/bpm/comment")
@Validated
public class BpmCommentController {
@Resource
private BpmCommentService commentService;
@Resource
private BpmTaskService taskService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/list-by-process-instance-id")
@Operation(summary = "获得指定流程实例的评论列表")
@Parameter(name = "processInstanceId", description = "流程实例的编号", required = true)
@PreAuthorize("@ss.hasPermission('bpm:task:query')")
public CommonResult<List<BpmCommentRespVO>> getCommentListByProcessInstanceId(
@RequestParam("processInstanceId") String processInstanceId) {
// 获得评论列表
List<Comment> commentList = commentService.getCommentListByProcessInstanceId(processInstanceId);
if (CollUtil.isEmpty(commentList)) {
return success(Collections.emptyList());
}
// 拼接 VO
Map<String, HistoricTaskInstance> taskMap = taskService.getHistoricTaskMap(
convertSet(commentList, Comment::getTaskId));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSet(commentList, comment -> NumberUtils.parseLong(comment.getUserId())));
return success(convertList(commentList, comment -> BeanUtils.toBean(comment, BpmCommentRespVO.class, commentVO -> {
commentVO.setMessage(comment.getFullMessage())
.setCreateTime(DateUtils.of(comment.getTime()))
.setTask(BeanUtils.toBean(taskMap.get(comment.getTaskId()), BpmCommentRespVO.Task.class))
.setUser(BeanUtils.toBean(userMap.get(NumberUtils.parseLong(comment.getUserId())), UserSimpleBaseVO.class));
})));
}
@PostMapping("/create")
@Operation(summary = "创建流程评论")
@PreAuthorize("@ss.hasPermission('bpm:task:update')")
public CommonResult<Boolean> createComment(@Valid @RequestBody BpmCommentCreateReqVO reqVO) {
commentService.createComment(reqVO);
return success(true);
}
}
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.bpm.controller.admin.comment.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - 流程评论创建 Request VO")
@Data
public class BpmCommentCreateReqVO {
@Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
@NotEmpty(message = "任务编号不能为空")
private String taskId;
@Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "请关注报销发票附件")
@NotBlank(message = "评论内容不能为空")
@Size(max = 500, message = "评论内容不能超过 500 个字符")
private String message;
}
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.bpm.controller.admin.comment.vo;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 流程评论 Response VO")
@Data
public class BpmCommentRespVO {
@Schema(description = "评论编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private String id;
@Schema(description = "任务编号", example = "2048")
private String taskId;
@Schema(description = "任务")
private Task task;
@Schema(description = "流程实例编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
private String processInstanceId;
@Schema(description = "评论类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private String type;
@Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "请关注报销发票附件")
private String message;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "创建人")
private UserSimpleBaseVO user;
@Schema(description = "任务")
@Data
public static class Task {
@Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private String id;
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "经理审批")
private String name;
@Schema(description = "任务定义的标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "Activity_one")
private String taskDefinitionKey;
}
}
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.bpm.service.comment;
import cn.iocoder.yudao.module.bpm.controller.admin.comment.vo.BpmCommentCreateReqVO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
import javax.validation.Valid;
import org.flowable.engine.task.Comment;
import java.util.List;
/**
* 流程评论 Service 接口
*
* @author 芋道源码
*/
public interface BpmCommentService {
/**
* 获得指定流程实例的评论列表
*
* @param processInstanceId 流程实例的编号
* @return 评论列表
*/
List<Comment> getCommentListByProcessInstanceId(String processInstanceId);
/**
* 创建流程评论
*
* @param reqVO 评论请求
*/
void createComment(@Valid BpmCommentCreateReqVO reqVO);
/**
* 创建流程评论
*
* @param taskId 任务编号
* @param processInstanceId 流程实例编号
* @param type 评论类型
* @param params 评论模板参数
*/
void createComment(String taskId, String processInstanceId, BpmCommentTypeEnum type, Object... params);
}
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.bpm.service.comment;
import cn.iocoder.yudao.module.bpm.controller.admin.comment.vo.BpmCommentCreateReqVO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import javax.annotation.Resource;
import javax.validation.Valid;
import org.flowable.engine.TaskService;
import org.flowable.engine.task.Comment;
import org.flowable.task.api.Task;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 流程评论 Service 实现类
*
* @author 芋道源码
*/
@Service
public class BpmCommentServiceImpl implements BpmCommentService {
@Resource
private TaskService taskService;
@Resource
@Lazy // 延迟加载,避免循环依赖
private BpmTaskService bpmTaskService;
@Override
public List<Comment> getCommentListByProcessInstanceId(String processInstanceId) {
return taskService.getProcessInstanceComments(processInstanceId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void createComment(@Valid BpmCommentCreateReqVO reqVO) {
Task task = bpmTaskService.validateTaskExists(reqVO.getTaskId());
createComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.COMMENT, reqVO.getMessage());
}
@Override
public void createComment(String taskId, String processInstanceId, BpmCommentTypeEnum type, Object... params) {
taskService.addComment(taskId, processInstanceId, type.getType(), type.formatComment(params));
}
}
@@ -0,0 +1,175 @@
package cn.iocoder.yudao.module.bpm.service.definition;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ModelQuery;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* {@link BpmModelServiceImpl} 的单元测试
*
* @author 芋道源码
*/
public class BpmModelServiceImplTest extends BaseMockitoUnitTest {
private static final Long TENANT_ID = 1L;
private static final String MODEL_ID = "model-id";
@InjectMocks
private BpmModelServiceImpl modelService;
@Mock
private RepositoryService repositoryService;
@Mock
private ModelQuery modelQuery;
@BeforeEach
public void setUp() {
TenantContextHolder.setTenantId(TENANT_ID);
}
@AfterEach
public void tearDown() {
TenantContextHolder.clear();
}
@Test
public void testExportModel_bpmn() {
// 准备参数
Model model = mockModel(BpmModelTypeEnum.BPMN.getType());
String bpmnXml = "<definitions />";
// mock 方法(repositoryService)
mockGetModel(model);
when(repositoryService.getModelEditorSource(eq(MODEL_ID))).thenReturn(StrUtil.utf8Bytes(bpmnXml));
// 调用
BpmModelSaveReqVO result = modelService.exportModel(MODEL_ID);
// 断言
assertEquals(model.getKey(), result.getKey());
assertEquals(model.getName(), result.getName());
assertEquals(model.getCategory(), result.getCategory());
assertEquals(BpmModelTypeEnum.BPMN.getType(), result.getType());
assertEquals(bpmnXml, result.getBpmnXml());
assertNull(result.getSimpleModel());
}
@Test
public void testExportModel_simple() {
// 准备参数
Model model = mockModel(BpmModelTypeEnum.SIMPLE.getType());
BpmSimpleModelNodeVO simpleModel = new BpmSimpleModelNodeVO();
simpleModel.setId("start");
simpleModel.setType(BpmSimpleModelNodeTypeEnum.START_NODE.getType());
// mock 方法(repositoryService)
mockGetModel(model);
when(repositoryService.getModelEditorSourceExtra(eq(MODEL_ID)))
.thenReturn(JsonUtils.toJsonByte(simpleModel));
// 调用
BpmModelSaveReqVO result = modelService.exportModel(MODEL_ID);
// 断言
assertEquals(BpmModelTypeEnum.SIMPLE.getType(), result.getType());
assertNotNull(result.getSimpleModel());
assertEquals(simpleModel.getId(), result.getSimpleModel().getId());
assertEquals(simpleModel.getType(), result.getSimpleModel().getType());
assertNull(result.getBpmnXml());
}
@Test
public void testExportModel_notExists() {
// mock 方法(repositoryService)
when(repositoryService.createModelQuery()).thenReturn(modelQuery);
when(modelQuery.modelId(eq(MODEL_ID))).thenReturn(modelQuery);
when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
when(modelQuery.singleResult()).thenReturn(null);
// 调用,并断言异常
assertServiceException(() -> modelService.exportModel(MODEL_ID), MODEL_NOT_EXISTS);
}
@Test
public void testImportModel_bpmn() {
// 准备参数
BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO();
reqVO.setKey("test_process");
reqVO.setName("测试流程");
reqVO.setCategory("OA");
reqVO.setType(BpmModelTypeEnum.BPMN.getType());
reqVO.setBpmnXml("<definitions />");
reqVO.setStartUserIds(Arrays.asList(10L, 20L));
reqVO.setStartDeptIds(Collections.singletonList(30L));
reqVO.setManagerUserIds(Collections.singletonList(40L));
Model model = mock(Model.class);
when(model.getId()).thenReturn(MODEL_ID);
// mock 方法(repositoryService)
when(repositoryService.createModelQuery()).thenReturn(modelQuery);
when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
when(modelQuery.modelKey(eq(reqVO.getKey()))).thenReturn(modelQuery);
when(modelQuery.singleResult()).thenReturn(null);
when(repositoryService.newModel()).thenReturn(model);
// 调用
String result = modelService.importModel(reqVO);
// 断言
assertEquals(MODEL_ID, result);
verify(model).setTenantId(TENANT_ID.toString());
verify(repositoryService).saveModel(same(model));
verify(repositoryService).addModelEditorSource(eq(MODEL_ID),
aryEq(StrUtil.utf8Bytes(reqVO.getBpmnXml())));
ArgumentCaptor<String> metaInfoCaptor = ArgumentCaptor.forClass(String.class);
verify(model).setMetaInfo(metaInfoCaptor.capture());
BpmModelMetaInfoVO metaInfo = JsonUtils.parseObject(metaInfoCaptor.getValue(), BpmModelMetaInfoVO.class);
assertNotNull(metaInfo);
assertEquals(Arrays.asList(10L, 20L), metaInfo.getStartUserIds());
assertEquals(Collections.singletonList(30L), metaInfo.getStartDeptIds());
assertEquals(Collections.singletonList(40L), metaInfo.getManagerUserIds());
}
private Model mockModel(Integer type) {
Model model = mock(Model.class);
when(model.getKey()).thenReturn("test_process");
when(model.getName()).thenReturn("测试流程");
when(model.getCategory()).thenReturn("OA");
when(model.getCreateTime()).thenReturn(new Date(1_000L));
BpmModelMetaInfoVO metaInfo = new BpmModelMetaInfoVO();
metaInfo.setType(type);
when(model.getMetaInfo()).thenReturn(JsonUtils.toJsonString(metaInfo));
return model;
}
private void mockGetModel(Model model) {
when(repositoryService.createModelQuery()).thenReturn(modelQuery);
when(modelQuery.modelId(eq(MODEL_ID))).thenReturn(modelQuery);
when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
when(modelQuery.singleResult()).thenReturn(model);
}
}
@@ -0,0 +1,149 @@
package cn.iocoder.yudao.module.crm.controller.admin.performance;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.performance.CrmPerformanceConfigDO;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import cn.iocoder.yudao.module.crm.service.performance.CrmPerformanceConfigService;
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 javax.annotation.Resource;
import javax.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
@Tag(name = "管理后台 - CRM 业绩目标设置")
@RestController
@RequestMapping("/crm/performance-config")
@Validated
public class CrmPerformanceConfigController {
@Resource
private CrmPerformanceConfigService performanceConfigService;
@Resource
private DeptApi deptApi;
@Resource
private AdminUserApi adminUserApi;
@PostMapping("/create")
@Operation(summary = "创建业绩目标")
@PreAuthorize("@ss.hasPermission('crm:performance-config:create')")
public CommonResult<Long> createPerformanceConfig(@Valid @RequestBody CrmPerformanceConfigSaveReqVO createReqVO) {
return success(performanceConfigService.createPerformanceConfig(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新业绩目标")
@PreAuthorize("@ss.hasPermission('crm:performance-config:update')")
public CommonResult<Boolean> updatePerformanceConfig(@Valid @RequestBody CrmPerformanceConfigSaveReqVO updateReqVO) {
performanceConfigService.updatePerformanceConfig(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除业绩目标")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('crm:performance-config:delete')")
public CommonResult<Boolean> deletePerformanceConfig(@RequestParam("id") Long id) {
performanceConfigService.deletePerformanceConfig(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得业绩目标")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('crm:performance-config:query')")
public CommonResult<CrmPerformanceConfigRespVO> getPerformanceConfig(@RequestParam("id") Long id) {
// 1. 获取业绩目标
CrmPerformanceConfigDO performanceConfig = performanceConfigService.getPerformanceConfig(id);
// 2. 拼接数据
return success(buildPerformanceConfigDetail(performanceConfig));
}
private CrmPerformanceConfigRespVO buildPerformanceConfigDetail(CrmPerformanceConfigDO performanceConfig) {
if (performanceConfig == null) {
return null;
}
// 1. 转换成 VO
CrmPerformanceConfigRespVO respVO = BeanUtils.toBean(performanceConfig, CrmPerformanceConfigRespVO.class);
// 2. 拼接目标对象名称
return buildPerformanceConfigObjectName(respVO);
}
@GetMapping("/page")
@Operation(summary = "获得业绩目标分页")
@PreAuthorize("@ss.hasPermission('crm:performance-config:query')")
public CommonResult<PageResult<CrmPerformanceConfigRespVO>> getPerformanceConfigPage(@Valid CrmPerformanceConfigPageReqVO pageReqVO) {
// 1. 查询业绩目标分页
PageResult<CrmPerformanceConfigDO> pageResult = performanceConfigService.getPerformanceConfigPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2. 拼接数据
return success(BeanUtils.toBean(pageResult, CrmPerformanceConfigRespVO.class)
.setList(buildPerformanceConfigDetailList(pageResult.getList())));
}
private List<CrmPerformanceConfigRespVO> buildPerformanceConfigDetailList(List<CrmPerformanceConfigDO> list) {
if (CollUtil.isEmpty(list)) {
return emptyList();
}
// 1. 转换成 VO
List<CrmPerformanceConfigRespVO> voList = BeanUtils.toBean(list, CrmPerformanceConfigRespVO.class);
// 2. 拼接目标对象名称
return buildPerformanceConfigObjectNameList(voList);
}
// ========== VO 拼装 ==========
private CrmPerformanceConfigRespVO buildPerformanceConfigObjectName(CrmPerformanceConfigRespVO config) {
if (config == null) {
return null;
}
return buildPerformanceConfigObjectNameList(singletonList(config)).get(0);
}
private List<CrmPerformanceConfigRespVO> buildPerformanceConfigObjectNameList(List<CrmPerformanceConfigRespVO> list) {
if (CollUtil.isEmpty(list)) {
return emptyList();
}
// 1.1 获取部门列表
Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(list, CrmPerformanceConfigRespVO::getObjectId,
item -> Objects.equals(item.getObjectType(), CrmPerformanceConfigObjectTypeEnum.DEPT.getObjectType())));
// 1.2 获取员工列表
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(list, CrmPerformanceConfigRespVO::getObjectId,
item -> Objects.equals(item.getObjectType(), CrmPerformanceConfigObjectTypeEnum.USER.getObjectType())));
// 2. 设置目标对象名称
list.forEach(item -> {
if (Objects.equals(item.getObjectType(), CrmPerformanceConfigObjectTypeEnum.DEPT.getObjectType())) {
findAndThen(deptMap, item.getObjectId(), dept -> item.setObjectName(dept.getName()));
} else if (Objects.equals(item.getObjectType(), CrmPerformanceConfigObjectTypeEnum.USER.getObjectType())) {
findAndThen(userMap, item.getObjectId(), user -> item.setObjectName(user.getNickname()));
}
});
return list;
}
}
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - CRM 业绩目标设置分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class CrmPerformanceConfigPageReqVO extends PageParam {
@Schema(description = "目标对象编号", example = "1")
private Long objectId;
@Schema(description = "目标对象类型", example = "2")
@InEnum(value = CrmPerformanceConfigObjectTypeEnum.class, message = "目标对象类型,必须是 {value}")
private Integer objectType;
@Schema(description = "年份", example = "2026")
private Integer year;
@Schema(description = "目标类型", example = "5")
@InEnum(value = CrmPerformanceConfigBizTypeEnum.class, message = "目标类型,必须是 {value}")
private Integer bizType;
}
@@ -0,0 +1,74 @@
package cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - CRM 业绩目标设置 Response VO")
@Data
public class CrmPerformanceConfigRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "目标对象编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long objectId;
@Schema(description = "目标对象名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "销售部")
private String objectName;
@Schema(description = "目标对象类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer objectType;
@Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026")
private Integer year;
@Schema(description = "一月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal januaryTargetPrice;
@Schema(description = "二月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal februaryTargetPrice;
@Schema(description = "三月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal marchTargetPrice;
@Schema(description = "四月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal aprilTargetPrice;
@Schema(description = "五月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal mayTargetPrice;
@Schema(description = "六月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal juneTargetPrice;
@Schema(description = "七月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal julyTargetPrice;
@Schema(description = "八月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal augustTargetPrice;
@Schema(description = "九月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal septemberTargetPrice;
@Schema(description = "十月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal octoberTargetPrice;
@Schema(description = "十一月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal novemberTargetPrice;
@Schema(description = "十二月目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal decemberTargetPrice;
@Schema(description = "目标类型",
requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
private Integer bizType;
@Schema(description = "年度目标金额", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal yearTargetPrice;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}
@@ -0,0 +1,74 @@
package cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 业绩目标设置新增/修改 Request VO")
@Data
public class CrmPerformanceConfigSaveReqVO {
@Schema(description = "编号", example = "1")
private Long id;
@Schema(description = "目标对象编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "目标对象编号不能为空")
private Long objectId;
@Schema(description = "目标对象类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@NotNull(message = "目标对象类型不能为空")
@InEnum(value = CrmPerformanceConfigObjectTypeEnum.class, message = "目标对象类型,必须是 {value}")
private Integer objectType;
@Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026")
@NotNull(message = "年份不能为空")
private Integer year;
@Schema(description = "一月目标金额")
private BigDecimal januaryTargetPrice;
@Schema(description = "二月目标金额")
private BigDecimal februaryTargetPrice;
@Schema(description = "三月目标金额")
private BigDecimal marchTargetPrice;
@Schema(description = "四月目标金额")
private BigDecimal aprilTargetPrice;
@Schema(description = "五月目标金额")
private BigDecimal mayTargetPrice;
@Schema(description = "六月目标金额")
private BigDecimal juneTargetPrice;
@Schema(description = "七月目标金额")
private BigDecimal julyTargetPrice;
@Schema(description = "八月目标金额")
private BigDecimal augustTargetPrice;
@Schema(description = "九月目标金额")
private BigDecimal septemberTargetPrice;
@Schema(description = "十月目标金额")
private BigDecimal octoberTargetPrice;
@Schema(description = "十一月目标金额")
private BigDecimal novemberTargetPrice;
@Schema(description = "十二月目标金额")
private BigDecimal decemberTargetPrice;
@Schema(description = "目标类型",
requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
@NotNull(message = "目标类型不能为空")
@InEnum(value = CrmPerformanceConfigBizTypeEnum.class, message = "目标类型,必须是 {value}")
private Integer bizType;
}
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetRespVO;
import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsPerformanceTargetService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.annotation.Resource;
import javax.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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - CRM 业绩目标统计")
@RestController
@RequestMapping("/crm/statistics-performance-target")
@Validated
public class CrmStatisticsPerformanceTargetController {
@Resource
private CrmStatisticsPerformanceTargetService performanceTargetService;
@GetMapping("/get-performance-target-summary")
@Operation(summary = "获取业绩目标完成情况")
@PreAuthorize("@ss.hasPermission('crm:statistics-performance-target:query')")
public CommonResult<List<CrmStatisticsPerformanceTargetRespVO>> getPerformanceTargetSummary(@Valid CrmStatisticsPerformanceTargetReqVO reqVO) {
return success(performanceTargetService.getPerformanceTargetSummary(reqVO));
}
}
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO;
import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsProductService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.annotation.Resource;
import javax.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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - CRM 产品统计")
@RestController
@RequestMapping("/crm/statistics-product")
@Validated
public class CrmStatisticsProductController {
@Resource
private CrmStatisticsProductService productService;
@GetMapping("/get-product-sales-list")
@Operation(summary = "获取产品销售情况统计")
@PreAuthorize("@ss.hasPermission('crm:statistics-product:query')")
public CommonResult<List<CrmStatisticsProductSalesRespVO>> getProductSalesList(@Valid CrmStatisticsProductReqVO reqVO) {
return success(productService.getProductSalesList(reqVO));
}
@GetMapping("/get-product-category-summary")
@Operation(summary = "获取产品分类销售分析")
@PreAuthorize("@ss.hasPermission('crm:statistics-product:query')")
public CommonResult<List<CrmStatisticsProductCategoryRespVO>> getProductCategorySummary(@Valid CrmStatisticsProductReqVO reqVO) {
return success(productService.getProductCategorySummary(reqVO));
}
}
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 商机阶段统计 Response VO")
@NoArgsConstructor
@AllArgsConstructor
@Data
public class CrmStatisticsBusinessSummaryByStatusRespVO {
@Schema(description = "商机阶段编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long statusId;
@Schema(description = "商机阶段名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "初步接洽")
private String statusName;
@Schema(description = "赢单率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
private Integer statusPercent;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer sort;
@Schema(description = "商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long businessCount;
@Schema(description = "商机总金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private BigDecimal totalPrice;
}
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 员工业绩合同汇总 Response VO")
@Data
public class CrmStatisticsPerformanceSummaryRespVO {
@Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
private String time;
@Schema(description = "合同数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer contractCount;
@Schema(description = "合同金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000.00")
private BigDecimal contractPrice;
@Schema(description = "回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000.00")
private BigDecimal receivablePrice;
@Schema(description = "未回款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000.00")
private BigDecimal unreceivedPrice;
}
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 业绩目标完成实际值 Response VO")
@Data
@Accessors(chain = true)
public class CrmStatisticsPerformanceTargetActualRespVO {
@Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer month;
@Schema(description = "完成金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000.00")
private BigDecimal currentPrice;
}
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigBizTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - CRM 业绩目标完成情况 Request VO")
@Data
public class CrmStatisticsPerformanceTargetReqVO {
@Schema(description = "部门 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "部门 id 不能为空")
private Long deptId;
@Schema(description = "负责人用户 id", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1")
private Long userId;
@Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
private List<Long> userIds;
@Schema(description = "年份", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024")
@NotNull(message = "年份不能为空")
private Integer year;
@Schema(description = "目标类型",
requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
@NotNull(message = "目标类型不能为空")
@InEnum(value = CrmPerformanceConfigBizTypeEnum.class, message = "目标类型,必须是 {value}")
private Integer bizType;
}
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 业绩目标完成情况 Response VO")
@Data
@Accessors(chain = true)
public class CrmStatisticsPerformanceTargetRespVO {
@Schema(description = "月份", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer month;
@Schema(description = "目标金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000.00")
private BigDecimal targetPrice;
@Schema(description = "完成金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000.00")
private BigDecimal currentPrice;
@Schema(description = "完成率", requiredMode = Schema.RequiredMode.REQUIRED, example = "80.00")
private BigDecimal completionRate;
}
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 产品分类销售分析 Response VO")
@Data
public class CrmStatisticsProductCategoryRespVO {
@Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Long categoryId;
@Schema(description = "产品分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "软件")
private String categoryName;
@Schema(description = "合同数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
private Integer contractCount;
@Schema(description = "销售数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
private BigDecimal productCount;
@Schema(description = "销售金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "20000.00")
private BigDecimal productTotalPrice;
}
@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - CRM 产品分析 Request VO")
@Data
public class CrmStatisticsProductReqVO {
@Schema(description = "部门 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "部门 id 不能为空")
private Long deptId;
@Schema(description = "负责人用户 id", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1")
private Long userId;
@Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
private List<Long> userIds;
@Schema(description = "时间范围", requiredMode = Schema.RequiredMode.REQUIRED)
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@NotEmpty(message = "请选择时间范围")
@Size(min = 2, max = 2, message = "请选择时间范围")
private LocalDateTime[] times;
@Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "10")
private Long categoryId;
@Schema(description = "产品分类编号集合", hidden = true, example = "10")
private List<Long> categoryIds;
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "20")
private Long productId;
}
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - CRM 产品销售情况 Response VO")
@Data
public class CrmStatisticsProductSalesRespVO {
@Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Long categoryId;
@Schema(description = "产品分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "软件")
private String categoryName;
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
private Long productId;
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "CRM 专业版")
private String productName;
@Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30")
private Long contractId;
@Schema(description = "合同编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "HT20240101001")
private String contractNo;
@Schema(description = "合同名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "CRM 采购合同")
private String contractName;
@Schema(description = "负责人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long ownerUserId;
@Schema(description = "负责人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String ownerUserName;
@Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "40")
private Long customerId;
@Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试客户")
private String customerName;
@Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
private BigDecimal productPrice;
@Schema(description = "销售数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private BigDecimal productCount;
@Schema(description = "销售金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "200.00")
private BigDecimal productTotalPrice;
}
@@ -0,0 +1,107 @@
package cn.iocoder.yudao.module.crm.dal.dataobject.performance;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
* CRM 业绩目标 DO
*
* @author 芋道源码
*/
@TableName("crm_performance_config")
@KeySequence("crm_performance_config_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CrmPerformanceConfigDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 目标类型
*
* 枚举 {@link CrmPerformanceConfigBizTypeEnum}
*/
private Integer bizType;
/**
* 目标对象编号
*/
private Long objectId;
/**
* 目标对象类型
*
* 枚举 {@link CrmPerformanceConfigObjectTypeEnum}
*/
private Integer objectType;
/**
* 年份
*/
private Integer year;
/**
* 年度目标金额
*/
private BigDecimal yearTargetPrice;
/**
* 一月目标金额
*/
private BigDecimal januaryTargetPrice;
/**
* 二月目标金额
*/
private BigDecimal februaryTargetPrice;
/**
* 三月目标金额
*/
private BigDecimal marchTargetPrice;
/**
* 四月目标金额
*/
private BigDecimal aprilTargetPrice;
/**
* 五月目标金额
*/
private BigDecimal mayTargetPrice;
/**
* 六月目标金额
*/
private BigDecimal juneTargetPrice;
/**
* 七月目标金额
*/
private BigDecimal julyTargetPrice;
/**
* 八月目标金额
*/
private BigDecimal augustTargetPrice;
/**
* 九月目标金额
*/
private BigDecimal septemberTargetPrice;
/**
* 十月目标金额
*/
private BigDecimal octoberTargetPrice;
/**
* 十一月目标金额
*/
private BigDecimal novemberTargetPrice;
/**
* 十二月目标金额
*/
private BigDecimal decemberTargetPrice;
}
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.crm.dal.dataobject.permission;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* CRM 负责人变更记录 DO
*
* @author 芋道源码
*/
@TableName("crm_owner_record")
@KeySequence("crm_owner_record_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CrmOwnerRecordDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* CRM 业务类型
*
* 枚举 {@link CrmBizTypeEnum}
*/
private Integer bizType;
/**
* CRM 业务编号
*/
private Long bizId;
/**
* 变更前负责人
*/
private Long preOwnerUserId;
/**
* 变更后负责人
*/
private Long postOwnerUserId;
}
@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.crm.dal.mysql.performance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigPageReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.performance.CrmPerformanceConfigDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
* CRM 业绩目标配置 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface CrmPerformanceConfigMapper extends BaseMapperX<CrmPerformanceConfigDO> {
default PageResult<CrmPerformanceConfigDO> selectPage(CrmPerformanceConfigPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<CrmPerformanceConfigDO>()
.eqIfPresent(CrmPerformanceConfigDO::getObjectId, reqVO.getObjectId())
.eqIfPresent(CrmPerformanceConfigDO::getObjectType, reqVO.getObjectType())
.eqIfPresent(CrmPerformanceConfigDO::getYear, reqVO.getYear())
.eqIfPresent(CrmPerformanceConfigDO::getBizType, reqVO.getBizType())
.orderByDesc(CrmPerformanceConfigDO::getYear)
.orderByAsc(CrmPerformanceConfigDO::getObjectType)
.orderByAsc(CrmPerformanceConfigDO::getObjectId)
.orderByAsc(CrmPerformanceConfigDO::getBizType));
}
default CrmPerformanceConfigDO selectByObjectIdAndObjectTypeAndYearAndBizType(Long objectId, Integer objectType,
Integer year, Integer bizType) {
return selectOne(new LambdaQueryWrapperX<CrmPerformanceConfigDO>()
.eq(CrmPerformanceConfigDO::getObjectId, objectId)
.eq(CrmPerformanceConfigDO::getObjectType, objectType)
.eq(CrmPerformanceConfigDO::getYear, year)
.eq(CrmPerformanceConfigDO::getBizType, bizType));
}
default List<CrmPerformanceConfigDO> selectListByObjectIdsAndObjectTypeAndYearAndBizType(Collection<Long> objectIds,
Integer objectType,
Integer year,
Integer bizType) {
return selectList(new LambdaQueryWrapperX<CrmPerformanceConfigDO>()
.in(CrmPerformanceConfigDO::getObjectId, objectIds)
.eq(CrmPerformanceConfigDO::getObjectType, objectType)
.eq(CrmPerformanceConfigDO::getYear, year)
.eq(CrmPerformanceConfigDO::getBizType, bizType));
}
}
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.crm.dal.mysql.permission;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByUserRespVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmOwnerRecordDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* CRM 负责人变更记录 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface CrmOwnerRecordMapper extends BaseMapperX<CrmOwnerRecordDO> {
/**
* 进入公海客户数(按日期)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByDateRespVO> selectPoolCustomerPutCountByDate(CrmStatisticsCustomerReqVO reqVO);
/**
* 公海领取客户数(按日期)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByDateRespVO> selectPoolCustomerTakeCountByDate(CrmStatisticsCustomerReqVO reqVO);
/**
* 进入公海客户数(按用户)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByUserRespVO> selectPoolCustomerPutCountByUser(CrmStatisticsCustomerReqVO reqVO);
/**
* 公海领取客户数(按用户)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByUserRespVO> selectPoolCustomerTakeCountByUser(CrmStatisticsCustomerReqVO reqVO);
}
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetActualRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetReqVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* CRM 业绩目标完成情况 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface CrmStatisticsPerformanceTargetMapper {
/**
* 按月统计合同金额
*
* @param reqVO 请求参数
* @return 按月统计合同金额
*/
List<CrmStatisticsPerformanceTargetActualRespVO> selectContractActualByMonth(CrmStatisticsPerformanceTargetReqVO reqVO);
/**
* 按月统计回款金额
*
* @param reqVO 请求参数
* @return 按月统计回款金额
*/
List<CrmStatisticsPerformanceTargetActualRespVO> selectReceivableActualByMonth(CrmStatisticsPerformanceTargetReqVO reqVO);
}
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* CRM 产品分析 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface CrmStatisticsProductMapper {
/**
* 获取产品销售情况统计
*
* @param reqVO 请求参数
* @return 产品销售情况
*/
List<CrmStatisticsProductSalesRespVO> selectProductSalesList(CrmStatisticsProductReqVO reqVO);
/**
* 获取产品分类销售分析
*
* @param reqVO 请求参数
* @return 产品分类销售分析
*/
List<CrmStatisticsProductCategoryRespVO> selectProductCategorySummary(CrmStatisticsProductReqVO reqVO);
}
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.crm.enums.performance;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* CRM 业绩目标业务类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum CrmPerformanceConfigBizTypeEnum implements ArrayValuable<Integer> {
CRM_CONTRACT(CrmBizTypeEnum.CRM_CONTRACT.getType(), "销售目标"),
CRM_RECEIVABLE(CrmBizTypeEnum.CRM_RECEIVABLE.getType(), "回款目标");
public static final Integer[] ARRAYS = Arrays.stream(values())
.map(CrmPerformanceConfigBizTypeEnum::getBizType).toArray(Integer[]::new);
/**
* 业务类型
*/
private final Integer bizType;
/**
* 名称
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.crm.enums.performance;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* CRM 业绩目标对象类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum CrmPerformanceConfigObjectTypeEnum implements ArrayValuable<Integer> {
DEPT(2, "部门"),
USER(3, "员工");
public static final Integer[] ARRAYS = Arrays.stream(values())
.map(CrmPerformanceConfigObjectTypeEnum::getObjectType).toArray(Integer[]::new);
/**
* 对象类型
*/
private final Integer objectType;
/**
* 名称
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}
@@ -0,0 +1,80 @@
package cn.iocoder.yudao.module.crm.service.performance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.performance.CrmPerformanceConfigDO;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* CRM 业绩目标设置 Service 接口
*
* @author 芋道源码
*/
public interface CrmPerformanceConfigService {
/**
* 创建业绩目标
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createPerformanceConfig(@Valid CrmPerformanceConfigSaveReqVO createReqVO);
/**
* 更新业绩目标
*
* @param updateReqVO 更新信息
*/
void updatePerformanceConfig(@Valid CrmPerformanceConfigSaveReqVO updateReqVO);
/**
* 删除业绩目标
*
* @param id 编号
*/
void deletePerformanceConfig(Long id);
/**
* 获得业绩目标
*
* @param id 编号
* @return 业绩目标
*/
CrmPerformanceConfigDO getPerformanceConfig(Long id);
/**
* 获得业绩目标分页
*
* @param pageReqVO 分页查询
* @return 业绩目标分页
*/
PageResult<CrmPerformanceConfigDO> getPerformanceConfigPage(CrmPerformanceConfigPageReqVO pageReqVO);
/**
* 根据对象、年份、目标类型获得业绩目标配置
*
* @param objectId 对象编号
* @param objectType 对象类型
* @param year 年份
* @param bizType 业务类型
* @return 业绩目标配置
*/
CrmPerformanceConfigDO getPerformanceConfig(Long objectId, Integer objectType, Integer year, Integer bizType);
/**
* 根据对象数组、年份、目标类型获得业绩目标配置列表
*
* @param objectIds 对象编号数组
* @param objectType 对象类型
* @param year 年份
* @param bizType 业务类型
* @return 业绩目标配置列表
*/
List<CrmPerformanceConfigDO> getPerformanceConfigList(Collection<Long> objectIds, Integer objectType, Integer year,
Integer bizType);
}
@@ -0,0 +1,170 @@
package cn.iocoder.yudao.module.crm.service.performance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.performance.vo.config.CrmPerformanceConfigSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.performance.CrmPerformanceConfigDO;
import cn.iocoder.yudao.module.crm.dal.mysql.performance.CrmPerformanceConfigMapper;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static cn.hutool.core.collection.CollUtil.isEmpty;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static java.util.Collections.singleton;
/**
* CRM 业绩目标设置 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class CrmPerformanceConfigServiceImpl implements CrmPerformanceConfigService {
@Resource
private CrmPerformanceConfigMapper performanceConfigMapper;
@Resource
private DeptApi deptApi;
@Resource
private AdminUserApi adminUserApi;
@Override
public Long createPerformanceConfig(CrmPerformanceConfigSaveReqVO createReqVO) {
// 1.1 校验目标对象
validatePerformanceConfigObject(createReqVO.getObjectType(), createReqVO.getObjectId());
// 1.2 校验唯一
validatePerformanceConfigNotExists(null, createReqVO);
// 2. 插入
CrmPerformanceConfigDO performanceConfig = BeanUtils.toBean(createReqVO, CrmPerformanceConfigDO.class);
fillTargetPrice(performanceConfig);
performanceConfigMapper.insert(performanceConfig);
return performanceConfig.getId();
}
@Override
public void updatePerformanceConfig(CrmPerformanceConfigSaveReqVO updateReqVO) {
// 1.1 校验存在
validatePerformanceConfigExists(updateReqVO.getId());
// 1.2 校验目标对象
validatePerformanceConfigObject(updateReqVO.getObjectType(), updateReqVO.getObjectId());
// 1.3 校验唯一
validatePerformanceConfigNotExists(updateReqVO.getId(), updateReqVO);
// 2. 更新
CrmPerformanceConfigDO updateObj = BeanUtils.toBean(updateReqVO, CrmPerformanceConfigDO.class);
fillTargetPrice(updateObj);
performanceConfigMapper.updateById(updateObj);
}
@Override
public void deletePerformanceConfig(Long id) {
// 1. 校验存在
validatePerformanceConfigExists(id);
// 2. 删除
performanceConfigMapper.deleteById(id);
}
@Override
public CrmPerformanceConfigDO getPerformanceConfig(Long id) {
return validatePerformanceConfigExists(id);
}
@Override
public PageResult<CrmPerformanceConfigDO> getPerformanceConfigPage(CrmPerformanceConfigPageReqVO pageReqVO) {
return performanceConfigMapper.selectPage(pageReqVO);
}
private CrmPerformanceConfigDO validatePerformanceConfigExists(Long id) {
CrmPerformanceConfigDO performanceConfig = performanceConfigMapper.selectById(id);
if (performanceConfig == null) {
throw exception(PERFORMANCE_CONFIG_NOT_EXISTS);
}
return performanceConfig;
}
private void validatePerformanceConfigNotExists(Long id, CrmPerformanceConfigSaveReqVO reqVO) {
CrmPerformanceConfigDO performanceConfig = performanceConfigMapper.selectByObjectIdAndObjectTypeAndYearAndBizType(
reqVO.getObjectId(), reqVO.getObjectType(), reqVO.getYear(), reqVO.getBizType());
if (performanceConfig == null || Objects.equals(performanceConfig.getId(), id)) {
return;
}
throw exception(PERFORMANCE_CONFIG_EXISTS);
}
private void validatePerformanceConfigObject(Integer objectType, Long objectId) {
// 情况一:部门
if (Objects.equals(objectType, CrmPerformanceConfigObjectTypeEnum.DEPT.getObjectType())) {
deptApi.validateDeptList(singleton(objectId));
return;
}
// 情况二:员工
if (Objects.equals(objectType, CrmPerformanceConfigObjectTypeEnum.USER.getObjectType())) {
adminUserApi.validateUserList(singleton(objectId));
return;
}
throw exception(PERFORMANCE_CONFIG_TYPE_INVALID);
}
@Override
public CrmPerformanceConfigDO getPerformanceConfig(Long objectId, Integer objectType, Integer year, Integer bizType) {
return performanceConfigMapper.selectByObjectIdAndObjectTypeAndYearAndBizType(objectId, objectType, year, bizType);
}
@Override
public List<CrmPerformanceConfigDO> getPerformanceConfigList(Collection<Long> objectIds, Integer objectType,
Integer year, Integer bizType) {
if (isEmpty(objectIds)) {
return Collections.emptyList();
}
return performanceConfigMapper.selectListByObjectIdsAndObjectTypeAndYearAndBizType(objectIds, objectType,
year, bizType);
}
private void fillTargetPrice(CrmPerformanceConfigDO performanceConfig) {
// 1. 空值设置为 0
performanceConfig.setJanuaryTargetPrice(defaultIfNull(performanceConfig.getJanuaryTargetPrice()));
performanceConfig.setFebruaryTargetPrice(defaultIfNull(performanceConfig.getFebruaryTargetPrice()));
performanceConfig.setMarchTargetPrice(defaultIfNull(performanceConfig.getMarchTargetPrice()));
performanceConfig.setAprilTargetPrice(defaultIfNull(performanceConfig.getAprilTargetPrice()));
performanceConfig.setMayTargetPrice(defaultIfNull(performanceConfig.getMayTargetPrice()));
performanceConfig.setJuneTargetPrice(defaultIfNull(performanceConfig.getJuneTargetPrice()));
performanceConfig.setJulyTargetPrice(defaultIfNull(performanceConfig.getJulyTargetPrice()));
performanceConfig.setAugustTargetPrice(defaultIfNull(performanceConfig.getAugustTargetPrice()));
performanceConfig.setSeptemberTargetPrice(defaultIfNull(performanceConfig.getSeptemberTargetPrice()));
performanceConfig.setOctoberTargetPrice(defaultIfNull(performanceConfig.getOctoberTargetPrice()));
performanceConfig.setNovemberTargetPrice(defaultIfNull(performanceConfig.getNovemberTargetPrice()));
performanceConfig.setDecemberTargetPrice(defaultIfNull(performanceConfig.getDecemberTargetPrice()));
// 2. 计算年度目标
performanceConfig.setYearTargetPrice(performanceConfig.getJanuaryTargetPrice()
.add(performanceConfig.getFebruaryTargetPrice())
.add(performanceConfig.getMarchTargetPrice())
.add(performanceConfig.getAprilTargetPrice())
.add(performanceConfig.getMayTargetPrice())
.add(performanceConfig.getJuneTargetPrice())
.add(performanceConfig.getJulyTargetPrice())
.add(performanceConfig.getAugustTargetPrice())
.add(performanceConfig.getSeptemberTargetPrice())
.add(performanceConfig.getOctoberTargetPrice())
.add(performanceConfig.getNovemberTargetPrice())
.add(performanceConfig.getDecemberTargetPrice()));
}
private BigDecimal defaultIfNull(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
}
@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.crm.service.permission;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByUserRespVO;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmOwnerRecordCreateReqBO;
import javax.validation.Valid;
import java.util.List;
/**
* CRM 负责人变更记录 Service 接口
*
* @author 芋道源码
*/
public interface CrmOwnerRecordService {
/**
* 创建负责人变更记录
*
* @param createReqBO 创建信息
* @return 编号
*/
Long createOwnerRecord(@Valid CrmOwnerRecordCreateReqBO createReqBO);
/**
* 批量创建负责人变更记录
*
* @param createReqBOs 创建信息
*/
void createOwnerRecordList(@Valid List<CrmOwnerRecordCreateReqBO> createReqBOs);
/**
* 进入公海客户数(按日期)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByDateRespVO> getPoolCustomerPutCountByDate(CrmStatisticsCustomerReqVO reqVO);
/**
* 公海领取客户数(按日期)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByDateRespVO> getPoolCustomerTakeCountByDate(CrmStatisticsCustomerReqVO reqVO);
/**
* 进入公海客户数(按用户)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByUserRespVO> getPoolCustomerPutCountByUser(CrmStatisticsCustomerReqVO reqVO);
/**
* 公海领取客户数(按用户)
*
* @param reqVO 请求参数
* @return 统计数据
*/
List<CrmStatisticsPoolSummaryByUserRespVO> getPoolCustomerTakeCountByUser(CrmStatisticsCustomerReqVO reqVO);
}
@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.crm.service.permission;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByUserRespVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmOwnerRecordDO;
import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmOwnerRecordMapper;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmOwnerRecordCreateReqBO;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
/**
* CRM 负责人变更记录 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class CrmOwnerRecordServiceImpl implements CrmOwnerRecordService {
@Resource
private CrmOwnerRecordMapper ownerRecordMapper;
@Override
public Long createOwnerRecord(CrmOwnerRecordCreateReqBO createReqBO) {
CrmOwnerRecordDO ownerRecord = BeanUtils.toBean(createReqBO, CrmOwnerRecordDO.class);
ownerRecordMapper.insert(ownerRecord);
return ownerRecord.getId();
}
@Override
public void createOwnerRecordList(List<CrmOwnerRecordCreateReqBO> createReqBOs) {
if (CollUtil.isEmpty(createReqBOs)) {
return;
}
List<CrmOwnerRecordDO> list = BeanUtils.toBean(createReqBOs, CrmOwnerRecordDO.class);
ownerRecordMapper.insertBatch(list);
}
@Override
public List<CrmStatisticsPoolSummaryByDateRespVO> getPoolCustomerPutCountByDate(CrmStatisticsCustomerReqVO reqVO) {
return ownerRecordMapper.selectPoolCustomerPutCountByDate(reqVO);
}
@Override
public List<CrmStatisticsPoolSummaryByDateRespVO> getPoolCustomerTakeCountByDate(CrmStatisticsCustomerReqVO reqVO) {
return ownerRecordMapper.selectPoolCustomerTakeCountByDate(reqVO);
}
@Override
public List<CrmStatisticsPoolSummaryByUserRespVO> getPoolCustomerPutCountByUser(CrmStatisticsCustomerReqVO reqVO) {
return ownerRecordMapper.selectPoolCustomerPutCountByUser(reqVO);
}
@Override
public List<CrmStatisticsPoolSummaryByUserRespVO> getPoolCustomerTakeCountByUser(CrmStatisticsCustomerReqVO reqVO) {
return ownerRecordMapper.selectPoolCustomerTakeCountByUser(reqVO);
}
}
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.crm.service.permission.bo;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import javax.validation.constraints.NotNull;
import lombok.Data;
/**
* CRM 负责人变更记录 Create Req BO
*
* @author 芋道源码
*/
@Data
public class CrmOwnerRecordCreateReqBO {
/**
* CRM 业务类型
*/
@NotNull(message = "CRM 业务类型不能为空")
@InEnum(CrmBizTypeEnum.class)
private Integer bizType;
/**
* CRM 业务编号
*/
@NotNull(message = "CRM 业务编号不能为空")
private Long bizId;
/**
* 变更前负责人
*/
private Long preOwnerUserId;
/**
* 变更后负责人
*/
private Long postOwnerUserId;
}
@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.crm.service.statistics;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetRespVO;
import java.util.List;
/**
* CRM 业绩目标统计 Service 接口
*
* @author 芋道源码
*/
public interface CrmStatisticsPerformanceTargetService {
/**
* 获取业绩目标完成情况
*
* @param reqVO 请求参数
* @return 业绩目标完成情况
*/
List<CrmStatisticsPerformanceTargetRespVO> getPerformanceTargetSummary(CrmStatisticsPerformanceTargetReqVO reqVO);
}
@@ -0,0 +1,190 @@
package cn.iocoder.yudao.module.crm.service.statistics;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetActualRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetRespVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.performance.CrmPerformanceConfigDO;
import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsPerformanceTargetMapper;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.performance.CrmPerformanceConfigObjectTypeEnum;
import cn.iocoder.yudao.module.crm.service.performance.CrmPerformanceConfigService;
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 javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
* CRM 业绩目标统计 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class CrmStatisticsPerformanceTargetServiceImpl implements CrmStatisticsPerformanceTargetService {
@Resource
private CrmStatisticsPerformanceTargetMapper performanceTargetMapper;
@Resource
private CrmPerformanceConfigService performanceConfigService;
@Resource
private AdminUserApi adminUserApi;
@Resource
private DeptApi deptApi;
@Override
public List<CrmStatisticsPerformanceTargetRespVO> getPerformanceTargetSummary(CrmStatisticsPerformanceTargetReqVO reqVO) {
// 1. 获得用户编号数组
reqVO.setUserIds(getUserIds(reqVO));
if (CollUtil.isEmpty(reqVO.getUserIds())) {
return Collections.emptyList();
}
// 2.1 获得目标配置
List<CrmPerformanceConfigDO> performanceConfigs = getPerformanceConfigs(reqVO);
// 2.2 获得实际完成金额
List<CrmStatisticsPerformanceTargetActualRespVO> actualList = ObjUtil.equal(reqVO.getBizType(),
CrmPerformanceConfigBizTypeEnum.CRM_RECEIVABLE.getBizType())
? performanceTargetMapper.selectReceivableActualByMonth(reqVO)
: performanceTargetMapper.selectContractActualByMonth(reqVO);
Map<Integer, BigDecimal> actualMap = convertMap(actualList,
CrmStatisticsPerformanceTargetActualRespVO::getMonth,
CrmStatisticsPerformanceTargetActualRespVO::getCurrentPrice);
// 3. 组装数据返回
List<CrmStatisticsPerformanceTargetRespVO> result = new ArrayList<>();
for (int month = 1; month <= 12; month++) {
BigDecimal targetPrice = getMonthTargetPrice(performanceConfigs, month);
BigDecimal currentPrice = actualMap.getOrDefault(month, BigDecimal.ZERO);
result.add(new CrmStatisticsPerformanceTargetRespVO().setMonth(month).setTargetPrice(targetPrice)
.setCurrentPrice(currentPrice)
.setCompletionRate(calculateCompletionRate(currentPrice, targetPrice)));
}
return result;
}
private List<CrmPerformanceConfigDO> getPerformanceConfigs(CrmStatisticsPerformanceTargetReqVO reqVO) {
// 情况一:选中某个用户
if (ObjUtil.isNotNull(reqVO.getUserId())) {
CrmPerformanceConfigDO performanceConfig = performanceConfigService.getPerformanceConfig(reqVO.getUserId(),
CrmPerformanceConfigObjectTypeEnum.USER.getObjectType(), reqVO.getYear(), reqVO.getBizType());
return performanceConfig == null ? Collections.emptyList() : ListUtil.of(performanceConfig);
}
// 情况二:选中某个部门
return performanceConfigService.getPerformanceConfigList(getDeptIds(reqVO.getDeptId()),
CrmPerformanceConfigObjectTypeEnum.DEPT.getObjectType(), reqVO.getYear(), reqVO.getBizType());
}
private BigDecimal getMonthTargetPrice(List<CrmPerformanceConfigDO> performanceConfigs, int month) {
return performanceConfigs.stream()
.map(performanceConfig -> getMonthTargetPrice(performanceConfig, month))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@SuppressWarnings("EnhancedSwitchMigration")
private BigDecimal getMonthTargetPrice(CrmPerformanceConfigDO performanceConfig, int month) {
// 1. 无目标配置,返回 0
if (performanceConfig == null) {
return BigDecimal.ZERO;
}
// 2. 获得月份目标
BigDecimal targetPrice;
switch (month) {
case 1:
targetPrice = performanceConfig.getJanuaryTargetPrice();
break;
case 2:
targetPrice = performanceConfig.getFebruaryTargetPrice();
break;
case 3:
targetPrice = performanceConfig.getMarchTargetPrice();
break;
case 4:
targetPrice = performanceConfig.getAprilTargetPrice();
break;
case 5:
targetPrice = performanceConfig.getMayTargetPrice();
break;
case 6:
targetPrice = performanceConfig.getJuneTargetPrice();
break;
case 7:
targetPrice = performanceConfig.getJulyTargetPrice();
break;
case 8:
targetPrice = performanceConfig.getAugustTargetPrice();
break;
case 9:
targetPrice = performanceConfig.getSeptemberTargetPrice();
break;
case 10:
targetPrice = performanceConfig.getOctoberTargetPrice();
break;
case 11:
targetPrice = performanceConfig.getNovemberTargetPrice();
break;
case 12:
targetPrice = performanceConfig.getDecemberTargetPrice();
break;
default:
targetPrice = BigDecimal.ZERO;
}
return targetPrice != null ? targetPrice : BigDecimal.ZERO;
}
private BigDecimal calculateCompletionRate(BigDecimal currentPrice, BigDecimal targetPrice) {
if (targetPrice == null || targetPrice.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
return currentPrice.multiply(BigDecimal.valueOf(100)).divide(targetPrice, 2, RoundingMode.HALF_UP);
}
/**
* 获取用户编号数组
*
* 如果用户编号为空,则获得部门下的用户编号数组,包括子部门的所有用户编号
*
* @param reqVO 请求参数
* @return 用户编号数组
*/
private List<Long> getUserIds(CrmStatisticsPerformanceTargetReqVO reqVO) {
// 情况一:选中某个用户
if (ObjUtil.isNotNull(reqVO.getUserId())) {
return ListUtil.of(reqVO.getUserId());
}
// 情况二:选中某个部门
// 2.1 获得部门列表
List<Long> deptIds = getDeptIds(reqVO.getDeptId());
// 2.2 获得用户编号
return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
}
/**
* 获得部门编号数组,包括子部门
*
* @param deptId 部门编号
* @return 部门编号数组
*/
private List<Long> getDeptIds(Long deptId) {
List<Long> deptIds = convertList(deptApi.getChildDeptList(deptId), DeptRespDTO::getId);
deptIds.add(deptId);
return deptIds;
}
}
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.crm.service.statistics;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO;
import java.util.List;
/**
* CRM 产品分析 Service 接口
*
* @author 芋道源码
*/
public interface CrmStatisticsProductService {
/**
* 获取产品销售情况统计
*
* @param reqVO 请求参数
* @return 产品销售情况
*/
List<CrmStatisticsProductSalesRespVO> getProductSalesList(CrmStatisticsProductReqVO reqVO);
/**
* 获取产品分类销售分析
*
* @param reqVO 请求参数
* @return 产品分类销售分析
*/
List<CrmStatisticsProductCategoryRespVO> getProductCategorySummary(CrmStatisticsProductReqVO reqVO);
}
@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.crm.service.statistics;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.crm.controller.admin.product.vo.category.CrmProductCategoryListReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductCategoryDO;
import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsProductMapper;
import cn.iocoder.yudao.module.crm.service.product.CrmProductCategoryService;
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 javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* CRM 产品分析 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class CrmStatisticsProductServiceImpl implements CrmStatisticsProductService {
@Resource
private CrmStatisticsProductMapper productMapper;
@Resource
private AdminUserApi adminUserApi;
@Resource
private DeptApi deptApi;
@Resource
private CrmProductCategoryService productCategoryService;
@Override
public List<CrmStatisticsProductSalesRespVO> getProductSalesList(CrmStatisticsProductReqVO reqVO) {
reqVO.setUserIds(getUserIds(reqVO));
if (CollUtil.isEmpty(reqVO.getUserIds())) {
return Collections.emptyList();
}
setCategoryIds(reqVO);
return productMapper.selectProductSalesList(reqVO);
}
@Override
public List<CrmStatisticsProductCategoryRespVO> getProductCategorySummary(CrmStatisticsProductReqVO reqVO) {
reqVO.setUserIds(getUserIds(reqVO));
if (CollUtil.isEmpty(reqVO.getUserIds())) {
return Collections.emptyList();
}
setCategoryIds(reqVO);
return productMapper.selectProductCategorySummary(reqVO);
}
/**
* 设置产品分类编号数组,选择父分类时,包含子分类
*
* @param reqVO 请求参数
*/
private void setCategoryIds(CrmStatisticsProductReqVO reqVO) {
if (ObjUtil.isNull(reqVO.getCategoryId())) {
return;
}
List<Long> categoryIds = ListUtil.of(reqVO.getCategoryId());
List<CrmProductCategoryDO> categoryList = productCategoryService.getProductCategoryList(
new CrmProductCategoryListReqVO());
categoryIds.addAll(convertList(categoryList, CrmProductCategoryDO::getId,
category -> Objects.equals(category.getParentId(), reqVO.getCategoryId())));
reqVO.setCategoryIds(categoryIds);
}
/**
* 获取用户编号数组
*
* 如果用户编号为空,则获得部门下的用户编号数组,包括子部门的所有用户编号
*
* @param reqVO 请求参数
* @return 用户编号数组
*/
private List<Long> getUserIds(CrmStatisticsProductReqVO reqVO) {
// 情况一:选中某个用户
if (ObjUtil.isNotNull(reqVO.getUserId())) {
return ListUtil.of(reqVO.getUserId());
}
// 情况二:选中某个部门
// 2.1 获得部门列表
List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()), DeptRespDTO::getId);
deptIds.add(reqVO.getDeptId());
// 2.2 获得用户编号
return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
}
}
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmOwnerRecordMapper">
<select id="selectPoolCustomerPutCountByDate"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO">
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
COUNT(*) AS customer_put_count
FROM crm_owner_record
WHERE deleted = 0
AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
AND pre_owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND post_owner_user_id IS NULL
AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
GROUP BY time
</select>
<select id="selectPoolCustomerTakeCountByDate"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO">
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
COUNT(*) AS customer_take_count
FROM crm_owner_record
WHERE deleted = 0
AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
AND pre_owner_user_id IS NULL
AND post_owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
GROUP BY time
</select>
<select id="selectPoolCustomerPutCountByUser"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByUserRespVO">
SELECT pre_owner_user_id AS owner_user_id,
COUNT(*) AS customer_put_count
FROM crm_owner_record
WHERE deleted = 0
AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
AND pre_owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND post_owner_user_id IS NULL
AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
GROUP BY pre_owner_user_id
</select>
<select id="selectPoolCustomerTakeCountByUser"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByUserRespVO">
SELECT post_owner_user_id AS owner_user_id,
COUNT(*) AS customer_take_count
FROM crm_owner_record
WHERE deleted = 0
AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
AND pre_owner_user_id IS NULL
AND post_owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
GROUP BY post_owner_user_id
</select>
</mapper>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsPerformanceTargetMapper">
<resultMap id="CrmStatisticsPerformanceTargetActualResultMap"
type="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceTargetActualRespVO">
<result property="month" column="month"/>
<result property="currentPrice" column="current_price"/>
</resultMap>
<select id="selectContractActualByMonth"
resultMap="CrmStatisticsPerformanceTargetActualResultMap">
SELECT MONTH(order_date) AS month,
IFNULL(SUM(total_price), 0) AS current_price
FROM crm_contract
WHERE deleted = 0
AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
AND owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND YEAR(order_date) = #{year}
GROUP BY MONTH(order_date)
</select>
<select id="selectReceivableActualByMonth"
resultMap="CrmStatisticsPerformanceTargetActualResultMap">
SELECT MONTH(return_time) AS month,
IFNULL(SUM(price), 0) AS current_price
FROM crm_receivable
WHERE deleted = 0
AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
AND owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND YEAR(return_time) = #{year}
GROUP BY MONTH(return_time)
</select>
</mapper>
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsProductMapper">
<sql id="ProductStatisticsWhere">
contract.deleted = 0
AND contract_product.deleted = 0
AND product.deleted = 0
AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
AND contract.owner_user_id IN
<foreach collection="userIds" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
AND contract.order_date BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
<if test="categoryIds != null and categoryIds.size() > 0">
AND product.category_id IN
<foreach collection="categoryIds" item="categoryId" open="(" close=")" separator=",">
#{categoryId}
</foreach>
</if>
<if test="productId != null">
AND product.id = #{productId}
</if>
</sql>
<select id="selectProductSalesList"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO">
SELECT category.id AS category_id,
IFNULL(category.name, '未分类') AS category_name,
product.id AS product_id,
product.name AS product_name,
contract.id AS contract_id,
contract.no AS contract_no,
contract.name AS contract_name,
contract.owner_user_id,
owner.nickname AS owner_user_name,
customer.id AS customer_id,
customer.name AS customer_name,
contract_product.product_price,
contract_product.count AS product_count,
contract_product.total_price AS product_total_price
FROM crm_contract_product AS contract_product
INNER JOIN crm_contract AS contract ON contract_product.contract_id = contract.id
INNER JOIN crm_product AS product ON contract_product.product_id = product.id
LEFT JOIN crm_product_category AS category ON product.category_id = category.id AND category.deleted = 0
LEFT JOIN crm_customer AS customer ON contract.customer_id = customer.id AND customer.deleted = 0
LEFT JOIN system_users AS owner ON contract.owner_user_id = owner.id AND owner.deleted = 0
WHERE <include refid="ProductStatisticsWhere"/>
ORDER BY category.id ASC, product.id ASC, contract.order_date DESC
</select>
<select id="selectProductCategorySummary"
resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO">
SELECT category.id AS category_id,
IFNULL(category.name, '未分类') AS category_name,
COUNT(DISTINCT contract.id) AS contract_count,
IFNULL(SUM(contract_product.count), 0) AS product_count,
IFNULL(SUM(contract_product.total_price), 0) AS product_total_price
FROM crm_contract_product AS contract_product
INNER JOIN crm_contract AS contract ON contract_product.contract_id = contract.id
INNER JOIN crm_product AS product ON contract_product.product_id = product.id
LEFT JOIN crm_product_category AS category ON product.category_id = category.id AND category.deleted = 0
WHERE <include refid="ProductStatisticsWhere"/>
GROUP BY category.id, category.name
ORDER BY product_count DESC, product_total_price DESC
</select>
</mapper>
@@ -48,6 +48,10 @@ public interface ErrorCodeConstants {
ErrorCode CODEGEN_MASTER_TABLE_NOT_EXISTS = new ErrorCode(1_001_004_010, "主表(id={})定义不存在,请检查");
ErrorCode CODEGEN_SUB_COLUMN_NOT_EXISTS = new ErrorCode(1_001_004_011, "子表的字段(id={})不存在,请检查");
ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE = new ErrorCode(1_001_004_012, "主表生成代码失败,原因:它没有子表");
ErrorCode CODEGEN_MASTER_TABLE_NAME_DUPLICATE = new ErrorCode(1_001_004_013,
"主子表规范化类名({})重复,请调整主表或子表类名");
ErrorCode CODEGEN_MASTER_TABLE_FIELD_DUPLICATE = new ErrorCode(1_001_004_014,
"主子表属性名({})重复,请调整主表字段、子表类名或关联关系");
// ========== 文件配置 1-001-006-000 ==========
ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_006_000, "文件配置不存在");
@@ -48,6 +48,9 @@ import java.util.*;
import static cn.hutool.core.map.MapUtil.getStr;
import static cn.hutool.core.text.CharSequenceUtil.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.CODEGEN_MASTER_TABLE_FIELD_DUPLICATE;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.CODEGEN_MASTER_TABLE_NAME_DUPLICATE;
/**
* 代码生成的引擎,用于具体生成代码
@@ -152,6 +155,16 @@ public class CodegenEngine {
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/form/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail/index.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/index_tree.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("components/breadcrumb_tree.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/components/breadcrumb.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail_master.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail_erp.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/form_sub_erp.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/${subSimpleClassName_strikeCase}/form/index.vue"))
// VUE3_VBEN2_ANTD_SCHEMA
.put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
@@ -380,12 +393,17 @@ public class CodegenEngine {
// 2. 执行生成
Map<String, String> result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序
Map<String, String> generatedSources = Maps.newHashMapWithExpectedSize(templates.size());
templates.forEach((vmPath, filePath) -> {
// 2.1 特殊:主子表专属逻辑
if (isSubTemplate(vmPath)) {
generateSubCode(table, subTables, result, vmPath, filePath, bindingMap);
// 2.1 特殊:主子表、Uniapp 树表使用独立页面模版
if (isVue3AdminUniappTemplate(vmPath) && !matchVue3AdminUniappTemplate(table, vmPath)) {
return;
// 2.2 特殊:树表专属逻辑
}
// 2.2 特殊:主子表专属逻辑
if (isSubTemplate(vmPath)) {
generateSubCode(table, subTables, result, generatedSources, vmPath, filePath, bindingMap);
return;
// 2.3 特殊:树表专属逻辑
} else if (isPageReqVOTemplate(vmPath)) {
// 减少多余的类生成,例如说 PageVO.java 类
if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
@@ -402,23 +420,36 @@ public class CodegenEngine {
return;
}
}
// 2.3 默认生成
generateCode(result, vmPath, filePath, bindingMap);
// 2.4 默认生成
generateCode(result, generatedSources, vmPath, filePath, bindingMap);
});
return result;
}
private void generateCode(Map<String, String> result, String vmPath,
private void generateCode(Map<String, String> result, Map<String, String> generatedSources, String vmPath,
String filePath, Map<String, Object> bindingMap) {
filePath = formatFilePath(filePath, bindingMap);
registerGeneratedSource(generatedSources, filePath, vmPath, bindingMap);
String content = templateEngine.getTemplate(vmPath).render(bindingMap);
// 格式化代码
content = prettyCode(content, vmPath);
result.put(filePath, content);
}
@VisibleForTesting
static void registerGeneratedSource(Map<String, String> generatedSources, String filePath, String vmPath,
Map<String, Object> bindingMap) {
String source = vmPath
+ (bindingMap.containsKey("subIndex") ? "[subIndex=" + bindingMap.get("subIndex") + "]" : "");
String previousSource = generatedSources.putIfAbsent(filePath, source);
if (previousSource != null) {
throw new IllegalStateException(String.format("生成文件路径重复:%s,来源模板:%s、%s",
filePath, previousSource, source));
}
}
private void generateSubCode(CodegenTableDO table, List<CodegenTableDO> subTables,
Map<String, String> result, String vmPath,
Map<String, String> result, Map<String, String> generatedSources, String vmPath,
String filePath, Map<String, Object> bindingMap) {
// 没有子表,所以不生成
if (CollUtil.isEmpty(subTables)) {
@@ -441,7 +472,7 @@ public class CodegenEngine {
// 逐个生成
for (int i = 0; i < subTables.size(); i++) {
bindingMap.put("subIndex", i);
generateCode(result, vmPath, filePath, bindingMap);
generateCode(result, generatedSources, vmPath, filePath, bindingMap);
}
bindingMap.remove("subIndex");
}
@@ -482,6 +513,13 @@ public class CodegenEngine {
if (StrUtil.count(content, "DICT_TYPE.") == 0) {
content = StrUtils.removeLineContains(content, "DICT_TYPE");
}
// Vue3 Admin UniApp:清理空白行,并补充文件末尾换行,满足 ESLint 格式要求
if (vmPath.contains("vue3_admin_uniapp")) {
content = content.replaceAll("(?m)^[\\t ]+$", "");
if (!content.endsWith("\n")) {
content += "\n";
}
}
return content;
}
@@ -533,6 +571,14 @@ public class CodegenEngine {
List<String> subClassNameVars = new ArrayList<>();
List<String> simpleClassNameUnderlineCases = new ArrayList<>();
List<String> subSimpleClassNameStrikeCases = new ArrayList<>();
Set<String> masterSymbols = new HashSet<>();
masterSymbols.add(simpleClassName);
boolean mergeSubTableFields = ObjectUtils.equalsAny(table.getTemplateType(),
CodegenTemplateTypeEnum.MASTER_NORMAL.getType(), CodegenTemplateTypeEnum.MASTER_INNER.getType());
Set<String> masterFieldSymbols = new HashSet<>();
if (mergeSubTableFields) {
columns.forEach(column -> masterFieldSymbols.add(column.getJavaField()));
}
for (int i = 0; i < subTables.size(); i++) {
CodegenTableDO subTable = subTables.get(i);
List<CodegenColumnDO> subColumns = subColumnsList.get(i);
@@ -543,9 +589,18 @@ public class CodegenEngine {
subJoinColumnStrikeCases.add(toSymbolCase(subColumn.getJavaField(), '-')); // 将 DictType 转换成 dict-type
// className 相关
String subSimpleClassName = removePrefix(subTable.getClassName(), upperFirst(subTable.getModuleName()));
if (!masterSymbols.add(subSimpleClassName)) {
throw exception(CODEGEN_MASTER_TABLE_NAME_DUPLICATE, subSimpleClassName);
}
String subClassNameVar = lowerFirst(subSimpleClassName);
String subFieldName = subClassNameVar
+ (Boolean.TRUE.equals(subTable.getSubJoinMany()) ? "s" : "");
if (mergeSubTableFields && !masterFieldSymbols.add(subFieldName)) {
throw exception(CODEGEN_MASTER_TABLE_FIELD_DUPLICATE, subFieldName);
}
subSimpleClassNames.add(subSimpleClassName);
simpleClassNameUnderlineCases.add(toUnderlineCase(subSimpleClassName)); // 将 DictType 转换成 dict_type
subClassNameVars.add(lowerFirst(subSimpleClassName)); // 将 DictType 转换成 dictType,用于变量
subClassNameVars.add(subClassNameVar); // 将 DictType 转换成 dictType,用于变量
subSimpleClassNameStrikeCases.add(toSymbolCase(subSimpleClassName, '-')); // 将 DictType 转换成 dict-type
}
bindingMap.put("subPrimaryColumns", subPrimaryColumns);
@@ -734,6 +789,33 @@ public class CodegenEngine {
return path.contains("_sub");
}
private static boolean isVue3AdminUniappTemplate(String path) {
return path.contains("codegen/vue3_admin_uniapp/");
}
/**
* 匹配 Uniapp 不同表类型的专用页面模版
*/
private static boolean matchVue3AdminUniappTemplate(CodegenTableDO table, String path) {
boolean tree = CodegenTemplateTypeEnum.isTree(table.getTemplateType());
boolean masterSubmit = ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_NORMAL.getType())
|| ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_INNER.getType());
boolean masterErp = ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_ERP.getType());
if (path.contains("_tree")) {
return tree;
}
if (path.contains("detail_master")) {
return masterSubmit;
}
if (path.contains("_erp")) {
return masterErp;
}
if (tree && path.endsWith("views/index.vue.vm")) {
return false;
}
return !(masterSubmit || masterErp) || !path.endsWith("views/detail/index.vue.vm");
}
private static boolean isPageReqVOTemplate(String path) {
return path.contains("pageReqVO");
}
@@ -1,36 +1,80 @@
#if ($table.templateType != 2)
import type { PageParam, PageResult } from '@/http/types'
#end
import { http } from '@/http/http'
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
#set ($javaType = $column.javaType.toLowerCase())
#set ($javaFieldLower = $column.javaField.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
/** ${subTable.classComment}信息 */
export interface ${subSimpleClassName} {
#foreach ($column in $subColumns)
#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
#set ($javaType = $column.javaType.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey || ($table.templateType != 11 && $column.id == $subJoinColumn.id))
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}#if($optional)?#end: number
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}#if($optional)?#end: Date
#elseif(${javaType} == "boolean")
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}#if($optional)?#end: number
#elseif(${javaType} == "boolean")
${column.javaField}#if($optional)?#end: boolean
#else
#else
${column.javaField}#if($optional)?#end: string
#end
#end
#end
#end
#end
}
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
#set ($javaType = $column.javaType.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey)
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}#if($optional)?#end: number
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}#if($optional)?#end: number
#elseif(${javaType} == "boolean")
${column.javaField}#if($optional)?#end: boolean
#else
${column.javaField}#if($optional)?#end: string
#end
#end
#end
#if ($table.templateType == 2)
children?: ${simpleClassName}[]
#end
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
${subClassNameVar}#if($subTable.subJoinMany)s#end?: ${subSimpleClassName}#if($subTable.subJoinMany)[]#else | null#end
#end
#end
}
#if ($table.templateType == 2)
/** 获取${table.classComment}列表 */
export function get${simpleClassName}List(params?: Record<string, any>) {
return http.get<${simpleClassName}[]>('/${table.moduleName}/${simpleClassName_strikeCase}/list', params)
}
#else
/** 获取${table.classComment}分页列表 */
export function get${simpleClassName}Page(params: PageParam) {
return http.get<PageResult<${simpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/page', params)
}
#end
/** 获取${table.classComment}详情 */
export function get${simpleClassName}(id: ${primaryTsType}) {
@@ -39,7 +83,7 @@ export function get${simpleClassName}(id: ${primaryTsType}) {
/** 创建${table.classComment} */
export function create${simpleClassName}(data: ${simpleClassName}) {
return http.post<number>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data)
return http.post<${primaryTsType}>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data)
}
/** 更新${table.classComment} */
@@ -51,3 +95,65 @@ export function update${simpleClassName}(data: ${simpleClassName}) {
export function delete${simpleClassName}(id: ${primaryTsType}) {
return http.delete<boolean>(`/${table.moduleName}/${simpleClassName_strikeCase}/delete?id=${id}`)
}
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase())
#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "float" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte")
#set ($subPrimaryTsType = "number")
#else
#set ($subPrimaryTsType = "string")
#end
#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase())
#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "float" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte")
#set ($subJoinTsType = "number")
#else
#set ($subJoinTsType = "string")
#end
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
// ==================== 子表:${subTable.classComment} ====================
#if ($table.templateType == 11)
/** 获取${subTable.classComment}分页列表 */
export function get${subSimpleClassName}Page(params: PageParam & { ${subJoinColumn.javaField}: ${subJoinTsType} }) {
return http.get<PageResult<${subSimpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/page', params)
}
/** 获取${subTable.classComment}详情 */
export function get${subSimpleClassName}(id: ${subPrimaryTsType}) {
return http.get<${subSimpleClassName}>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/get?id=${id}`)
}
/** 创建${subTable.classComment} */
export function create${subSimpleClassName}(data: ${subSimpleClassName}) {
return http.post<${subPrimaryTsType}>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/create', data)
}
/** 更新${subTable.classComment} */
export function update${subSimpleClassName}(data: ${subSimpleClassName}) {
return http.put<boolean>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/update', data)
}
/** 删除${subTable.classComment} */
export function delete${subSimpleClassName}(id: ${subPrimaryTsType}) {
return http.delete<boolean>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/delete?id=${id}`)
}
#elseif ($subTable.subJoinMany)
/** 获取${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: ${subJoinTsType}) {
return http.get<${subSimpleClassName}[]>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=#[[${]]#${subJoinColumn.javaField}#[[}]]#`)
}
#else
/** 获取${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: ${subJoinTsType}) {
return http.get<${subSimpleClassName} | null>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=#[[${]]#${subJoinColumn.javaField}#[[}]]#`)
}
#end
#end
@@ -0,0 +1,87 @@
<template>
<view class="bg-white px-24rpx py-16rpx">
<scroll-view scroll-x class="whitespace-nowrap">
<view class="inline-flex items-center">
<view
class="flex items-center text-28rpx"
:class="items.length > 0 ? 'text-[#1890ff]' : 'text-[#333]'"
@click="handleClick(-1)"
>
<text>全部${table.classComment}</text>
</view>
<template v-for="(item, index) in items" :key="item.id">
<wd-icon name="arrow-right" size="12px" color="#999" custom-class="mx-8rpx" />
<view
class="flex items-center text-28rpx"
:class="index < items.length - 1 ? 'text-[#1890ff]' : 'text-[#333]'"
@click="handleClick(index)"
>
<text>{{ item.name }}</text>
</view>
</template>
</view>
</scroll-view>
</view>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
interface BreadcrumbItem {
id: number
name: string
}
const props = defineProps<{
modelValue: number
}>()
const emit = defineEmits<{
'update:modelValue': [value: number]
}>()
const items = ref<BreadcrumbItem[]>([]) // 面包屑数据
/** 监听外部值变化 */
watch(() => props.modelValue, (value) => {
if (value === 0) {
items.value = []
}
})
/** 点击面包屑 */
function handleClick(index: number) {
if (index === -1) {
reset()
return
}
if (index < items.value.length - 1) {
items.value = items.value.slice(0, index + 1)
emit('update:modelValue', items.value[index].id)
}
}
/** 进入子层级 */
function enter(item: BreadcrumbItem) {
items.value.push(item)
emit('update:modelValue', item.id)
}
/** 返回上一层级 */
function back(): boolean {
if (items.value.length === 0) {
return false
}
items.value.pop()
emit('update:modelValue', items.value.at(-1)?.id ?? 0)
return true
}
/** 重置面包屑 */
function reset() {
items.value = []
emit('update:modelValue', 0)
}
defineExpose({ back, enter, reset })
</script>
@@ -42,23 +42,12 @@
#elseif ($column.htmlType == "datetime" && $listOperationCondition == "BETWEEN")
<yd-search-date-range v-model="formData.${javaField}" label="${comment}" />
#elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $dictType && "" != $dictType)
<view class="yd-search-form-item">
<view class="yd-search-form-label">
${comment}
</view>
<wd-radio-group v-model="formData.${javaField}" type="button">
<wd-radio :value="-1">
全部
</wd-radio>
<wd-radio
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</view>
<yd-search-picker
v-model="formData.${javaField}"
label="${comment}"
:columns="$dictMethod(DICT_TYPE.$dictType.toUpperCase())"
all-option
/>
#else
<view class="yd-search-form-item">
<view class="yd-search-form-label">
@@ -150,13 +139,13 @@ const formData = reactive({
${column.javaField}: [undefined, undefined] as [number | undefined, number | undefined],
#elseif ($column.dictType && "" != $column.dictType)
#if(${javaType} == "string")
${column.javaField}: -1 as -1 | string, // -1 表示全部
${column.javaField}: undefined as string | undefined,
#elseif(${javaType} == "boolean")
${column.javaField}: -1 as -1 | boolean, // -1 表示全部
${column.javaField}: undefined as boolean | undefined,
#else
${column.javaField}: -1, // -1 表示全部
${column.javaField}: undefined as number | undefined,
#end
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}: undefined as number | undefined,
#elseif(${javaType} == "boolean")
${column.javaField}: undefined as boolean | undefined,
@@ -181,11 +170,11 @@ const placeholder = computed(() => {
conditions.push(`${comment}:#[[${]]#formatDate(formData.${javaField}[0])#[[}]]#~#[[${]]#formatDate(formData.${javaField}[1])#[[}]]#`)
}
#elseif ($dictType && "" != $dictType)
if (formData.${javaField} !== -1) {
if (formData.${javaField} !== undefined) {
conditions.push(`${comment}:#[[${]]#getDictLabel(DICT_TYPE.${dictType.toUpperCase()}, formData.${javaField})#[[}]]#`)
}
#else
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte" || ${javaType} == "boolean")
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte" || ${javaType} == "boolean")
if (formData.${javaField} !== undefined) {
conditions.push(`${comment}:#[[${]]#formData.${javaField}#[[}]]#`)
}
@@ -207,9 +196,7 @@ function handleSearch() {
...formData,
#foreach($column in $columns)
#if ($column.listOperation)
#if ($column.dictType && "" != $column.dictType)
${column.javaField}: formData.${column.javaField} === -1 ? undefined : formData.${column.javaField},
#elseif ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
#if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
${column.javaField}: formatDateRange(formData.${column.javaField}),
#end
#end
@@ -224,7 +211,7 @@ function handleReset() {
#if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
formData.${column.javaField} = [undefined, undefined]
#elseif ($column.dictType && "" != $column.dictType)
formData.${column.javaField} = -1
formData.${column.javaField} = undefined
#else
formData.${column.javaField} = undefined
#end
@@ -39,7 +39,7 @@
</wd-button>
<wd-button
v-if="hasAccessByCodes(['${permissionPrefix}:delete'])"
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
>
删除
</wd-button>
@@ -49,6 +49,12 @@
</template>
<script lang="ts" setup>
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
#set ($hasDict = 0)
#foreach($column in $columns)
#if ($hasDict == 0 && $column.dictType && "" != $column.dictType)
@@ -64,6 +70,7 @@
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { onUnload } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { useDialog } from '@wot-ui/ui/components/wd-dialog'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { delete${simpleClassName}, get${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
@@ -76,7 +83,7 @@ import { formatDateTime } from '@/utils/date'
#end
const props = defineProps<{
id?: number | any
id?: ${primaryTsType}
}>()
definePage({
@@ -87,6 +94,7 @@ definePage({
})
const { hasAccessByCodes } = useAccess()
const dialog = useDialog()
const toast = useToast()
const formData = ref<${simpleClassName}>() // 详情数据
const deleting = ref(false) // 删除状态
@@ -117,28 +125,27 @@ function handleEdit() {
}
/** 删除${table.classComment} */
function handleDelete() {
async function handleDelete() {
if (!props.id) {
return
}
uni.showModal({
title: '提示',
content: '确定要删除该${table.classComment}吗?',
success: async (res) => {
if (!res.confirm) {
return
}
deleting.value = true
try {
await delete${simpleClassName}(props.id)
toast.success('删除成功')
uni.$emit('${table.moduleName}:${table.businessName}:reload')
delay(handleBack)
} finally {
deleting.value = false
}
},
})
try {
await dialog.confirm({
title: '提示',
msg: '确定要删除该${table.classComment}吗?',
})
} catch {
return
}
deleting.value = true
try {
await delete${simpleClassName}(props.id)
toast.success('删除成功')
uni.$emit('${table.moduleName}:${table.businessName}:reload')
delay(handleBack)
} finally {
deleting.value = false
}
}
/** 初始化 */
@@ -0,0 +1,258 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar title="${table.classComment}详情" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
<view class="pb-160rpx">
<!-- 主表详情 -->
<wd-cell-group border title="${table.classComment}信息">
#foreach($column in $columns)
#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
#if ($column.dictType && "" != $column.dictType)
<wd-cell title="${column.columnComment}">
<dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${column.javaField}" />
</wd-cell>
#elseif ($column.javaType == "LocalDateTime")
<wd-cell title="${column.columnComment}" :value="formatDateTime(formData?.${column.javaField}) || '-'" />
#else
<wd-cell title="${column.columnComment}" :value="formData?.${column.javaField} ?? '-'" />
#end
#end
#end
</wd-cell-group>
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subColumns = $subColumnsList.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
#set ($pageSize = 10)
#if (!$subTable.subJoinMany)
#set ($pageSize = 1)
#end
<!-- 子表:${subTable.classComment} -->
<view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
<text class="text-28rpx text-[#333] font-semibold">${subTable.classComment}({{ ${subClassNameVar}Total }})</text>
<wd-button
#if (!$subTable.subJoinMany)
v-if="${subClassNameVar}Total === 0 && hasAccessByCodes(['${permissionPrefix}:create'])"
#else
v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
#end
size="small" type="primary" variant="plain" @click="open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}()"
>
添加
</wd-button>
</view>
<view class="px-24rpx">
<z-paging
ref="${subClassNameVar}PagingRef"
v-model="${subClassNameVar}List"
:fixed="false"
:auto="false"
height="560rpx"
:default-page-size="${pageSize}"
:refresher-enabled="false"
:inside-more="true"
:to-bottom-loading-more-enabled="false"
loading-more-default-text="点击加载更多"
empty-view-text="暂无${subTable.classComment}"
@query="query${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}List"
>
<view
v-for="item in ${subClassNameVar}List"
:key="item.${subPrimaryColumn.javaField}"
class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
#foreach ($column in $subColumns)
#if ($column.listOperationResult && !$column.primaryKey)
<view class="mb-8rpx text-28rpx text-[#666]">
${column.columnComment}:{{ item.${column.javaField} ?? '-' }}
</view>
#end
#end
<view class="mt-16rpx flex justify-end gap-16rpx">
<wd-button size="small" variant="plain" @click="open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item)">
编辑
</wd-button>
<wd-button size="small" type="danger" variant="plain" @click="remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item)">
删除
</wd-button>
</view>
</view>
</z-paging>
</view>
#end
</view>
<!-- 底部操作按钮 -->
<view class="yd-detail-footer">
<view class="yd-detail-footer-actions">
<wd-button v-if="hasAccessByCodes(['${permissionPrefix}:update'])" class="flex-1" type="warning" @click="handleEdit">
编辑
</wd-button>
<wd-button v-if="hasAccessByCodes(['${permissionPrefix}:delete'])" class="flex-1" type="danger" :loading="deleting" @click="handleDelete">
删除
</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
#set ($hasDict = 0)
#set ($hasDateTime = 0)
#foreach($column in $columns)
#if ($column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#end
#if ($column.javaType == "LocalDateTime")
#set ($hasDateTime = 1)
#end
#end
import type {
${simpleClassName},
#foreach ($subSimpleClassName in $subSimpleClassNames)
${subSimpleClassName},
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { onShow } from '@dcloudio/uni-app'
import { nextTick, ref } from 'vue'
import { useDialog } from '@wot-ui/ui/components/wd-dialog'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import {
delete${simpleClassName},
#foreach ($subSimpleClassName in $subSimpleClassNames)
delete${subSimpleClassName},
#end
get${simpleClassName},
#foreach ($subSimpleClassName in $subSimpleClassNames)
get${subSimpleClassName}Page,
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
import { delay, navigateBackPlus } from '@/utils'
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDateTime == 1)
import { formatDateTime } from '@/utils/date'
#end
const props = defineProps<{ id?: ${primaryTsType} }>()
definePage({
style: { navigationBarTitleText: '', navigationStyle: 'custom' },
})
const { hasAccessByCodes } = useAccess()
const dialog = useDialog()
const toast = useToast()
const formData = ref<${simpleClassName}>() // 主表数据
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
const ${subClassNameVar}List = ref<${subSimpleClassName}[]>([]) // ${subTable.classComment}列表
const ${subClassNameVar}Total = ref(0) // ${subTable.classComment}总数
const ${subClassNameVar}PagingRef = ref<any>() // ${subTable.classComment}分页组件引用
#end
const deleting = ref(false) // 主表删除状态
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
}
/** 加载主表 */
async function loadMain() {
if (props.id) {
formData.value = await get${simpleClassName}(props.id)
}
}
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
/** 查询${subTable.classComment}列表 */
async function query${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}List(pageNo: number, pageSize: number) {
if (!props.id) {
${subClassNameVar}PagingRef.value?.completeByTotal([], 0)
return
}
try {
const data = await get${subSimpleClassName}Page({ ${subJoinColumn.javaField}: props.id, pageNo, pageSize })
${subClassNameVar}Total.value = data.total
${subClassNameVar}PagingRef.value?.completeByTotal(data.list, data.total)
} catch {
${subClassNameVar}PagingRef.value?.complete(false)
}
}
/** 打开${subTable.classComment}表单 */
function open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item?: ${subSimpleClassName}) {
const query = [
`${subJoinColumn.javaField}=#[[${]]#props.id#[[}]]#`,
item?.${subPrimaryColumn.javaField} ? `id=#[[${]]#item.${subPrimaryColumn.javaField}#[[}]]#` : '',
].filter(Boolean).join('&')
uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/${subSimpleClassName_strikeCases.get($index)}/form/index?#[[${]]#query#[[}]]#` })
}
/** 删除${subTable.classComment} */
async function remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item: ${subSimpleClassName}) {
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该${subTable.classComment}吗?' })
} catch {
return
}
await delete${subSimpleClassName}(item.${subPrimaryColumn.javaField}!)
toast.success('删除成功')
${subClassNameVar}PagingRef.value?.reload()
}
#end
/** 编辑主表 */
function handleEdit() {
uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?id=#[[${]]#props.id#[[}]]#` })
}
/** 删除主表 */
async function handleDelete() {
if (!props.id) {
return
}
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该${table.classComment}吗?' })
} catch {
return
}
deleting.value = true
try {
await delete${simpleClassName}(props.id)
toast.success('删除成功')
uni.$emit('${table.moduleName}:${table.businessName}:reload')
delay(handleBack)
} finally {
deleting.value = false
}
}
/** 进入页面加载(含编辑和子表返回后刷新) */
onShow(() => {
loadMain()
nextTick(() => {
#foreach ($subClassNameVar in $subClassNameVars)
${subClassNameVar}PagingRef.value?.reload()
#end
})
})
</script>
@@ -0,0 +1,239 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="${table.classComment}详情"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<view class="pb-160rpx">
<!-- 主表详情 -->
<wd-cell-group border title="${table.classComment}信息">
#foreach($column in $columns)
#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
#if ($column.dictType && "" != $column.dictType)
<wd-cell title="${column.columnComment}">
<dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${column.javaField}" />
</wd-cell>
#elseif ($column.javaType == "LocalDateTime")
<wd-cell title="${column.columnComment}" :value="formatDateTime(formData?.${column.javaField}) || '-'" />
#else
<wd-cell title="${column.columnComment}" :value="formData?.${column.javaField} ?? '-'" />
#end
#end
#end
</wd-cell-group>
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subColumns = $subColumnsList.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
<!-- 子表:${subTable.classComment} -->
<view class="mt-20rpx px-24rpx py-16rpx text-28rpx text-[#999]">
${subTable.classComment}#if($subTable.subJoinMany)({{ ${subClassNameVar}s.length }})#end
</view>
<view class="px-24rpx">
#if ($subTable.subJoinMany)
<view v-if="!${subClassNameVar}s.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
暂无${subTable.classComment}
</view>
<view
v-for="item in ${subClassNameVar}s"
:key="item.${subPrimaryColumn.javaField}"
class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
#foreach ($column in $subColumns)
#if ($column.listOperationResult && !$column.primaryKey)
<view class="mb-8rpx text-28rpx text-[#666]">
${column.columnComment}:{{ item.${column.javaField} ?? '-' }}
</view>
#end
#end
</view>
#else
<view class="rounded-12rpx bg-white p-24rpx shadow-sm">
<template v-if="${subClassNameVar}">
#foreach ($column in $subColumns)
#if ($column.listOperationResult && !$column.primaryKey)
<view class="mb-8rpx text-28rpx text-[#666]">
${column.columnComment}:{{ ${subClassNameVar}.${column.javaField} ?? '-' }}
</view>
#end
#end
</template>
<view v-else class="text-center text-26rpx text-[#999]">
暂无${subTable.classComment}
</view>
</view>
#end
</view>
#end
</view>
<!-- 底部操作按钮 -->
<view class="yd-detail-footer">
<view class="yd-detail-footer-actions">
<wd-button
v-if="hasAccessByCodes(['${permissionPrefix}:update'])"
class="flex-1" type="warning" @click="handleEdit"
>
编辑
</wd-button>
<wd-button
v-if="hasAccessByCodes(['${permissionPrefix}:delete'])"
class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
>
删除
</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
#set ($hasDict = 0)
#set ($hasDateTime = 0)
#foreach($column in $columns)
#if ($column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#end
#if ($column.javaType == "LocalDateTime")
#set ($hasDateTime = 1)
#end
#end
import type {
${simpleClassName},
#foreach ($subSimpleClassName in $subSimpleClassNames)
${subSimpleClassName},
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { useDialog } from '@wot-ui/ui/components/wd-dialog'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import {
delete${simpleClassName},
get${simpleClassName},
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
#if ($subTable.subJoinMany)
get${subSimpleClassName}ListBy${SubJoinColumnName},
#else
get${subSimpleClassName}By${SubJoinColumnName},
#end
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
import { delay, navigateBackPlus } from '@/utils'
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDateTime == 1)
import { formatDateTime } from '@/utils/date'
#end
const props = defineProps<{
id?: ${primaryTsType}
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const dialog = useDialog()
const toast = useToast()
const formData = ref<${simpleClassName}>() // 主表数据
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany)
const ${subClassNameVar}s = ref<${subSimpleClassName}[]>([]) // ${subTable.classComment}数据
#else
const ${subClassNameVar} = ref<${subSimpleClassName} | null>(null) // ${subTable.classComment}数据
#end
#end
const deleting = ref(false) // 删除状态
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
}
/** 加载主表和子表详情 */
async function getDetail() {
if (!props.id || deleting.value) {
return
}
const [data#foreach ($subTable in $subTables)#set ($index = $foreach.count - 1), ${subClassNameVars.get($index)}Data#end] = await Promise.all([
get${simpleClassName}(props.id),
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
#if ($subTable.subJoinMany)
get${subSimpleClassName}ListBy${SubJoinColumnName}(props.id),
#else
get${subSimpleClassName}By${SubJoinColumnName}(props.id),
#end
#end
])
formData.value = data
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany)
${subClassNameVar}s.value = ${subClassNameVar}Data || []
#else
${subClassNameVar}.value = ${subClassNameVar}Data || null
#end
#end
}
/** 编辑${table.classComment} */
function handleEdit() {
uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?id=#[[${]]#props.id#[[}]]#` })
}
/** 删除${table.classComment} */
async function handleDelete() {
if (!props.id) {
return
}
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该${table.classComment}吗?' })
} catch {
return
}
deleting.value = true
try {
await delete${simpleClassName}(props.id)
toast.success('删除成功')
uni.$emit('${table.moduleName}:${table.businessName}:reload')
delay(handleBack)
} finally {
deleting.value = false
}
}
/** 进入页面加载(含编辑返回后刷新) */
onShow(() => {
getDetail()
})
</script>
@@ -12,34 +12,48 @@
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
#foreach($column in $columns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
#set ($comment = $column.columnComment)
#set ($dictMethod = "getDictOptions")
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "getIntDictOptions")
#elseif ($javaType == "String")
#set ($dictMethod = "getStrDictOptions")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "getBoolDictOptions")
#end
## 下拉选择(字典)→ yd-form-picker,内部按 dictType 解析选项,适合选项较多
#if ($column.htmlType == "select" && $dictType && "" != $dictType)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
#set ($comment = $column.columnComment)
#set ($dictMethod = "getDictOptions")
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "getIntDictOptions")
#elseif ($javaType == "String")
#set ($dictMethod = "getStrDictOptions")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "getBoolDictOptions")
#end
## 树表的父级选择
#if ($table.templateType == 2 && $column.id == $treeParentColumn.id)
<yd-tree-select
v-model="formData.${javaField}"
:data="treeData"
label="${comment}"
prop="${javaField}"
:props="{ label: '${treeNameColumn.javaField}', value: '${primaryColumn.javaField}' }"
placeholder="请选择${comment}"
/>
## 下拉选择(字典)→ yd-form-picker,内部按 dictType 解析选项,适合选项较多
#elseif ($column.htmlType == "select" && $dictType && "" != $dictType)
<yd-form-picker
v-model="formData.${javaField}"
label="${comment}"
label-width="180rpx"
prop="${javaField}"
#if ($javaType == "Boolean")
:columns="getBoolDictOptions(DICT_TYPE.${dictType.toUpperCase()})"
#else
:dict-type="DICT_TYPE.${dictType.toUpperCase()}"
#if ($javaType == "String")
dict-kind="str"
#end
#end
placeholder="请选择${comment}"
/>
## 单选(字典)→ radio-group,适合选项较少
#elseif ($column.htmlType == "radio" && $dictType && "" != $dictType)
## 单选(字典)→ radio-group,适合选项较少
#elseif ($column.htmlType == "radio" && $dictType && "" != $dictType)
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}" center>
<wd-radio-group v-model="formData.${javaField}" type="button">
<wd-radio
@@ -51,70 +65,78 @@
</wd-radio>
</wd-radio-group>
</wd-form-item>
## 数字类型(无字典)
#elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte")
## 数字类型(无字典)
#elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "float" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte")
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
<wd-input-number
v-model="formData.${javaField}"
#if (${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal")
#if ($column.nullable)
allow-null
#end
#if (${javaType.toLowerCase()} == "float" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal")
:precision="2"
input-type="number"
#end
:min="0"
/>
</wd-form-item>
## 布尔类型
#elseif (${javaType.toLowerCase()} == "boolean")
## 布尔类型
#elseif (${javaType.toLowerCase()} == "boolean")
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}" center>
<wd-switch v-model="formData.${javaField}" />
</wd-form-item>
## 日期时间类型
#elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "date")
#if (${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "datetime")
#end
## 日期时间类型
#elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "date")
#if (${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "datetime")
#end
<wd-form-item
title="${comment}"
title-width="180rpx"
prop="${javaField}"
is-link
#if ($pickerType == "datetime")
:value="formatDateTime(formData.${javaField})"
:value="formData.${javaField} ? formatDateTime(formData.${javaField}) : ''"
#else
:value="formatDate(formData.${javaField})"
:value="formData.${javaField} ? formatDate(formData.${javaField}) : ''"
#end
placeholder="请选择${comment}"
@click="${javaField}Visible = true"
/>
<wd-datetime-picker
v-model="formData.${javaField}"
v-model:visible="${javaField}Visible"
:model-value="formData.${javaField} || Date.now()"
type="${pickerType}"
@update:model-value="value => formData.${javaField} = value"
/>
## 文本域
#elseif ($column.htmlType == "textarea")
## 文本域
#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
<wd-textarea
v-model="formData.${javaField}"
clearable
placeholder="请输入${comment}"
#if ($column.htmlType == "editor")
:maxlength="2000"
#else
:maxlength="200"
#end
show-word-limit
/>
</wd-form-item>
## 图片上传 → yd-upload-img(包 wd-form-item 才能注册到 wd-form 做校验/错误展示)
#elseif ($column.htmlType == "imageUpload")
## 图片上传 → yd-upload-img(包 wd-form-item 才能注册到 wd-form 做校验/错误展示)
#elseif ($column.htmlType == "imageUpload")
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
<yd-upload-img v-model="formData.${javaField}" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
## 文件上传 → yd-upload-file
#elseif ($column.htmlType == "fileUpload")
## 文件上传 → yd-upload-file
#elseif ($column.htmlType == "fileUpload")
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
<yd-upload-file v-model="formData.${javaField}" directory="${table.moduleName}/${table.businessName}" />
<yd-upload-file v-model="formData.${javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
## 默认:文本输入
#else
## 默认:文本输入
#else
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
<wd-input
v-model="formData.${javaField}"
@@ -122,11 +144,277 @@
placeholder="请输入${comment}"
/>
</wd-form-item>
#end
#end
#end
#end
#end
</wd-cell-group>
</wd-form>
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
<!-- 子表:${subTable.classComment} -->
<view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
<text class="text-28rpx text-[#333] font-semibold">${subTable.classComment}</text>
#if ($subTable.subJoinMany)
<wd-button size="small" type="primary" variant="plain" @click="add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}">
添加
</wd-button>
#else
<wd-button
v-if="!${subClassNameVar}"
size="small"
type="primary"
variant="plain"
@click="add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}()"
>
添加
</wd-button>
#end
</view>
<view class="px-24rpx">
#if ($subTable.subJoinMany)
<view v-if="!${subClassNameVar}s.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
暂无${subTable.classComment}
</view>
<view
v-for="(item, index) in ${subClassNameVar}s"
:key="index"
class="mb-20rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
<view class="mb-16rpx flex justify-end">
<wd-button size="small" type="danger" variant="plain" @click="remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(index)">
删除
</wd-button>
</view>
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($javaType = $column.javaType.toLowerCase())
#set ($dictMethod = "getDictOptions")
#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
#set ($dictMethod = "getIntDictOptions")
#elseif ($column.javaType == "String")
#set ($dictMethod = "getStrDictOptions")
#elseif ($column.javaType == "Boolean")
#set ($dictMethod = "getBoolDictOptions")
#end
#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType)
<yd-form-picker
v-model="item.${column.javaField}"
label="${column.columnComment}"
label-width="180rpx"
#if ($column.javaType == "Boolean")
:columns="getBoolDictOptions(DICT_TYPE.${column.dictType.toUpperCase()})"
#else
:dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
#if ($column.javaType == "String")
dict-kind="str"
#end
#end
placeholder="请选择${column.columnComment}"
/>
#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType)
<wd-form-item title="${column.columnComment}" title-width="180rpx" center>
<wd-radio-group v-model="item.${column.javaField}" type="button">
<wd-radio
v-for="dict in $dictMethod(DICT_TYPE.${column.dictType.toUpperCase()})"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
#elseif ($javaType == "boolean")
<wd-form-item title="${column.columnComment}" title-width="180rpx" center>
<wd-switch v-model="item.${column.javaField}" />
</wd-form-item>
#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-input-number
v-model="item.${column.javaField}"
#if($column.nullable)
allow-null
#end
#if ($javaType == "float" || $javaType == "double" || $javaType == "bigdecimal")
:precision="2"
input-type="number"
#end
:min="0"
/>
</wd-form-item>
#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
#set ($subPickerType = "date")
#if ($javaType == "localdatetime")
#set ($subPickerType = "datetime")
#end
<wd-form-item
title="${column.columnComment}"
title-width="180rpx"
is-link
#if ($javaType == "localdatetime")
:value="item.${column.javaField} ? formatDateTime(item.${column.javaField}) : ''"
#else
:value="item.${column.javaField} ? formatDate(item.${column.javaField}) : ''"
#end
placeholder="请选择${column.columnComment}"
@click="subDateVisible[`${subClassNameVar}s.#[[${]]#index#[[}]]#.${column.javaField}`] = true"
/>
<wd-datetime-picker
v-model:visible="subDateVisible[`${subClassNameVar}s.#[[${]]#index#[[}]]#.${column.javaField}`]"
:model-value="item.${column.javaField} || Date.now()"
type="${subPickerType}"
@update:model-value="value => item.${column.javaField} = value"
/>
#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-textarea
v-model="item.${column.javaField}"
clearable
placeholder="请输入${column.columnComment}"
#if ($column.htmlType == "editor")
:maxlength="2000"
#else
:maxlength="200"
#end
/>
</wd-form-item>
#elseif ($column.htmlType == "imageUpload")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<yd-upload-img v-model="item.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
#elseif ($column.htmlType == "fileUpload")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<yd-upload-file v-model="item.${column.javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
#else
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-input v-model="item.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
</wd-form-item>
#end
#end
#end
</view>
#else
<view v-if="!${subClassNameVar}" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
暂无${subTable.classComment}
</view>
<view v-else class="rounded-12rpx bg-white p-24rpx shadow-sm">
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($javaType = $column.javaType.toLowerCase())
#set ($dictMethod = "getDictOptions")
#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
#set ($dictMethod = "getIntDictOptions")
#elseif ($column.javaType == "String")
#set ($dictMethod = "getStrDictOptions")
#elseif ($column.javaType == "Boolean")
#set ($dictMethod = "getBoolDictOptions")
#end
#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType)
<yd-form-picker
v-model="${subClassNameVar}.${column.javaField}"
label="${column.columnComment}"
label-width="180rpx"
#if ($column.javaType == "Boolean")
:columns="getBoolDictOptions(DICT_TYPE.${column.dictType.toUpperCase()})"
#else
:dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
#if ($column.javaType == "String")
dict-kind="str"
#end
#end
placeholder="请选择${column.columnComment}"
/>
#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType)
<wd-form-item title="${column.columnComment}" title-width="180rpx" center>
<wd-radio-group v-model="${subClassNameVar}.${column.javaField}" type="button">
<wd-radio
v-for="dict in $dictMethod(DICT_TYPE.${column.dictType.toUpperCase()})"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
#elseif ($javaType == "boolean")
<wd-form-item title="${column.columnComment}" title-width="180rpx" center>
<wd-switch v-model="${subClassNameVar}.${column.javaField}" />
</wd-form-item>
#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-input-number
v-model="${subClassNameVar}.${column.javaField}"
#if($column.nullable)
allow-null
#end
#if ($javaType == "float" || $javaType == "double" || $javaType == "bigdecimal")
:precision="2"
input-type="number"
#end
:min="0"
/>
</wd-form-item>
#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
#set ($subPickerType = "date")
#if ($javaType == "localdatetime")
#set ($subPickerType = "datetime")
#end
<wd-form-item
title="${column.columnComment}"
title-width="180rpx"
is-link
#if ($javaType == "localdatetime")
:value="${subClassNameVar}.${column.javaField} ? formatDateTime(${subClassNameVar}.${column.javaField}) : ''"
#else
:value="${subClassNameVar}.${column.javaField} ? formatDate(${subClassNameVar}.${column.javaField}) : ''"
#end
placeholder="请选择${column.columnComment}"
@click="subDateVisible['${subClassNameVar}.${column.javaField}'] = true"
/>
<wd-datetime-picker
v-model:visible="subDateVisible['${subClassNameVar}.${column.javaField}']"
:model-value="${subClassNameVar}.${column.javaField} || Date.now()"
type="${subPickerType}"
@update:model-value="value => ${subClassNameVar}.${column.javaField} = value"
/>
#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-textarea
v-model="${subClassNameVar}.${column.javaField}"
clearable
placeholder="请输入${column.columnComment}"
#if ($column.htmlType == "editor")
:maxlength="2000"
#else
:maxlength="200"
#end
/>
</wd-form-item>
#elseif ($column.htmlType == "imageUpload")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<yd-upload-img v-model="${subClassNameVar}.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
#elseif ($column.htmlType == "fileUpload")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<yd-upload-file v-model="${subClassNameVar}.${column.javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
#else
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-input v-model="${subClassNameVar}.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
</wd-form-item>
#end
#end
#end
</view>
#end
</view>
#end
#end
</view>
<!-- 底部保存按钮 -->
@@ -146,7 +434,7 @@
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
@@ -154,54 +442,113 @@ import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
#set ($hasDict = 0)
#set ($hasDate = 0)
#set ($hasDateTime = 0)
#set ($hasSubDate = 0)
#set ($hasGetDictOptions = 0)
#set ($hasGetIntDictOptions = 0)
#set ($hasGetStrDictOptions = 0)
#set ($hasGetBoolDictOptions = 0)
#foreach ($column in $columns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ($column.htmlType == "select" || $column.htmlType == "radio")
&& $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
#set ($hasGetIntDictOptions = 1)
#elseif ($column.htmlType == "radio" && $column.javaType == "String")
#set ($hasGetStrDictOptions = 1)
#elseif ($column.htmlType == "radio" && $column.javaType == "Boolean")
#set ($hasGetBoolDictOptions = 1)
#elseif ($column.htmlType == "radio")
#set ($hasGetDictOptions = 1)
#end
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate"))
#set ($hasDate = 1)
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ${column.javaType.toLowerCase()} == "localdatetime")
#set ($hasDateTime = 1)
#end
#set ($hasDict = 1)
#if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
#set ($hasGetIntDictOptions = 1)
#elseif ($column.htmlType == "radio" && $column.javaType == "String")
#set ($hasGetStrDictOptions = 1)
#elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $column.javaType == "Boolean")
#set ($hasGetBoolDictOptions = 1)
#elseif ($column.htmlType == "radio")
#set ($hasGetDictOptions = 1)
#end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate"))
#set ($hasDate = 1)
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ${column.javaType.toLowerCase()} == "localdatetime")
#set ($hasDateTime = 1)
#end
#end
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subColumns in $subColumnsList)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ($column.htmlType == "select" || $column.htmlType == "radio")
&& $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
#set ($hasGetIntDictOptions = 1)
#elseif ($column.htmlType == "radio" && $column.javaType == "String")
#set ($hasGetStrDictOptions = 1)
#elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $column.javaType == "Boolean")
#set ($hasGetBoolDictOptions = 1)
#elseif ($column.htmlType == "radio")
#set ($hasGetDictOptions = 1)
#end
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate"))
#set ($hasDate = 1)
#set ($hasSubDate = 1)
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ${column.javaType.toLowerCase()} == "localdatetime")
#set ($hasDateTime = 1)
#set ($hasSubDate = 1)
#end
#end
#end
#end
import type {
${simpleClassName},
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subSimpleClassName in $subSimpleClassNames)
${subSimpleClassName},
#end
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
#if ($table.templateType == 10 || $table.templateType == 12)
import {
create${simpleClassName},
get${simpleClassName},
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
#if ($subTable.subJoinMany)
get${subSimpleClassName}ListBy${SubJoinColumnName},
#else
get${subSimpleClassName}By${SubJoinColumnName},
#end
#end
update${simpleClassName},
} from '@/api/${table.moduleName}/${table.businessName}'
#elseif ($table.templateType == 2)
import { create${simpleClassName}, get${simpleClassName}, get${simpleClassName}List, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
#else
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
#end
#if ($hasGetDictOptions == 1 || $hasGetIntDictOptions == 1 || $hasGetStrDictOptions == 1 || $hasGetBoolDictOptions == 1)
#set ($dictImportNames = "")
#if ($hasGetDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getDictOptions, ")
#end
#if ($hasGetIntDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getIntDictOptions, ")
#end
#if ($hasGetStrDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getStrDictOptions, ")
#end
#if ($hasGetBoolDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getBoolDictOptions, ")
#end
#set ($dictImportNames = $dictImportNames.trim())
#set ($dictImportNames = $dictImportNames.substring(0, $dictImportNames.length() - 1))
#set ($dictImportNames = "")
#if ($hasGetDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getDictOptions, ")
#end
#if ($hasGetIntDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getIntDictOptions, ")
#end
#if ($hasGetStrDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getStrDictOptions, ")
#end
#if ($hasGetBoolDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getBoolDictOptions, ")
#end
#set ($dictImportNames = $dictImportNames.trim())
#set ($dictImportNames = $dictImportNames.substring(0, $dictImportNames.length() - 1))
import { $dictImportNames } from '@/hooks/useDict'
#end
import { delay, navigateBackPlus } from '@/utils'
@@ -209,21 +556,27 @@ import { delay, navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDate == 1 || $hasDateTime == 1)
#set ($dateImportNames = "")
#if ($hasDate == 1)
#set ($dateImportNames = "${dateImportNames}formatDate, ")
#end
#if ($hasDateTime == 1)
#set ($dateImportNames = "${dateImportNames}formatDateTime, ")
#end
#set ($dateImportNames = $dateImportNames.trim())
#set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
#set ($dateImportNames = "")
#if ($hasDate == 1)
#set ($dateImportNames = "${dateImportNames}formatDate, ")
#end
#if ($hasDateTime == 1)
#set ($dateImportNames = "${dateImportNames}formatDateTime, ")
#end
#set ($dateImportNames = $dateImportNames.trim())
#set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
import { $dateImportNames } from '@/utils/date'
#end
import { createFormSchema } from '@/utils/wot'
#if ($table.templateType == 2)
import { handleTree } from '@/utils/tree'
#end
const props = defineProps<{
id?: ${primaryTsType} | any
id?: ${primaryTsType}
#if ($table.templateType == 2)
${treeParentColumn.javaField}?: ${primaryTsType}
#end
}>()
definePage({
@@ -236,42 +589,165 @@ definePage({
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑${table.classComment}' : '新增${table.classComment}')
const formLoading = ref(false) // 表单提交状态
const formData = ref<${simpleClassName}>({
const formData = ref<Partial<${simpleClassName}>>({
#foreach($column in $columns)
#if (($column.createOperation || $column.updateOperation) || $column.primaryKey)
#set ($javaType = $column.javaType.toLowerCase())
#set ($javaFieldLower = $column.javaField.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
#if ($column.primaryKey)
#if (($column.createOperation || $column.updateOperation) || $column.primaryKey)
#set ($javaType = $column.javaType.toLowerCase())
#set ($javaFieldLower = $column.javaField.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
#if ($column.primaryKey)
${column.javaField}: undefined,
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
#elseif($table.templateType == 2 && $column.id == $treeParentColumn.id)
${column.javaField}: props.${column.javaField} ?? 0,
#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
${column.javaField}: undefined,
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
#if($column.nullable)
${column.javaField}: undefined,
#else
${column.javaField}: 0,
#elseif(${javaType} == "boolean")
#end
#elseif(${javaType} == "boolean")
${column.javaField}: false,
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}: undefined,
#else
#else
${column.javaField}: '',
#end
#end
#end
#end
#end
}) // 表单数据
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#if ($subTable.subJoinMany)
const ${subClassNameVar}s = ref<Partial<${subSimpleClassName}>[]>([]) // ${subTable.classComment}数据
#else
const ${subClassNameVar} = ref<Partial<${subSimpleClassName}> | null>(null) // ${subTable.classComment}数据
#end
#end
#end
#foreach($column in $columns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
const ${column.javaField}Visible = ref(false) // ${column.columnComment}选择器状态
#end
#end
#end
#if ($hasSubDate == 1)
const subDateVisible = ref<Record<string, boolean>>({}) // 子表日期选择器状态
#end
const formSchema = createFormSchema({
#foreach($column in $columns)
#set ($javaFieldLower = $column.javaField.toLowerCase())
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
#set ($javaFieldLower = $column.javaField.toLowerCase())
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
&& $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }],
#end
#end
#end
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
#if ($table.templateType == 2)
const treeData = ref<${simpleClassName}[]>([]) // 父级树形选项
#end
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#if ($subTable.subJoinMany)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
/** 添加${subTable.classComment} */
function add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}() {
${subClassNameVar}s.value.push({
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($javaType = $column.javaType.toLowerCase())
#if((($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
${column.javaField}: undefined,
#else
${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") 0#elseif($javaType == "boolean") false#else ''#end,
#end
#end
#end
})
}
/** 删除${subTable.classComment} */
function remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(index: number) {
${subClassNameVar}s.value.splice(index, 1)
}
#else
/** 添加${subTable.classComment} */
function add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}() {
${subClassNameVar}.value = {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($javaType = $column.javaType.toLowerCase())
#if((($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
${column.javaField}: undefined,
#else
${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") 0#elseif($javaType == "boolean") false#else ''#end,
#end
#end
#end
}
}
#end
#end
/** 校验主子表数据 */
function isEmptySubValue(value: unknown) {
return value === undefined || value === null || value === ''
}
function validateSubTables() {
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#if ($subTable.subJoinMany)
for (let index = 0; index < ${subClassNameVar}s.value.length; index++) {
const item = ${subClassNameVar}s.value[index]!
#foreach ($column in $subColumns)
#set ($javaFieldLower = $column.javaField.toLowerCase())
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
&& $column.id != $subJoinColumn.id && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
if (isEmptySubValue(item.${column.javaField})) {
toast.warning(`${subTable.classComment}第#[[${]]#index + 1#[[}]]#项的${column.columnComment}不能为空`)
return false
}
#end
#end
}
#else
if (${subClassNameVar}.value) {
#foreach ($column in $subColumns)
#set ($javaFieldLower = $column.javaField.toLowerCase())
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
&& $column.id != $subJoinColumn.id && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
if (isEmptySubValue(${subClassNameVar}.value.${column.javaField})) {
toast.warning('${subTable.classComment}的${column.columnComment}不能为空')
return false
}
#end
#end
}
#end
#end
return true
}
#end
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
@@ -282,7 +758,34 @@ async function getDetail() {
if (!props.id) {
return
}
#if ($table.templateType == 10 || $table.templateType == 12)
const [data#foreach ($subTable in $subTables)#set ($index = $foreach.count - 1), ${subClassNameVars.get($index)}Data#end] = await Promise.all([
get${simpleClassName}(props.id),
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
#if ($subTable.subJoinMany)
get${subSimpleClassName}ListBy${SubJoinColumnName}(props.id),
#else
get${subSimpleClassName}By${SubJoinColumnName}(props.id),
#end
#end
])
formData.value = data
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany)
${subClassNameVar}s.value = ${subClassNameVar}Data || []
#else
${subClassNameVar}.value = ${subClassNameVar}Data || null
#end
#end
#else
formData.value = await get${simpleClassName}(props.id)
#end
}
/** 提交表单 */
@@ -291,14 +794,31 @@ async function handleSubmit() {
if (!valid) {
return
}
#if ($table.templateType == 10 || $table.templateType == 12)
if (!validateSubTables()) {
return
}
#end
formLoading.value = true
try {
#if ($table.templateType == 10 || $table.templateType == 12)
const data = {
...formData.value,
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
${subClassNameVar}#if($subTable.subJoinMany)s#end: ${subClassNameVar}#if($subTable.subJoinMany)s#end.value,
#end
} as ${simpleClassName}
#else
const data = formData.value as ${simpleClassName}
#end
if (props.id) {
await update${simpleClassName}(formData.value)
await update${simpleClassName}(data)
toast.success('修改成功')
} else {
await create${simpleClassName}(formData.value)
await create${simpleClassName}(data)
toast.success('新增成功')
}
uni.$emit('${table.moduleName}:${table.businessName}:reload')
@@ -309,7 +829,16 @@ async function handleSubmit() {
}
/** 初始化 */
onMounted(() => {
onMounted(async () => {
#if ($table.templateType == 2)
const data = await get${simpleClassName}List()
treeData.value = [{
${primaryColumn.javaField}: 0,
${treeNameColumn.javaField}: '顶级${table.classComment}',
${treeParentColumn.javaField}: 0,
children: handleTree(data, '${primaryColumn.javaField}', '${treeParentColumn.javaField}'),
} as ${simpleClassName}]
#end
getDetail()
})
</script>
@@ -0,0 +1,250 @@
#set ($subTable = $subTables.get($subIndex))
#set ($subColumns = $subColumnsList.get($subIndex))
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($subPrimaryColumn = $subPrimaryColumns.get($subIndex))
#set ($subJoinColumn = $subJoinColumns.get($subIndex))
#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase())
#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "float" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte")
#set ($subPrimaryTsType = "number")
#else
#set ($subPrimaryTsType = "string")
#end
#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase())
#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "float" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte")
#set ($subJoinTsType = "number")
#else
#set ($subJoinTsType = "string")
#end
#set ($hasDict = 0)
#set ($hasBoolDict = 0)
#set ($hasDate = 0)
#set ($hasDateTime = 0)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#if ($column.javaType == "Boolean")
#set ($hasBoolDict = 1)
#end
#end
#if (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate")
#set ($hasDate = 1)
#end
#if (${column.javaType.toLowerCase()} == "localdatetime")
#set ($hasDateTime = 1)
#end
#end
#end
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar :title="getTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
<!-- 表单区域 -->
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($javaType = $column.javaType.toLowerCase())
#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
<yd-form-picker
v-model="formData.${column.javaField}"
label="${column.columnComment}"
label-width="180rpx"
prop="${column.javaField}"
#if ($column.javaType == "Boolean")
:columns="getBoolDictOptions(DICT_TYPE.${column.dictType.toUpperCase()})"
#else
:dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
#if ($column.javaType == "String")
dict-kind="str"
#end
#end
placeholder="请选择${column.columnComment}"
/>
#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
#set ($pickerType = "date")
#if ($javaType == "localdatetime")
#set ($pickerType = "datetime")
#end
<wd-form-item
title="${column.columnComment}"
title-width="180rpx"
prop="${column.javaField}"
is-link
#if ($javaType == "localdatetime")
:value="formData.${column.javaField} ? formatDateTime(formData.${column.javaField}) : ''"
#else
:value="formData.${column.javaField} ? formatDate(formData.${column.javaField}) : ''"
#end
placeholder="请选择${column.columnComment}"
@click="${column.javaField}Visible = true"
/>
<wd-datetime-picker
v-model:visible="${column.javaField}Visible"
:model-value="formData.${column.javaField} || Date.now()"
type="${pickerType}"
@update:model-value="value => formData.${column.javaField} = value"
/>
#else
<wd-form-item title="${column.columnComment}" title-width="180rpx" prop="${column.javaField}">
#if ($javaType == "boolean")
<wd-switch v-model="formData.${column.javaField}" />
#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
<wd-input-number
v-model="formData.${column.javaField}"
#if($column.nullable)
allow-null
#end
#if ($javaType == "float" || $javaType == "double" || $javaType == "bigdecimal")
:precision="2"
input-type="number"
#end
:min="0"
/>
#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
<wd-textarea
v-model="formData.${column.javaField}"
clearable
placeholder="请输入${column.columnComment}"
#if ($column.htmlType == "editor")
:maxlength="2000"
#else
:maxlength="200"
#end
/>
#elseif ($column.htmlType == "imageUpload")
<yd-upload-img v-model="formData.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
#elseif ($column.htmlType == "fileUpload")
<yd-upload-file v-model="formData.${column.javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
#else
<wd-input v-model="formData.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
#end
</wd-form-item>
#end
#end
#end
</wd-cell-group>
</wd-form>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button type="primary" block :loading="formLoading" @click="handleSubmit">
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
import type { ${subSimpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { create${subSimpleClassName}, get${subSimpleClassName}, update${subSimpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { delay, navigateBackPlus } from '@/utils'
#if ($hasBoolDict == 1)
import { getBoolDictOptions } from '@/hooks/useDict'
#end
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDate == 1 || $hasDateTime == 1)
#set ($dateImportNames = "")
#if ($hasDate == 1)
#set ($dateImportNames = "${dateImportNames}formatDate, ")
#end
#if ($hasDateTime == 1)
#set ($dateImportNames = "${dateImportNames}formatDateTime, ")
#end
#set ($dateImportNames = $dateImportNames.trim())
#set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
import { $dateImportNames } from '@/utils/date'
#end
import { createFormSchema } from '@/utils/wot'
const props = defineProps<{
id?: ${subPrimaryTsType}
${subJoinColumn.javaField}: ${subJoinTsType}
}>()
definePage({
style: { navigationBarTitleText: '', navigationStyle: 'custom' },
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑${subTable.classComment}' : '新增${subTable.classComment}')
const formLoading = ref(false) // 表单提交状态
const formData = ref<Partial<${subSimpleClassName}>>({
#foreach ($column in $subColumns)
#if ($column.primaryKey)
${column.javaField}: undefined,
#elseif ($column.id == $subJoinColumn.id)
${column.javaField}: props.${column.javaField},
#elseif ($column.createOperation || $column.updateOperation)
#set ($javaType = $column.javaType.toLowerCase())
#if((($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
${column.javaField}: undefined,
#else
${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") 0#elseif($javaType == "boolean") false#else ''#end,
#end
#end
#end
}) // 表单数据
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id
&& (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
const ${column.javaField}Visible = ref(false) // ${column.columnComment}选择器状态
#end
#end
const formSchema = createFormSchema({
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey)
${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }],
#end
#end
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 加载${subTable.classComment}详情 */
async function getDetail() {
if (props.id) {
formData.value = await get${subSimpleClassName}(props.id)
}
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
const data = {
...formData.value,
${subJoinColumn.javaField}: props.${subJoinColumn.javaField} ?? formData.value.${subJoinColumn.javaField},
} as ${subSimpleClassName}
if (props.id) {
await update${subSimpleClassName}(data)
toast.success('修改成功')
} else {
await create${subSimpleClassName}(data)
toast.success('新增成功')
}
delay(handleBack)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
@@ -0,0 +1,151 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="${table.classComment}管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 搜索组件 -->
<SearchForm @search="handleQuery" @reset="handleReset" />
<!-- 面包屑导航 -->
<Breadcrumb ref="breadcrumbRef" v-model="currentParentId" />
<!-- ${table.classComment}列表 -->
<view class="p-24rpx">
<view
v-for="item in currentList"
:key="item.${primaryColumn.javaField}"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
>
<view class="p-24rpx" @click="handleDetail(item)">
<view class="flex items-center justify-between">
<view class="flex min-w-0 items-center">
<view class="mr-16rpx h-48rpx w-48rpx shrink-0 flex items-center justify-center rounded-8rpx bg-[#1890ff]">
<wd-icon name="folder" size="20px" color="#fff" />
</view>
<view class="truncate text-32rpx text-[#333] font-semibold">
{{ item.${treeNameColumn.javaField} }}
</view>
</view>
<view
v-if="item.children?.length"
class="ml-16rpx shrink-0 flex items-center"
@click.stop="handleEnterChildren(item)"
>
<text class="text-24rpx text-[#1890ff]">子级 ({{ item.children.length }})</text>
<wd-icon name="arrow-right" size="12px" color="#1890ff" />
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="!loading && currentList.length === 0" class="py-100rpx text-center">
<wd-empty icon="content" tip="暂无${table.classComment}数据" />
</view>
</view>
<!-- 新增按钮 -->
<wd-fab
v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
position="right-bottom"
type="primary"
:expandable="false"
@click="handleAdd"
/>
</view>
</template>
<script lang="ts" setup>
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { onUnload } from '@dcloudio/uni-app'
import { computed, onMounted, ref } from 'vue'
import { get${simpleClassName}List } from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
import { findChildren, handleTree } from '@/utils/tree'
import Breadcrumb from './components/breadcrumb.vue'
import SearchForm from './components/search-form.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const loading = ref(false) // 列表加载状态
const list = ref<${simpleClassName}[]>([]) // 完整树形列表
const currentParentId = ref<${primaryTsType}>(0) // 当前层级的父节点编号
const currentList = computed(() => currentParentId.value === 0
? list.value
: findChildren(list.value, currentParentId.value, '${primaryColumn.javaField}')) // 当前层级列表
const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>() // 面包屑组件引用
const queryParams = ref<Record<string, any>>({}) // 查询参数
/** 返回上一页或上一层级 */
function handleBack() {
if (!breadcrumbRef.value?.back()) {
navigateBackPlus()
}
}
/** 进入子层级 */
function handleEnterChildren(item: ${simpleClassName}) {
breadcrumbRef.value?.enter({ id: item.${primaryColumn.javaField}!, name: item.${treeNameColumn.javaField} })
}
/** 查询${table.classComment}列表 */
async function getList() {
loading.value = true
try {
list.value = handleTree(await get${simpleClassName}List(queryParams.value), '${primaryColumn.javaField}', '${treeParentColumn.javaField}')
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
function handleQuery(data?: Record<string, any>) {
queryParams.value = { ...data }
currentParentId.value = 0
breadcrumbRef.value?.reset()
getList()
}
/** 重置按钮操作 */
function handleReset() {
handleQuery()
}
/** 新增${table.classComment} */
function handleAdd() {
uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?${treeParentColumn.javaField}=#[[${]]#currentParentId.value#[[}]]#` })
}
/** 查看详情 */
function handleDetail(item: ${simpleClassName}) {
uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/detail/index?id=#[[${]]#item.${primaryColumn.javaField}#[[}]]#` })
}
/** 初始化 */
onMounted(() => {
getList()
uni.$on('${table.moduleName}:${table.businessName}:reload', getList)
})
/** 卸载 */
onUnload(() => {
uni.$off('${table.moduleName}:${table.businessName}:reload', getList)
})
</script>
@@ -1,5 +1,11 @@
#set ($selectOptionEnable = false)
#foreach ($column in $columns)
#if (($column.createOperation || $column.updateOperation) && $column.htmlType == "select")
#set ($selectOptionEnable = true)
#end
#end
<script lang="ts" setup>
import type { Rule } from 'antdv-next/es/form';
import type { Rule } from 'antdv-next';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref } from 'vue';
@@ -9,7 +15,7 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message, Tabs, Form, Input, TextArea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'antdv-next';
import { Checkbox, CheckboxGroup, DatePicker, Form, FormItem, Input, message, Radio, RadioGroup, Select#if ($selectOptionEnable), SelectOption#end#if ( $table.templateType == 10 || $table.templateType == 12 ), TabPane, Tabs#end, TextArea, TreeSelect } from 'antdv-next';
#if($table.templateType == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}';
import { handleTree } from '@vben/utils'
@@ -204,7 +210,7 @@ const [Modal, modalApi] = useVbenModal({
#set ($dictMethod = "boolean")
#end
#if ( $table.templateType == 2 && $column.id == $treeParentColumn.id )
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<TreeSelect
v-model:value="formData.${javaField}"
:treeData="${classNameVar}Tree"
@@ -225,25 +231,25 @@ const [Modal, modalApi] = useVbenModal({
treeDefaultExpandAll
placeholder="请选择${comment}"
/>
</Form.Item>
</FormItem>
#elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "imageUpload")## 图片上传
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<ImageUpload v-model:value="formData.${javaField}" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "fileUpload")## 文件上传
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<FileUpload v-model:value="formData.${javaField}" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "editor")## 文本编辑器
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<RichTextarea v-model="formData.${javaField}" height="500px" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "select")## 下拉框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<Select v-model:value="formData.${javaField}" placeholder="请选择${comment}">
#if ("" != $dictType)## 有数据字典
<SelectOption
@@ -257,9 +263,9 @@ const [Modal, modalApi] = useVbenModal({
<SelectOption label="请选择字典生成" value="" />
#end
</Select>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "checkbox")## 多选框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<CheckboxGroup v-model:value="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<Checkbox
@@ -273,9 +279,9 @@ const [Modal, modalApi] = useVbenModal({
<Checkbox label="请选择字典生成" />
#end
</CheckboxGroup>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "radio")## 单选框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<RadioGroup v-model:value="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<Radio
@@ -289,19 +295,19 @@ const [Modal, modalApi] = useVbenModal({
<Radio value="1">请选择字典生成</Radio>
#end
</RadioGroup>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "datetime")## 时间框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<DatePicker
v-model:value="formData.${javaField}"
valueFormat="x"
placeholder="选择${comment}"
/>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "textarea")## 文本框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<TextArea v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
</Form.Item>
</FormItem>
#end
#end
#end
@@ -1,6 +1,4 @@
<script lang="ts" setup>
import type { FileType } from 'antdv-next/es/upload/interface';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -43,8 +41,8 @@ const [Modal, modalApi] = useVbenModal({
});
/** 上传前:拦截 antd Upload 的自动上传,文件存到 ref */
function beforeUpload(file: FileType) {
fileRef.value = file as unknown as File;
function beforeUpload(file: File) {
fileRef.value = file;
return false;
}
@@ -1,3 +1,13 @@
#set ($rangePickerEnable = false)
#set ($selectOptionEnable = false)
#foreach ($column in $columns)
#if ($column.listOperation && $column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
#set ($rangePickerEnable = true)
#end
#if ($column.listOperation && ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox"))
#set ($selectOptionEnable = true)
#end
#end
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
@@ -8,12 +18,14 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
import { cloneDeep, downloadFileFromBlobPart, formatDateTime, isEmpty } from '@vben/utils';
import { Button, Card, message, Tabs, Pagination, Form, RangePicker, DatePicker, Select, Input } from 'antdv-next';
import { Button, Card, DatePicker, Form, FormItem, Input, message, Pagination#if ($rangePickerEnable), DateRangePicker as RangePicker#end, Select#if ($selectOptionEnable), SelectOption#end#if ( $table.templateType == 11 || $table.templateType == 12 ), TabPane, Tabs#end } from 'antdv-next';
import ${simpleClassName}Form from './modules/form.vue';
import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
import { DictTag } from '#/components/dict-tag';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
#if ($rangePickerEnable)
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
#end
## 特殊:主子表专属逻辑
#if ( $table.templateType == 11 || $table.templateType == 12 )
@@ -249,7 +261,7 @@ onMounted(() => {
#set ($dictMethod = "boolean")
#end
#if ($column.htmlType == "input")
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<Input
v-model:value="queryParams.${javaField}"
placeholder="请输入${comment}"
@@ -257,9 +269,9 @@ onMounted(() => {
@pressEnter="handleQuery"
class="w-full"
/>
</Form.Item>
</FormItem>
#elseif ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox")
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<Select
v-model:value="queryParams.${javaField}"
placeholder="请选择${comment}"
@@ -278,10 +290,10 @@ onMounted(() => {
<SelectOption label="请选择字典生成" value="" />
#end
</Select>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "datetime")
#if ($column.listOperationCondition != "BETWEEN")## 非范围
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<DatePicker
v-model:value="queryParams.${javaField}"
valueFormat="YYYY-MM-DD"
@@ -289,25 +301,25 @@ onMounted(() => {
allowClear
class="w-full"
/>
</Form.Item>
</FormItem>
#else## 范围
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<RangePicker
v-model:value="queryParams.${javaField}"
v-bind="getRangePickerDefaultProps()"
class="w-full"
/>
</Form.Item>
</FormItem>
#end
#end
#end
#end
<Form.Item>
<FormItem>
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
<Button class="ml-2" @click="handleQuery" type="primary">
搜索
</Button>
</Form.Item>
</FormItem>
</Form>
</Card>
@@ -2,8 +2,14 @@
#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($selectOptionEnable = false)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && $column.htmlType == "select")
#set ($selectOptionEnable = true)
#end
#end
<script lang="ts" setup>
import type { Rule } from 'antdv-next/es/form';
import type { Rule } from 'antdv-next';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref } from 'vue';
@@ -13,7 +19,7 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message, Tabs, Form, Input, TextArea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'antdv-next';
import { Checkbox, CheckboxGroup, DatePicker, Form, FormItem, Input, message, Radio, RadioGroup, Select#if ($selectOptionEnable), SelectOption#end, TextArea, TreeSelect } from 'antdv-next';
import { $t } from '#/locales';
@@ -130,23 +136,23 @@ function resetForm(){
#set ($dictMethod = "boolean")
#end
#if ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "imageUpload")## 图片上传
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<ImageUpload v-model:value="formData.${javaField}" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "fileUpload")## 文件上传
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<FileUpload v-model:value="formData.${javaField}" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "editor")## 文本编辑器
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<RichTextarea v-model="formData.${javaField}" height="500px" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "select")## 下拉框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<Select v-model:value="formData.${javaField}" placeholder="请选择${comment}">
#if ("" != $dictType)## 有数据字典
<SelectOption
@@ -160,9 +166,9 @@ function resetForm(){
<SelectOption label="请选择字典生成" value="" />
#end
</Select>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "checkbox")## 多选框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<CheckboxGroup v-model:value="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<Checkbox
@@ -176,9 +182,9 @@ function resetForm(){
<Checkbox label="请选择字典生成" />
#end
</CheckboxGroup>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "radio")## 单选框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<RadioGroup v-model:value="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<Radio
@@ -192,19 +198,19 @@ function resetForm(){
<Radio value="1">请选择字典生成</Radio>
#end
</RadioGroup>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "datetime")## 时间框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<DatePicker
v-model:value="formData.${javaField}"
valueFormat="x"
placeholder="选择${comment}"
/>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "textarea")## 文本框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<TextArea v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
</Form.Item>
</FormItem>
#end
#end
#end
@@ -1,2 +1,2 @@
## 主表的 normal 和 inner 使用相同的 form 表单
#parse("codegen/vue3_vben5_antd/general/views/modules/form_sub_normal.vue.vm")
#parse("codegen/vue3_vben5_antdv_next/general/views/modules/form_sub_normal.vue.vm")
@@ -4,6 +4,12 @@
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($selectOptionEnable = false)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && (!$subTable.subJoinMany || $column.id != $subJoinColumn.id) && $column.htmlType == "select")
#set ($selectOptionEnable = true)
#end
#end
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
@@ -12,7 +18,7 @@ import { computed, ref, h, onMounted, watch, nextTick } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { message, Tabs, Form, Input, TextArea, Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'antdv-next';
import { Button, Checkbox, CheckboxGroup, DatePicker#if (!$subTable.subJoinMany), Form, FormItem#end, Input, Radio, RadioGroup, Select#if ($selectOptionEnable), SelectOption#end, TextArea } from 'antdv-next';
import { $t } from '#/locales';
#if ($subTable.subJoinMany) ## 一对多
@@ -22,7 +28,7 @@ import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { ImageUpload, FileUpload } from "#/components/upload";
import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}';
#else
import type { Rule } from 'antdv-next/es/form';
import type { Rule } from 'antdv-next';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}';
@@ -264,23 +270,23 @@ watch(
#set ($dictMethod = "boolean")
#end
#if ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "imageUpload")## 图片上传
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<ImageUpload v-model:value="formData.${javaField}" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "fileUpload")## 文件上传
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<FileUpload v-model:value="formData.${javaField}" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "editor")## 文本编辑器
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<RichTextarea v-model="formData.${javaField}" height="500px" />
</Form.Item>
</FormItem>
#elseif($column.htmlType == "select")## 下拉框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<Select v-model:value="formData.${javaField}" placeholder="请选择${comment}">
#if ("" != $dictType)## 有数据字典
<SelectOption
@@ -294,9 +300,9 @@ watch(
<SelectOption label="请选择字典生成" value="" />
#end
</Select>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "checkbox")## 多选框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<CheckboxGroup v-model:value="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<Checkbox
@@ -310,9 +316,9 @@ watch(
<Checkbox label="请选择字典生成" />
#end
</CheckboxGroup>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "radio")## 单选框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<RadioGroup v-model:value="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<Radio
@@ -326,19 +332,19 @@ watch(
<Radio value="1">请选择字典生成</Radio>
#end
</RadioGroup>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "datetime")## 时间框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<DatePicker
v-model:value="formData.${javaField}"
valueFormat="x"
placeholder="选择${comment}"
/>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "textarea")## 文本框
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<TextArea v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
</Form.Item>
</FormItem>
#end
#end
#end
@@ -5,6 +5,16 @@
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($rangePickerEnable = false)
#set ($selectOptionEnable = false)
#foreach ($column in $subColumns)
#if ($column.listOperation && $column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
#set ($rangePickerEnable = true)
#end
#if ($column.listOperation && ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox"))
#set ($selectOptionEnable = true)
#end
#end
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
@@ -15,7 +25,9 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { DictTag } from '#/components/dict-tag';
#if ($table.templateType == 11 && $rangePickerEnable)
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
#end
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { formatDateTime } from '@vben/utils';
@@ -25,7 +37,7 @@ import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
import ${subSimpleClassName}Form from './${subSimpleClassName_strikeCase}-form.vue'
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { message, Button, Card, Tabs, Pagination, Form, Input, TextArea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, RangePicker, DatePicker, TreeSelect } from 'antdv-next';
import { Button, Card, Checkbox, CheckboxGroup, DatePicker, Form, FormItem, Input, message, Pagination, Radio, RadioGroup#if ($rangePickerEnable), DateRangePicker as RangePicker#end, Select#if ($selectOptionEnable), SelectOption#end, TextArea, TreeSelect } from 'antdv-next';
import { Plus, Trash2 } from '@vben/icons';
import { $t } from '#/locales';
#end
@@ -224,7 +236,7 @@ onMounted(() => {
#set ($dictMethod = "boolean")
#end
#if ($column.htmlType == "input")
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<Input
v-model:value="queryParams.${javaField}"
placeholder="请输入${comment}"
@@ -232,9 +244,9 @@ onMounted(() => {
@pressEnter="handleQuery"
class="w-full"
/>
</Form.Item>
</FormItem>
#elseif ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox")
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<Select
v-model:value="queryParams.${javaField}"
placeholder="请选择${comment}"
@@ -253,10 +265,10 @@ onMounted(() => {
<SelectOption label="请选择字典生成" value="" />
#end
</Select>
</Form.Item>
</FormItem>
#elseif($column.htmlType == "datetime")
#if ($column.listOperationCondition != "BETWEEN")## 非范围
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<DatePicker
v-model:value="queryParams.${javaField}"
valueFormat="YYYY-MM-DD"
@@ -264,25 +276,25 @@ onMounted(() => {
allowClear
class="w-full"
/>
</Form.Item>
</FormItem>
#else## 范围
<Form.Item label="${comment}" name="${javaField}">
<FormItem label="${comment}" name="${javaField}">
<RangePicker
v-model:value="queryParams.${javaField}"
v-bind="getRangePickerDefaultProps()"
class="w-full"
/>
</Form.Item>
</FormItem>
#end
#end
#end
#end
<Form.Item>
<FormItem>
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
<Button class="ml-2" @click="handleQuery" type="primary">
搜索
</Button>
</Form.Item>
</FormItem>
</Form>
</Card>
@@ -1,4 +1,4 @@
## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点:
## 1)inner 使用 list 不分页,erp 使用 page 分页
## 2)erp 支持单个子表的新增、修改、删除,inner 不支持
#parse("codegen/vue3_vben5_antd/general/views/modules/list_sub_erp.vue.vm")
#parse("codegen/vue3_vben5_antdv_next/general/views/modules/list_sub_erp.vue.vm")
@@ -6,7 +6,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message#if ($table.templateType == 11), Tabs#end } from 'antdv-next';
import { message#if ($table.templateType == 10 || $table.templateType == 12), TabPane, Tabs#end } from 'antdv-next';
import { useVbenForm } from '#/adapter/form';
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
@@ -1,6 +1,4 @@
<script lang="ts" setup>
import type { FileType } from 'antdv-next/es/upload/interface';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
@@ -46,7 +44,7 @@ const [Modal, modalApi] = useVbenModal({
});
/** 上传前 */
function beforeUpload(file: FileType) {
function beforeUpload(file: File) {
formApi.setFieldValue('file', file);
return false;
}
@@ -8,7 +8,7 @@ import { ref } from 'vue';
import {#if ($table.templateType != 2 && $deleteBatchEnable) confirm,#end Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart#if ($table.templateType != 2 && $deleteBatchEnable), isEmpty#end } from '@vben/utils';
import { message#if ($table.templateType == 11), Tabs#end } from 'antdv-next';
import { message#if ($table.templateType == 11 || $table.templateType == 12), TabPane, Tabs#end } from 'antdv-next';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
#if (${table.templateType} == 2)## 树表接口
@@ -1,2 +1,2 @@
## 主表的 normal 和 inner 使用相同的 form 表单
#parse("codegen/vue3_vben5_antd/schema/views/modules/form_sub_normal.vue.vm")
#parse("codegen/vue3_vben5_antdv_next/schema/views/modules/form_sub_normal.vue.vm")
@@ -5,6 +5,12 @@
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($apiName = "${table.moduleName.substring(0,1).toUpperCase()}${table.moduleName.substring(1)}${simpleClassName}Api")
#set ($selectOptionEnable = false)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && $column.id != $subJoinColumn.id && $column.htmlType == "select")
#set ($selectOptionEnable = true)
#end
#end
<script lang="ts" setup>
import type { ${apiName} } from '#/api/${table.moduleName}/${table.businessName}';
@@ -14,7 +20,19 @@
#if ($subTable.subJoinMany) ## 一对多
import { Plus } from "@vben/icons";
import { Button, Tabs, Checkbox, Input, TextArea, Select,RadioGroup,CheckboxGroup, DatePicker } from 'antdv-next';
import {
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Input,
RadioGroup,
Select,
#if ($selectOptionEnable)
SelectOption,
#end
TextArea,
} from 'antdv-next';
import { ImageUpload, FileUpload } from "#/components/upload";
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { use${subSimpleClassName}GridEditColumns } from '../data';
@@ -1,4 +1,4 @@
## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点:
## 1)inner 使用 list 不分页,erp 使用 page 分页
## 2)erp 支持单个子表的新增、修改、删除,inner 不支持
#parse("codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm")
#parse("codegen/vue3_vben5_antdv_next/schema/views/modules/list_sub_erp.vue.vm")
@@ -0,0 +1,431 @@
package cn.iocoder.yudao.module.infra.service.codegen.inner;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
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.assertTrue;
/**
* {@link CodegenEngine} 的 Vue3 + Vben5 + Antdv Next 单元测试
*
* @author 芋道源码
*/
public class CodegenEngineVben5AntdvNextTest extends CodegenEngineAbstractTest {
private static final Pattern ANTDV_NEXT_VALUE_IMPORT_PATTERN = Pattern.compile(
"import\\s*\\{([^}]*)}\\s*from\\s*'antdv-next';?", Pattern.DOTALL);
private static final Pattern RANGE_PICKER_COMPONENT_PATTERN = Pattern.compile("<RangePicker\\b");
private static final Pattern RANGE_PICKER_HELPER_BINDING_PATTERN = Pattern.compile(
"\\bv-bind\\s*=\\s*([\"'])\\s*getRangePickerDefaultProps\\s*\\(\\s*\\)\\s*\\1");
private static final Pattern RANGE_PICKER_HELPER_CALL_PATTERN = Pattern.compile(
"\\bgetRangePickerDefaultProps\\s*\\(\\s*\\)");
private static final Pattern RANGE_PICKER_HELPER_IMPORT_PATTERN = Pattern.compile(
"import\\s*\\{[^}]*\\bgetRangePickerDefaultProps\\b[^}]*}"
+ "\\s*from\\s*'#/utils(?:/rangePickerProps)?';?", Pattern.DOTALL);
private static final Pattern SCHEMA_RANGE_PICKER_COMPONENT_PATTERN = Pattern.compile(
"\\bcomponent\\s*:\\s*([\"'])RangePicker\\1");
private static final Pattern SCHEMA_RANGE_PICKER_HELPER_SPREAD_PATTERN = Pattern.compile(
"\\.\\.\\.\\s*getRangePickerDefaultProps\\s*\\(\\s*\\)");
private static final String IMPORT_FORM_PATH =
"yudao-ui-admin-vben/src/views/infra/demo/modules/import-form.vue";
private static final String SCHEMA_DATA_PATH =
"yudao-ui-admin-vben/src/views/infra/demo/data.ts";
private static final String STUDENT_CONTACT_FORM_PATH =
"yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue";
private static final String STUDENT_TEACHER_FORM_PATH =
"yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue";
private static final List<String> INNER_TEMPLATE_PATHS = Arrays.asList(
"codegen/vue3_vben5_antdv_next/general/views/modules/form_sub_inner.vue.vm",
"codegen/vue3_vben5_antdv_next/general/views/modules/list_sub_inner.vue.vm",
"codegen/vue3_vben5_antdv_next/schema/views/modules/form_sub_inner.vue.vm",
"codegen/vue3_vben5_antdv_next/schema/views/modules/list_sub_inner.vue.vm");
@Test
public void testExecute_schemaMasterNormal() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
CodegenTemplateTypeEnum.MASTER_NORMAL);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertSchemaRangePickerGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_schemaMasterErp() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
CodegenTemplateTypeEnum.MASTER_ERP);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertSchemaRangePickerGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_schemaMasterInner() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
CodegenTemplateTypeEnum.MASTER_INNER);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertSchemaRangePickerGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_generalTree() {
// 调用
Map<String, String> result = executeTree(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType());
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertRuleGenerated(result);
assertComponentGenerated(result, "FormItem");
assertComponentGenerated(result, "TreeSelect");
}
@Test
public void testExecute_generalMasterNormal() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
CodegenTemplateTypeEnum.MASTER_NORMAL);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertRuleGenerated(result);
assertGeneralCrudComponentsGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_generalMasterNormal_oneToOneJoinColumnOnlySelect() {
// 调用
Map<String, String> result = executeGeneralMasterNormalWithOneToOneJoinSelect();
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
String content = result.get(STUDENT_TEACHER_FORM_PATH);
assertNotNull(content, STUDENT_TEACHER_FORM_PATH + ":未生成一对一子表表单");
assertTrue(content.contains("<Select v-model:value=\"formData.studentId\""),
STUDENT_TEACHER_FORM_PATH + ":应渲染 join 列的 Select 组件");
assertTrue(content.contains("<SelectOption"), STUDENT_TEACHER_FORM_PATH + ":应渲染 SelectOption 组件");
assertRootValueImport(STUDENT_TEACHER_FORM_PATH, content, "\\bSelectOption\\b");
}
@Test
public void testExecute_generalMasterNormal_oneToManyJoinColumnOnlySelect() {
// 调用
Map<String, String> result = executeGeneralMasterNormalWithOneToManyJoinSelect();
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
String content = result.get(STUDENT_CONTACT_FORM_PATH);
assertNotNull(content, STUDENT_CONTACT_FORM_PATH + ":未生成一对多子表表单");
assertTrue(content.contains("<VxeTable"), STUDENT_CONTACT_FORM_PATH + ":应生成一对多子表表格");
assertFalse(content.contains("<VxeColumn field=\"studentId\""),
STUDENT_CONTACT_FORM_PATH + ":不应生成 join 列");
assertFalse(content.contains("<Select v-model:value=\"row.studentId\""),
STUDENT_CONTACT_FORM_PATH + ":不应渲染 join 列的 Select 组件");
assertFalse(content.contains("<SelectOption"), STUDENT_CONTACT_FORM_PATH + ":不应渲染 SelectOption 组件");
assertFalse(createRootImportPattern("import\\s*\\{", "\\bSelectOption\\b").matcher(content).find(),
STUDENT_CONTACT_FORM_PATH + ":不应导入 SelectOption 组件");
}
@Test
public void testExecute_generalMasterInner() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
CodegenTemplateTypeEnum.MASTER_INNER);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertRuleGenerated(result);
assertGeneralCrudComponentsGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_generalMasterErp() {
// 调用
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
CodegenTemplateTypeEnum.MASTER_ERP);
// 断言
assertGeneratedCodeCompatibleWithAntdvNext(result);
assertRuleGenerated(result);
assertGeneralCrudComponentsGenerated(result);
assertTabsGenerated(result);
}
@Test
public void testExecute_oneImportEnable() {
// 开启 import 开关
codegenProperties.setImportEnable(true);
codegenEngine.initGlobalBindingMap();
// 调用并断言
assertImportFormCompatibleWithAntdvNext(
executeOne(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType()));
assertImportFormCompatibleWithAntdvNext(
executeOne(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType()));
}
@Test
public void testInnerTemplatesParseAntdvNext() {
INNER_TEMPLATE_PATHS.forEach(path -> {
String template = ResourceUtil.readUtf8Str(path);
assertFalse(template.contains("codegen/vue3_vben5_antd/"), path + ":不应引用 Ant Design Vue 模板");
assertTrue(template.contains("codegen/vue3_vben5_antdv_next/"), path + ":应引用 Antdv Next 模板");
});
}
private Map<String, String> executeOne(Integer frontType) {
CodegenTableDO table = getTable("student")
.setFrontType(frontType)
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
return codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"), null, null);
}
private Map<String, String> executeTree(Integer frontType) {
CodegenTableDO table = getTable("category")
.setFrontType(frontType)
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
return codegenEngine.execute(DbType.MYSQL, table, getColumnList("category"), null, null);
}
private Map<String, String> executeGeneralMasterNormalWithOneToOneJoinSelect() {
return executeGeneralMasterNormalWithJoinSelect("teacher", 200L, false);
}
private Map<String, String> executeGeneralMasterNormalWithOneToManyJoinSelect() {
return executeGeneralMasterNormalWithJoinSelect("contact", 100L, true);
}
private Map<String, String> executeGeneralMasterNormalWithJoinSelect(String subTableName,
Long subJoinColumnId,
boolean subJoinMany) {
CodegenTableDO table = getTable("student")
.setFrontType(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType())
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType());
CodegenTableDO subTable = getTable(subTableName)
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType())
.setSubJoinColumnId(subJoinColumnId).setSubJoinMany(subJoinMany);
List<CodegenColumnDO> subColumns = getColumnList(subTableName);
// 稀疏场景:join 列是唯一的 select,避免其它 select 掩盖 join 列对导入判定的影响。
subColumns.forEach(column -> {
if (subJoinColumnId.equals(column.getId())) {
column.setHtmlType("select");
column.setDictType("");
} else if ("select".equals(column.getHtmlType())) {
column.setHtmlType("input");
}
});
return codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"),
Collections.singletonList(subTable), Collections.singletonList(subColumns));
}
private Map<String, String> executeMaster(Integer frontType, CodegenTemplateTypeEnum templateType) {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(frontType)
.setTemplateType(templateType.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 准备参数(子表)
CodegenTableDO contactTable = getTable("contact")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(frontType)
.setSubJoinColumnId(100L).setSubJoinMany(true);
List<CodegenColumnDO> contactColumns = getColumnList("contact");
// 准备参数(班主任)
CodegenTableDO teacherTable = getTable("teacher")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(frontType)
.setSubJoinColumnId(200L).setSubJoinMany(false);
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
// 调用
return codegenEngine.execute(DbType.MYSQL, table, columns,
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
}
private static void assertGeneratedCodeCompatibleWithAntdvNext(Map<String, String> result) {
assertTrue(result.values().stream().anyMatch(content -> content.contains("from 'antdv-next")),
"生成结果应使用 antdv-next");
result.forEach((filePath, content) -> {
assertFalse(content.contains("ant-design-vue"), filePath + ":不应使用 ant-design-vue");
assertFalse(content.contains("antdv-next/es/"), filePath + ":不应使用未导出的 antdv-next 深层路径");
assertFalse(content.contains("<Form.Item"), filePath + ":不应使用 Form.Item");
assertFalse(content.contains("</Form.Item>"), filePath + ":不应使用 Form.Item");
assertRootValueImportsSorted(filePath, content);
assertRootValueImportMatchesUsage(filePath, content,
"\\bDateRangePicker\\s+as\\s+RangePicker\\b", "<RangePicker\\b", "RangePicker");
assertRootValueImportMatchesUsage(filePath, content,
"\\bFormItem\\b", "<FormItem\\b", "FormItem");
assertRootValueImportMatchesUsage(filePath, content,
"\\bSelectOption\\b", "<SelectOption\\b", "SelectOption");
assertRootValueImportMatchesUsage(filePath, content,
"\\bTabPane\\b", "<TabPane\\b", "TabPane");
assertRootValueImportMatchesUsage(filePath, content,
"\\bTabs\\b", "<Tabs\\b", "Tabs");
assertRootTypeImportMatchesUsage(filePath, content,
"\\bRule\\b", "\\bRule\\b", "Rule");
assertRangePickerHelperImportMatchesUsage(filePath, content);
if (filePath.endsWith(".vue")) {
assertRangePickerHelperBindingMatchesComponent(filePath, content);
} else if (filePath.endsWith(".ts")) {
assertSchemaRangePickerHelperMatchesComponent(filePath, content);
}
});
}
private static void assertImportFormCompatibleWithAntdvNext(Map<String, String> result) {
assertGeneratedCodeCompatibleWithAntdvNext(result);
String content = result.get(IMPORT_FORM_PATH);
assertNotNull(content, IMPORT_FORM_PATH + ":未生成导入表单");
assertTrue(content.contains("<Upload"), IMPORT_FORM_PATH + ":应生成 Upload 组件");
assertRootValueImport(IMPORT_FORM_PATH, content, "\\bUpload\\b");
assertTrue(content.contains("beforeUpload(file: File)"), IMPORT_FORM_PATH + ":上传参数应使用原生 File 类型");
assertFalse(content.contains("UploadFile"), IMPORT_FORM_PATH + ":不应使用上传列表项类型 UploadFile");
assertFalse(content.contains("as unknown as File"), IMPORT_FORM_PATH + ":不应通过双重断言转换上传文件");
assertFalse(content.contains("FileType"), IMPORT_FORM_PATH + ":不应继续使用旧 FileType");
}
private static void assertTabsGenerated(Map<String, String> result) {
assertComponentGenerated(result, "Tabs");
assertComponentGenerated(result, "TabPane");
}
private static void assertGeneralCrudComponentsGenerated(Map<String, String> result) {
assertComponentGenerated(result, "FormItem");
assertComponentGenerated(result, "RangePicker");
assertComponentGenerated(result, "SelectOption");
}
private static void assertRuleGenerated(Map<String, String> result) {
assertTrue(result.values().stream().map(CodegenEngineVben5AntdvNextTest::removeAntdvNextRootImports)
.anyMatch(content -> Pattern.compile("\\bRule\\b").matcher(content).find()),
"生成结果应包含 Rule 类型校验规则");
}
private static void assertSchemaRangePickerGenerated(Map<String, String> result) {
String content = result.get(SCHEMA_DATA_PATH);
assertNotNull(content, SCHEMA_DATA_PATH + ":未生成 Schema 配置");
long componentCount = countMatches(SCHEMA_RANGE_PICKER_COMPONENT_PATTERN, content);
assertTrue(componentCount > 0, SCHEMA_DATA_PATH + ":应生成 RangePicker 组件配置");
}
private static void assertComponentGenerated(Map<String, String> result, String component) {
assertTrue(result.values().stream().anyMatch(content -> content.contains("<" + component)),
"生成结果应包含 " + component + " 组件");
}
private static void assertRootValueImportMatchesUsage(String filePath, String content,
String expectedImportRegex, String usageRegex,
String bindingName) {
assertRootImportMatchesUsage(filePath, content, "import\\s*\\{", expectedImportRegex,
usageRegex, bindingName);
}
private static void assertRootTypeImportMatchesUsage(String filePath, String content,
String expectedImportRegex, String usageRegex,
String bindingName) {
assertRootImportMatchesUsage(filePath, content, "import\\s+type\\s*\\{", expectedImportRegex,
usageRegex, bindingName);
}
private static void assertRootImportMatchesUsage(String filePath, String content, String importPrefix,
String expectedImportRegex, String usageRegex,
String bindingName) {
boolean imported = createRootImportPattern(importPrefix, expectedImportRegex).matcher(content).find();
boolean used = Pattern.compile(usageRegex).matcher(removeAntdvNextRootImports(content)).find();
assertEquals(used, imported, filePath + ":" + bindingName + (used ? " 缺少合法根导入" : " 存在孤立根导入"));
}
private static String removeAntdvNextRootImports(String content) {
Pattern pattern = Pattern.compile("import\\s+(?:type\\s+)?\\{[^}]*}\\s*from\\s*'antdv-next';?",
Pattern.DOTALL);
return pattern.matcher(content).replaceAll("");
}
private static void assertRootValueImport(String filePath, String content, String expectedRegex) {
assertRootImport(filePath, content, "import\\s*\\{", expectedRegex);
}
private static void assertRootValueImportsSorted(String filePath, String content) {
Matcher matcher = ANTDV_NEXT_VALUE_IMPORT_PATTERN.matcher(content);
while (matcher.find()) {
List<String> actual = Arrays.stream(matcher.group(1).split(","))
.map(String::trim)
.filter(item -> !item.isEmpty())
.map(item -> item.replaceFirst("^.*\\s+as\\s+", ""))
.collect(Collectors.toList());
List<String> expected = actual.stream().sorted(String.CASE_INSENSITIVE_ORDER)
.collect(Collectors.toList());
assertEquals(expected, actual, filePath + ":antdv-next 命名导入未按 natural 顺序排列");
}
}
private static void assertRangePickerHelperImportMatchesUsage(String filePath, String content) {
boolean imported = RANGE_PICKER_HELPER_IMPORT_PATTERN.matcher(content).find();
boolean called = RANGE_PICKER_HELPER_CALL_PATTERN.matcher(content).find();
assertEquals(called, imported,
filePath + ":getRangePickerDefaultProps() 调用与导入不一致");
}
private static void assertRangePickerHelperBindingMatchesComponent(String filePath, String content) {
long componentCount = countMatches(RANGE_PICKER_COMPONENT_PATTERN, content);
long bindingCount = countMatches(RANGE_PICKER_HELPER_BINDING_PATTERN, content);
assertEquals(componentCount, bindingCount,
filePath + ":RangePicker 与 getRangePickerDefaultProps() 绑定数量不一致");
}
private static void assertSchemaRangePickerHelperMatchesComponent(String filePath, String content) {
long componentCount = countMatches(SCHEMA_RANGE_PICKER_COMPONENT_PATTERN, content);
long helperSpreadCount = countMatches(SCHEMA_RANGE_PICKER_HELPER_SPREAD_PATTERN, content);
assertEquals(componentCount, helperSpreadCount,
filePath + ":Schema RangePicker 与 getRangePickerDefaultProps() 展开数量不一致");
}
private static long countMatches(Pattern pattern, String content) {
Matcher matcher = pattern.matcher(content);
long count = 0;
while (matcher.find()) {
count++;
}
return count;
}
private static void assertRootImport(String filePath, String content, String importPrefix, String expectedRegex) {
Pattern pattern = createRootImportPattern(importPrefix, expectedRegex);
assertTrue(pattern.matcher(content).find(), filePath + ":缺少合法的 antdv-next 根导入 " + expectedRegex);
}
private static Pattern createRootImportPattern(String importPrefix, String expectedRegex) {
return Pattern.compile(importPrefix + "[^}]*" + expectedRegex
+ "[^}]*}\\s*from\\s*'antdv-next';?", Pattern.DOTALL);
}
}
@@ -0,0 +1,73 @@
[ {
"contentPath" : "java/InfraStudentPageReqVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
}, {
"contentPath" : "java/InfraStudentRespVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
}, {
"contentPath" : "java/InfraStudentSaveReqVO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
}, {
"contentPath" : "java/InfraStudentController",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
}, {
"contentPath" : "java/InfraStudentDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
}, {
"contentPath" : "java/InfraStudentContactDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
}, {
"contentPath" : "java/InfraStudentTeacherDO",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
}, {
"contentPath" : "java/InfraStudentMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
}, {
"contentPath" : "java/InfraStudentContactMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
}, {
"contentPath" : "java/InfraStudentTeacherMapper",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
}, {
"contentPath" : "xml/InfraStudentMapper",
"filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
}, {
"contentPath" : "java/InfraStudentServiceImpl",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
}, {
"contentPath" : "java/InfraStudentService",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
}, {
"contentPath" : "java/InfraStudentServiceImplTest",
"filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
}, {
"contentPath" : "java/ErrorCodeConstants_手动操作",
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
}, {
"contentPath" : "sql/sql",
"filePath" : "sql/sql.sql"
}, {
"contentPath" : "sql/h2",
"filePath" : "sql/h2.sql"
}, {
"contentPath" : "ts/index",
"filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
}, {
"contentPath" : "vue/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
}, {
"contentPath" : "vue/search-form",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
}, {
"contentPath" : "vue/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
}, {
"contentPath" : "vue/detail/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
}, {
"contentPath" : "vue/student-contact/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue"
}, {
"contentPath" : "vue/student-teacher/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/student-teacher/form/index.vue"
} ]
@@ -0,0 +1,6 @@
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
// ========== 学生 TODO 补充编号 ==========
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");
@@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生联系人 DO
*
* @author 芋道源码
*/
@TableName("infra_student_contact")
@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentContactDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 学生编号
*/
private Long studentId;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 学生联系人 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
default PageResult<InfraStudentContactDO> selectPage(PageParam reqVO, Long studentId) {
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentContactDO>()
.eq(InfraStudentContactDO::getStudentId, studentId)
.orderByDesc(InfraStudentContactDO::getId));
}
default int deleteByStudentId(Long studentId) {
return delete(InfraStudentContactDO::getStudentId, studentId);
}
default int deleteByStudentIds(List<Long> studentIds) {
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
}
}
@@ -0,0 +1,210 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.*;
import jakarta.validation.*;
import jakarta.servlet.http.*;
import java.util.*;
import java.io.IOException;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
@Tag(name = "管理后台 - 学生")
@RestController
@RequestMapping("/infra/student")
@Validated
public class InfraStudentController {
@Resource
private InfraStudentService studentService;
@PostMapping("/create")
@Operation(summary = "创建学生")
@PreAuthorize("@ss.hasPermission('infra:student:create')")
public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
return success(studentService.createStudent(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新学生")
@PreAuthorize("@ss.hasPermission('infra:student:update')")
public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
studentService.updateStudent(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除学生")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
studentService.deleteStudent(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除学生")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
studentService.deleteStudentListByIds(ids);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得学生")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
InfraStudentDO student = studentService.getStudent(id);
return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得学生分页")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出学生 Excel")
@PreAuthorize("@ss.hasPermission('infra:student:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
BeanUtils.toBean(list, InfraStudentRespVO.class));
}
// ==================== 子表(学生联系人) ====================
@GetMapping("/student-contact/page")
@Operation(summary = "获得学生联系人分页")
@Parameter(name = "studentId", description = "学生编号")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<PageResult<InfraStudentContactDO>> getStudentContactPage(PageParam pageReqVO,
@RequestParam("studentId") Long studentId) {
return success(studentService.getStudentContactPage(pageReqVO, studentId));
}
@PostMapping("/student-contact/create")
@Operation(summary = "创建学生联系人")
@PreAuthorize("@ss.hasPermission('infra:student:create')")
public CommonResult<Long> createStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
return success(studentService.createStudentContact(studentContact));
}
@PutMapping("/student-contact/update")
@Operation(summary = "更新学生联系人")
@PreAuthorize("@ss.hasPermission('infra:student:update')")
public CommonResult<Boolean> updateStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
studentService.updateStudentContact(studentContact);
return success(true);
}
@DeleteMapping("/student-contact/delete")
@Parameter(name = "id", description = "编号", required = true)
@Operation(summary = "删除学生联系人")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentContact(@RequestParam("id") Long id) {
studentService.deleteStudentContact(id);
return success(true);
}
@DeleteMapping("/student-contact/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除学生联系人")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentContactList(@RequestParam("ids") List<Long> ids) {
studentService.deleteStudentContactListByIds(ids);
return success(true);
}
@GetMapping("/student-contact/get")
@Operation(summary = "获得学生联系人")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentContactDO> getStudentContact(@RequestParam("id") Long id) {
return success(studentService.getStudentContact(id));
}
// ==================== 子表(学生班主任) ====================
@GetMapping("/student-teacher/page")
@Operation(summary = "获得学生班主任分页")
@Parameter(name = "studentId", description = "学生编号")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<PageResult<InfraStudentTeacherDO>> getStudentTeacherPage(PageParam pageReqVO,
@RequestParam("studentId") Long studentId) {
return success(studentService.getStudentTeacherPage(pageReqVO, studentId));
}
@PostMapping("/student-teacher/create")
@Operation(summary = "创建学生班主任")
@PreAuthorize("@ss.hasPermission('infra:student:create')")
public CommonResult<Long> createStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
return success(studentService.createStudentTeacher(studentTeacher));
}
@PutMapping("/student-teacher/update")
@Operation(summary = "更新学生班主任")
@PreAuthorize("@ss.hasPermission('infra:student:update')")
public CommonResult<Boolean> updateStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
studentService.updateStudentTeacher(studentTeacher);
return success(true);
}
@DeleteMapping("/student-teacher/delete")
@Parameter(name = "id", description = "编号", required = true)
@Operation(summary = "删除学生班主任")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentTeacher(@RequestParam("id") Long id) {
studentService.deleteStudentTeacher(id);
return success(true);
}
@DeleteMapping("/student-teacher/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除学生班主任")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentTeacherList(@RequestParam("ids") List<Long> ids) {
studentService.deleteStudentTeacherListByIds(ids);
return success(true);
}
@GetMapping("/student-teacher/get")
@Operation(summary = "获得学生班主任")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentTeacherDO> getStudentTeacher(@RequestParam("id") Long id) {
return success(studentService.getStudentTeacher(id));
}
}
@@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生 DO
*
* @author 芋道源码
*/
@TableName("infra_student")
@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
/**
* 学生 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
.likeIfPresent(InfraStudentDO::getName, reqVO.getName())
.eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
.eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
.eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
.betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(InfraStudentDO::getId));
}
}
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 学生分页 Request VO")
@Data
public class InfraStudentPageReqVO extends PageParam {
@Schema(description = "名字", example = "芋头")
private String name;
@Schema(description = "出生日期")
private LocalDateTime birthday;
@Schema(description = "性别", example = "1")
private Integer sex;
@Schema(description = "是否有效", example = "true")
private Boolean enabled;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}
@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import cn.idev.excel.annotation.*;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
@Schema(description = "管理后台 - 学生 Response VO")
@Data
@ExcelIgnoreUnannotated
public class InfraStudentRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@ExcelProperty("编号")
private Long id;
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
@ExcelProperty("名字")
private String name;
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
@ExcelProperty("简介")
private String description;
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("出生日期")
private LocalDateTime birthday;
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty(value = "性别", converter = DictConvert.class)
@DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private Integer sex;
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@ExcelProperty(value = "是否有效", converter = DictConvert.class)
@DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private Boolean enabled;
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
@ExcelProperty("头像")
private String avatar;
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
@ExcelProperty("附件")
private String video;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
@ExcelProperty("备注")
private String memo;
@Schema(description = "创建时间")
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import jakarta.validation.constraints.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
@Schema(description = "管理后台 - 学生新增/修改 Request VO")
@Data
public class InfraStudentSaveReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
@NotEmpty(message = "名字不能为空")
private String name;
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
@NotEmpty(message = "简介不能为空")
private String description;
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "出生日期不能为空")
private LocalDateTime birthday;
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "性别不能为空")
private Integer sex;
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "是否有效不能为空")
private Boolean enabled;
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
@NotEmpty(message = "头像不能为空")
private String avatar;
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
@NotEmpty(message = "附件不能为空")
private String video;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
@NotEmpty(message = "备注不能为空")
private String memo;
}
@@ -0,0 +1,160 @@
package cn.iocoder.yudao.module.infra.service.demo;
import java.util.*;
import jakarta.validation.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
/**
* 学生 Service 接口
*
* @author 芋道源码
*/
public interface InfraStudentService {
/**
* 创建学生
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
/**
* 更新学生
*
* @param updateReqVO 更新信息
*/
void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
/**
* 删除学生
*
* @param id 编号
*/
void deleteStudent(Long id);
/**
* 批量删除学生
*
* @param ids 编号
*/
void deleteStudentListByIds(List<Long> ids);
/**
* 获得学生
*
* @param id 编号
* @return 学生
*/
InfraStudentDO getStudent(Long id);
/**
* 获得学生分页
*
* @param pageReqVO 分页查询
* @return 学生分页
*/
PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
// ==================== 子表(学生联系人) ====================
/**
* 获得学生联系人分页
*
* @param pageReqVO 分页查询
* @param studentId 学生编号
* @return 学生联系人分页
*/
PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId);
/**
* 创建学生联系人
*
* @param studentContact 创建信息
* @return 编号
*/
Long createStudentContact(@Valid InfraStudentContactDO studentContact);
/**
* 更新学生联系人
*
* @param studentContact 更新信息
*/
void updateStudentContact(@Valid InfraStudentContactDO studentContact);
/**
* 删除学生联系人
*
* @param id 编号
*/
void deleteStudentContact(Long id);
/**
* 批量删除学生联系人
*
* @param ids 编号
*/
void deleteStudentContactListByIds(List<Long> ids);
/**
* 获得学生联系人
*
* @param id 编号
* @return 学生联系人
*/
InfraStudentContactDO getStudentContact(Long id);
// ==================== 子表(学生班主任) ====================
/**
* 获得学生班主任分页
*
* @param pageReqVO 分页查询
* @param studentId 学生编号
* @return 学生班主任分页
*/
PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId);
/**
* 创建学生班主任
*
* @param studentTeacher 创建信息
* @return 编号
*/
Long createStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
/**
* 更新学生班主任
*
* @param studentTeacher 更新信息
*/
void updateStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
/**
* 删除学生班主任
*
* @param id 编号
*/
void deleteStudentTeacher(Long id);
/**
* 批量删除学生班主任
*
* @param ids 编号
*/
void deleteStudentTeacherListByIds(List<Long> ids);
/**
* 获得学生班主任
*
* @param id 编号
* @return 学生班主任
*/
InfraStudentTeacherDO getStudentTeacher(Long id);
}
@@ -0,0 +1,216 @@
package cn.iocoder.yudao.module.infra.service.demo;
import cn.hutool.core.collection.CollUtil;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
/**
* 学生 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class InfraStudentServiceImpl implements InfraStudentService {
@Resource
private InfraStudentMapper studentMapper;
@Resource
private InfraStudentContactMapper studentContactMapper;
@Resource
private InfraStudentTeacherMapper studentTeacherMapper;
@Override
public Long createStudent(InfraStudentSaveReqVO createReqVO) {
// 插入
InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
studentMapper.insert(student);
// 返回
return student.getId();
}
@Override
public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
// 校验存在
validateStudentExists(updateReqVO.getId());
// 更新
InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
studentMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteStudent(Long id) {
// 校验存在
validateStudentExists(id);
// 删除
studentMapper.deleteById(id);
// 删除子表
deleteStudentContactByStudentId(id);
deleteStudentTeacherByStudentId(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteStudentListByIds(List<Long> ids) {
// 删除
studentMapper.deleteByIds(ids);
// 删除子表
deleteStudentContactByStudentIds(ids);
deleteStudentTeacherByStudentIds(ids);
}
private void validateStudentExists(Long id) {
if (studentMapper.selectById(id) == null) {
throw exception(STUDENT_NOT_EXISTS);
}
}
@Override
public InfraStudentDO getStudent(Long id) {
return studentMapper.selectById(id);
}
@Override
public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
return studentMapper.selectPage(pageReqVO);
}
// ==================== 子表(学生联系人) ====================
@Override
public PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId) {
return studentContactMapper.selectPage(pageReqVO, studentId);
}
@Override
public Long createStudentContact(InfraStudentContactDO studentContact) {
studentContact.clean(); // 清理掉创建、更新时间等相关属性值
studentContactMapper.insert(studentContact);
return studentContact.getId();
}
@Override
public void updateStudentContact(InfraStudentContactDO studentContact) {
// 校验存在
validateStudentContactExists(studentContact.getId());
// 更新
studentContact.clean(); // 解决更新情况下:updateTime 不更新
studentContactMapper.updateById(studentContact);
}
@Override
public void deleteStudentContact(Long id) {
// 删除
studentContactMapper.deleteById(id);
}
@Override
public void deleteStudentContactListByIds(List<Long> ids) {
// 删除
studentContactMapper.deleteByIds(ids);
}
@Override
public InfraStudentContactDO getStudentContact(Long id) {
return studentContactMapper.selectById(id);
}
private void validateStudentContactExists(Long id) {
if (studentContactMapper.selectById(id) == null) {
throw exception(STUDENT_CONTACT_NOT_EXISTS);
}
}
private void deleteStudentContactByStudentId(Long studentId) {
studentContactMapper.deleteByStudentId(studentId);
}
private void deleteStudentContactByStudentIds(List<Long> studentIds) {
studentContactMapper.deleteByStudentIds(studentIds);
}
// ==================== 子表(学生班主任) ====================
@Override
public PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId) {
return studentTeacherMapper.selectPage(pageReqVO, studentId);
}
@Override
public Long createStudentTeacher(InfraStudentTeacherDO studentTeacher) {
// 校验是否已经存在
if (studentTeacherMapper.selectByStudentId(studentTeacher.getStudentId()) != null) {
throw exception(STUDENT_TEACHER_EXISTS);
}
// 插入
studentTeacher.clean(); // 清理掉创建、更新时间等相关属性值
studentTeacherMapper.insert(studentTeacher);
return studentTeacher.getId();
}
@Override
public void updateStudentTeacher(InfraStudentTeacherDO studentTeacher) {
// 校验存在
validateStudentTeacherExists(studentTeacher.getId());
// 更新
studentTeacher.clean(); // 解决更新情况下:updateTime 不更新
studentTeacherMapper.updateById(studentTeacher);
}
@Override
public void deleteStudentTeacher(Long id) {
// 删除
studentTeacherMapper.deleteById(id);
}
@Override
public void deleteStudentTeacherListByIds(List<Long> ids) {
// 删除
studentTeacherMapper.deleteByIds(ids);
}
@Override
public InfraStudentTeacherDO getStudentTeacher(Long id) {
return studentTeacherMapper.selectById(id);
}
private void validateStudentTeacherExists(Long id) {
if (studentTeacherMapper.selectById(id) == null) {
throw exception(STUDENT_TEACHER_NOT_EXISTS);
}
}
private void deleteStudentTeacherByStudentId(Long studentId) {
studentTeacherMapper.deleteByStudentId(studentId);
}
private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
studentTeacherMapper.deleteByStudentIds(studentIds);
}
}

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