# Conflicts:
#	yudao-module-ai/pom.xml
#	yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.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/knowledge/AiKnowledgeSegmentServiceImpl.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/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java
#	yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
#	yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImplTest.java
#	yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineVue2Test.java
#	yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineVue3Test.java
#	yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/sku/ProductSkuMapper.java
#	yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java
#	yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageListReqVO.java
#	yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/kefu/vo/message/AppKeFuMessagePageReqVO.java
#	yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
#	yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
#	yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateService.java
#	yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java
#	yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/aftersale/core/aop/AfterSaleLogAspect.java
#	yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/order/core/aop/TradeOrderLogAspect.java
#	yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java
#	yudao-module-pay/pom.xml
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeService.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java
#	yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
#	yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java
#	yudao-module-system/src/test/resources/sql/create_tables.sql
This commit is contained in:
YunaiV 2025-12-28 10:44:59 +08:00
commit 7bb0950094
68 changed files with 3650 additions and 1295 deletions

View File

@ -33,7 +33,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2025.11-jdk8-SNAPSHOT</revision>
<revision>2025.12-jdk8-SNAPSHOT</revision>
<!-- Maven 相关 -->
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>

View File

@ -1240,6 +1240,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3000, 16, '百川智能', 'BaiChuan', 'ai_platform', 0, '', '', '', '1', '2025-03-23 12:15:46', '1', '2025-03-23 12:15:46', '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 (3001, 50, 'Vben5.0 Ant Design Schema 模版', '40', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-04-23 21:47:47', '1', '2025-05-02 12:01:15', '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 (3002, 6, '支付宝余额', '6', 'brokerage_withdraw_type', 0, '', '', 'API 打款', '1', '2025-05-10 08:24:49', '1', '2025-05-10 08:24:49', '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 (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
COMMIT;
SET IDENTITY_INSERT system_dict_data OFF;
-- @formatter:on
@ -4179,7 +4180,8 @@ CREATE TABLE system_social_client
social_type smallint NOT NULL,
user_type smallint NOT NULL,
client_id varchar(255) NOT NULL,
client_secret varchar(255) NOT NULL,
client_secret varchar(2048) NOT NULL,
public_key varchar(2048) DEFAULT NULL NULL,
agent_id varchar(255) DEFAULT NULL NULL,
status smallint NOT NULL,
creator varchar(64) DEFAULT '' NULL,
@ -4195,6 +4197,7 @@ COMMENT ON COLUMN system_social_client.name IS '应用名';
COMMENT ON COLUMN system_social_client.social_type IS '社交平台的类型';
COMMENT ON COLUMN system_social_client.user_type IS '用户类型';
COMMENT ON COLUMN system_social_client.client_id IS '客户端编号';
COMMENT ON COLUMN system_social_client.public_key IS 'publicKey公钥';
COMMENT ON COLUMN system_social_client.client_secret IS '客户端密钥';
COMMENT ON COLUMN system_social_client.agent_id IS '代理编号';
COMMENT ON COLUMN system_social_client.status IS '状态';

View File

@ -1354,6 +1354,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3000, 16, '百川智能', 'BaiChuan', 'ai_platform', 0, '', '', '', '1', '2025-03-23 12:15:46', '1', '2025-03-23 12:15:46', '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 (3001, 50, 'Vben5.0 Ant Design Schema 模版', '40', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-04-23 21:47:47', '1', '2025-05-02 12:01:15', '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 (3002, 6, '支付宝余额', '6', 'brokerage_withdraw_type', 0, '', '', 'API 打款', '1', '2025-05-10 08:24:49', '1', '2025-05-10 08:24:49', '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 (3035, 40, '支付宝小程序', 40, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
COMMIT;
-- @formatter:on
@ -4461,7 +4462,8 @@ CREATE TABLE system_social_client
social_type int2 NOT NULL,
user_type int2 NOT NULL,
client_id varchar(255) NOT NULL,
client_secret varchar(255) NOT NULL,
client_secret varchar(2048) NOT NULL,
public_key varchar(2048) NULL DEFAULT NULL,
agent_id varchar(255) NULL DEFAULT NULL,
status int2 NOT NULL,
creator varchar(64) NULL DEFAULT '',
@ -4481,6 +4483,7 @@ COMMENT ON COLUMN system_social_client.social_type IS '社交平台的类型';
COMMENT ON COLUMN system_social_client.user_type IS '用户类型';
COMMENT ON COLUMN system_social_client.client_id IS '客户端编号';
COMMENT ON COLUMN system_social_client.client_secret IS '客户端密钥';
COMMENT ON COLUMN system_social_client.public_key IS 'publicKey公钥';
COMMENT ON COLUMN system_social_client.agent_id IS '代理编号';
COMMENT ON COLUMN system_social_client.status IS '状态';
COMMENT ON COLUMN system_social_client.creator IS '创建者';

View File

@ -1092,6 +1092,7 @@ 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 (3032, 50, 'Vben5.0 Element Plus Schema 模版', '50', 'infra_codegen_front_type', 0, '', '', '', '1', '2025-09-04 23:26:38', '1', '2025-09-04 23:26:38', 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 (3033, 51, 'Vben5.0 Element Plus 标准模版', '51', 'infra_codegen_front_type', 0, '', '', '', '1', '2025-09-04 23:26:49', '1', '2025-09-04 23:26:49', 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 (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', 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 (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', b'0');
COMMIT;
-- ----------------------------
@ -3669,7 +3670,8 @@ CREATE TABLE `system_social_client` (
`social_type` tinyint NOT NULL COMMENT '社交平台的类型',
`user_type` tinyint NOT NULL COMMENT '用户类型',
`client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号',
`client_secret` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端密钥',
`client_secret` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端密钥',
`public_key` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT 'publicKey公钥',
`agent_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '代理编号',
`status` tinyint NOT NULL COMMENT '状态',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',

View File

@ -1354,6 +1354,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3000, 16, '百川智能', 'BaiChuan', 'ai_platform', 0, '', '', '', '1', '2025-03-23 12:15:46', '1', '2025-03-23 12:15:46', '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 (3001, 50, 'Vben5.0 Ant Design Schema 模版', '40', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-04-23 21:47:47', '1', '2025-05-02 12:01:15', '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 (3002, 6, '支付宝余额', '6', 'brokerage_withdraw_type', 0, '', '', 'API 打款', '1', '2025-05-10 08:24:49', '1', '2025-05-10 08:24:49', '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 (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
COMMIT;
-- @formatter:on
@ -4461,7 +4462,8 @@ CREATE TABLE system_social_client
social_type int2 NOT NULL,
user_type int2 NOT NULL,
client_id varchar(255) NOT NULL,
client_secret varchar(255) NOT NULL,
client_secret varchar(2048) NOT NULL,
public_key varchar(2048) NULL DEFAULT NULL,
agent_id varchar(255) NULL DEFAULT NULL,
status int2 NOT NULL,
creator varchar(64) NULL DEFAULT '',
@ -4481,6 +4483,7 @@ COMMENT ON COLUMN system_social_client.social_type IS '社交平台的类型';
COMMENT ON COLUMN system_social_client.user_type IS '用户类型';
COMMENT ON COLUMN system_social_client.client_id IS '客户端编号';
COMMENT ON COLUMN system_social_client.client_secret IS '客户端密钥';
COMMENT ON COLUMN system_social_client.public_key IS 'publicKey公钥';
COMMENT ON COLUMN system_social_client.agent_id IS '代理编号';
COMMENT ON COLUMN system_social_client.status IS '状态';
COMMENT ON COLUMN system_social_client.creator IS '创建者';

View File

@ -1306,6 +1306,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3000, 16, '百川智能', 'BaiChuan', 'ai_platform', 0, '', '', '', '1', to_date('2025-03-23 12:15:46', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2025-03-23 12:15:46', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3001, 50, 'Vben5.0 Ant Design Schema 模版', '40', 'infra_codegen_front_type', 0, '', '', NULL, '1', to_date('2025-04-23 21:47:47', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2025-05-02 12:01:15', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3002, 6, '支付宝余额', '6', 'brokerage_withdraw_type', 0, '', '', 'API 打款', '1', to_date('2025-05-10 08:24:49', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2025-05-10 08:24:49', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', to_date('2023-11-04 13:05:38', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-11-04 13:07:16', 'SYYYY-MM-DD HH24:MI:SS'), '0');
COMMIT;
-- @formatter:on
@ -4355,7 +4356,8 @@ CREATE TABLE system_social_client
social_type smallint NOT NULL,
user_type smallint NOT NULL,
client_id varchar2(255) NULL,
client_secret varchar2(255) NULL,
client_secret varchar2(2048) NULL,
public_key varchar2(2048) DEFAULT NULL NULL,
agent_id varchar2(255) DEFAULT NULL NULL,
status smallint NOT NULL,
creator varchar2(64) DEFAULT '' NULL,
@ -4375,6 +4377,7 @@ COMMENT ON COLUMN system_social_client.social_type IS '社交平台的类型';
COMMENT ON COLUMN system_social_client.user_type IS '用户类型';
COMMENT ON COLUMN system_social_client.client_id IS '客户端编号';
COMMENT ON COLUMN system_social_client.client_secret IS '客户端密钥';
COMMENT ON COLUMN system_social_client.public_key IS 'publicKey公钥';
COMMENT ON COLUMN system_social_client.agent_id IS '代理编号';
COMMENT ON COLUMN system_social_client.status IS '状态';
COMMENT ON COLUMN system_social_client.creator IS '创建者';

File diff suppressed because it is too large Load Diff

View File

@ -3299,6 +3299,8 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
GO
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3002, 6, N'支付宝余额', N'6', N'brokerage_withdraw_type', 0, N'', N'', N'API 打款', N'1', N'2025-05-10 08:24:49', N'1', N'2025-05-10 08:24:49', N'0')
GO
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, N'支付宝小程序', N'40', N'system_social_type', 0, N'', N'', N'', N'1', N'2023-11-04 13:05:38', N'1', N'2023-11-04 13:07:16', N'0')
GO
SET IDENTITY_INSERT system_dict_data OFF
GO
COMMIT
@ -10433,7 +10435,8 @@ CREATE TABLE system_social_client
social_type tinyint NOT NULL,
user_type tinyint NOT NULL,
client_id nvarchar(255) NOT NULL,
client_secret nvarchar(255) NOT NULL,
client_secret nvarchar(2048) NOT NULL,
public_key nvarchar(2048) DEFAULT NULL NULL,
agent_id nvarchar(255) DEFAULT NULL NULL,
status tinyint NOT NULL,
creator nvarchar(64) DEFAULT '' NULL,
@ -10487,6 +10490,13 @@ EXEC sp_addextendedproperty
'COLUMN', N'client_secret'
GO
EXEC sp_addextendedproperty
'MS_Description', N'publicKey公钥',
'SCHEMA', N'dbo',
'TABLE', N'system_social_client',
'COLUMN', N'public_key'
GO
EXEC sp_addextendedproperty
'MS_Description', N'代理编号',
'SCHEMA', N'dbo',

View File

@ -14,7 +14,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2025.11-jdk8-SNAPSHOT</revision>
<revision>2025.12-jdk8-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 -->
<spring.framework.version>5.3.39</spring.framework.version>
@ -27,17 +27,17 @@
<!-- DB 相关 -->
<druid.version>1.2.27</druid.version>
<mybatis.version>3.5.19</mybatis.version>
<mybatis-plus.version>3.5.14</mybatis-plus.version>
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<mybatis-plus.version>3.5.15</mybatis-plus.version>
<mybatis-plus-join.version>1.5.5</mybatis-plus-join.version>
<dynamic-datasource.version>4.5.0</dynamic-datasource.version>
<easy-trans.version>3.0.6</easy-trans.version>
<redisson.version>3.52.0</redisson.version>
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
<kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
<opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
<taos.version>3.7.8</taos.version>
<taos.version>3.7.9</taos.version>
<!-- 消息队列 -->
<rocketmq-spring.version>2.3.4</rocketmq-spring.version>
<rocketmq-spring.version>2.3.5</rocketmq-spring.version>
<!-- 服务保障相关 -->
<lock4j.version>2.2.7</lock4j.version>
<!-- 监控相关 -->
@ -55,7 +55,7 @@
<jsoup.version>1.21.2</jsoup.version>
<lombok.version>1.18.42</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version>
<hutool-5.version>5.8.41</hutool-5.version>
<hutool-5.version>5.8.42</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>
@ -63,20 +63,21 @@
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<commons-net.version>3.12.0</commons-net.version>
<commons-lang3.version>3.20.0</commons-lang3.version>
<jsch.version>2.27.6</jsch.version>
<jsch.version>2.27.7</jsch.version>
<tika-core.version>2.9.3</tika-core.version> <!-- JDK8 不能从 2.9.3 升级到 3.X会报 JDK8 不支持 -->
<ip2region.version>2.7.0</ip2region.version>
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
<netty.version>4.2.7.Final</netty.version>
<netty.version>4.2.9.Final</netty.version>
<mqtt.version>1.2.5</mqtt.version>
<vertx.version>4.5.22</vertx.version>
<!-- 三方云服务相关 -->
<awssdk.version>2.39.2</awssdk.version>
<awssdk.version>2.40.15</awssdk.version>
<justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>2.1.3</jimureport.version>
<jimubi.version>2.2.0</jimubi.version>
<weixin-java.version>4.7.8-20251117.120146</weixin-java.version>
<jimubi.version>2.3.0</jimubi.version>
<weixin-java.version>4.7.9-20251224.161447</weixin-java.version>
<alipay-sdk-java.version>4.40.607.ALL</alipay-sdk-java.version>
<!-- 专属于 JDK8 安全漏洞升级 -->
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
</properties>
@ -588,6 +589,18 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>${alipay-sdk-java.version}</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>

View File

@ -27,10 +27,10 @@ public class PageParam implements Serializable {
@Min(value = 1, message = "页码最小值为 1")
private Integer pageNo = PAGE_NO;
@Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@Schema(description = "每页条数,最大值为 200", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "每页条数不能为空")
@Min(value = 1, message = "每页条数最小值为 1")
@Max(value = 100, message = "每页条数最大值为 100")
@Max(value = 200, message = "每页条数最大值为 200")
private Integer pageSize = PAGE_SIZE;
}

View File

@ -14,6 +14,7 @@ import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@ -37,6 +38,17 @@ public class HttpUtils {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
}
/**
* 解码 URL 参数
*
* @param value 参数
* @return 解码后的参数
*/
@SneakyThrows
public static String decodeUtf8(String value) {
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
}
@SuppressWarnings("unchecked")
public static String replaceUrlQuery(String url, String key, String value) {
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());

View File

@ -1,42 +1,85 @@
package cn.iocoder.yudao.framework.common.util.json.databind;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 基于时间戳的 LocalDateTime 序列化器
*
* @author 老五
*/
@Slf4j
public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
private static final Map<Class<?>, Map<String, Field>> FIELD_CACHE = new ConcurrentHashMap<>();
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
String fieldName = gen.getOutputContext().getCurrentName();
Class<?> clazz = gen.getOutputContext().getCurrentValue().getClass();
Field field = ReflectUtil.getField(clazz, fieldName);
// 情况一 JsonFormat 自定义注解则使用它https://github.com/YunaiV/ruoyi-vue-pro/pull/1019
JsonFormat[] jsonFormats = field.getAnnotationsByType(JsonFormat.class);
if (jsonFormats.length > 0) {
String pattern = jsonFormats[0].pattern();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
gen.writeString(formatter.format(value));
return;
String fieldName = gen.getOutputContext().getCurrentName();
if (fieldName != null) {
Object currentValue = gen.getOutputContext().getCurrentValue();
if (currentValue != null) {
Class<?> clazz = currentValue.getClass();
Map<String, Field> fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap);
Field field = fieldMap.get(fieldName);
// 进一步修复https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1480
if (field != null && field.isAnnotationPresent(JsonFormat.class)) {
JsonFormat jsonFormat = field.getAnnotation(JsonFormat.class);
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(jsonFormat.pattern());
gen.writeString(formatter.format(value));
return;
} catch (Exception ex) {
log.warn("[serialize][({}#{}) 使用 JsonFormat pattern 失败,尝试使用默认的 Long 时间戳]",
clazz.getName(), fieldName, ex);
}
}
}
}
// 情况二默认将 LocalDateTime 对象转换为 Long 时间戳
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
/**
* 构建字段映射缓存
*
* @param clazz
* @return 字段映射
*/
private Map<String, Field> buildFieldMap(Class<?> clazz) {
Map<String, Field> fieldMap = new HashMap<>();
for (Field field : ReflectUtil.getFields(clazz)) {
String fieldName = field.getName();
JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class);
if (jsonProperty != null) {
String value = jsonProperty.value();
if (StrUtil.isNotEmpty(value) && ObjUtil.notEqual("\u0000", value)) {
fieldName = value;
}
}
fieldMap.put(fieldName, field);
}
return fieldMap;
}
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.tenant.core.redis;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import lombok.extern.slf4j.Slf4j;
@ -21,6 +22,8 @@ import java.util.Set;
@Slf4j
public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
private static final String SPLIT = "#";
private final Set<String> ignoreCaches;
public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
@ -32,10 +35,11 @@ public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
@Override
public Cache getCache(String name) {
String[] names = StrUtil.splitToArray(name, SPLIT);
// 如果开启多租户 name 拼接租户后缀
if (!TenantContextHolder.isIgnore()
&& TenantContextHolder.getTenantId() != null
&& !CollUtil.contains(ignoreCaches, name)) {
&& TenantContextHolder.getTenantId() != null
&& !CollUtil.contains(ignoreCaches, names[0])) {
name = name + ":" + TenantContextHolder.getTenantId();
}
@ -43,4 +47,4 @@ public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
return super.getCache(name);
}
}
}

View File

@ -15,7 +15,6 @@ import org.springframework.scheduling.quartz.QuartzJobBean;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage;

View File

@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.ai.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* AI 知识库文档切片策略枚举
*
* @author runzhen
*/
@AllArgsConstructor
@Getter
public enum AiDocumentSplitStrategyEnum {
/**
* 自动识别文档类型并选择最佳切片策略
*/
AUTO("auto", "自动识别"),
/**
* 基于 Token 数量机械切分默认策略
*/
TOKEN("token", "Token 切分"),
/**
* 按段落切分以双换行符为分隔
*/
PARAGRAPH("paragraph", "段落切分"),
/**
* Markdown QA 格式专用切片器
* 识别二级标题作为问题保持问答对完整性
* 长答案智能切分但保留问题作为上下文
*/
MARKDOWN_QA("markdown_qa", "Markdown QA 切分"),
/**
* 语义化切分保留句子完整性
* 在段落和句子边界处切分避免截断
*/
SEMANTIC("semantic", "语义切分");
/**
* 策略代码
*/
private final String code;
/**
* 策略名称
*/
private final String name;
}

View File

@ -0,0 +1,342 @@
package cn.iocoder.yudao.module.ai.service.knowledge.splitter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.transformer.splitter.TextSplitter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Markdown QA 格式专用切片器
*
* <p>功能特点
* <ul>
* <li>识别二级标题## 作为问题标记</li>
* <li> QA 对保持完整不超过 Token 限制</li>
* <li>长答案智能切分每个片段保留完整问题作为上下文</li>
* <li>支持自定义 Token 估算器</li>
* </ul>
*
* @author runzhen
*/
@Slf4j
@SuppressWarnings("SizeReplaceableByIsEmpty")
public class MarkdownQaSplitter extends TextSplitter {
/**
* 二级标题正则匹配 "## " 开头的行
*/
private static final Pattern H2_PATTERN = Pattern.compile("^##\\s+(.+)$", Pattern.MULTILINE);
/**
* 段落分隔符双换行
*/
private static final String PARAGRAPH_SEPARATOR = "\n\n";
/**
* 句子分隔符
*/
private static final Pattern SENTENCE_PATTERN = Pattern.compile("[。!?.!?]\\s*");
/**
* 分段的最大 Token
*/
private final int chunkSize;
/**
* Token 估算器简单实现中文按字符数英文按单词数的 1.3
*/
private final TokenEstimator tokenEstimator;
public MarkdownQaSplitter(int chunkSize) {
this.chunkSize = chunkSize;
this.tokenEstimator = new SimpleTokenEstimator();
}
@Override
protected List<String> splitText(String text) {
if (StrUtil.isEmpty(text)) {
return Collections.emptyList();
}
// 解析 QA
List<QaPair> qaPairs = parseQaPairs(text);
if (CollUtil.isEmpty(qaPairs)) {
// 如果没有识别到 QA 格式按段落切分
return fallbackSplit(text);
}
// 处理每个 QA
List<String> result = new ArrayList<>();
for (QaPair qaPair : qaPairs) {
result.addAll(splitQaPair(qaPair));
}
return result;
}
/**
* 解析 Markdown QA
*
* @param content 文本内容
* @return QA 对列表
*/
private List<QaPair> parseQaPairs(String content) {
// 找到所有二级标题位置
List<QaPair> qaPairs = new ArrayList<>();
List<Integer> headingPositions = new ArrayList<>();
List<String> questions = new ArrayList<>();
Matcher matcher = H2_PATTERN.matcher(content);
while (matcher.find()) {
headingPositions.add(matcher.start());
questions.add(matcher.group(1).trim());
}
if (CollUtil.isEmpty(headingPositions)) {
return qaPairs;
}
// 提取每个 QA
for (int i = 0; i < headingPositions.size(); i++) {
int start = headingPositions.get(i);
int end = (i + 1 < headingPositions.size())
? headingPositions.get(i + 1)
: content.length();
String qaText = content.substring(start, end).trim();
String question = questions.get(i);
// 提取答案部分去掉问题标题
String answer = qaText.substring(qaText.indexOf('\n') + 1).trim();
qaPairs.add(new QaPair(question, answer, qaText));
}
return qaPairs;
}
/**
* 切分单个 QA
*
* @param qaPair QA
* @return 切分后的文本片段列表
*/
private List<String> splitQaPair(QaPair qaPair) {
// 如果整个 QA 对不超过限制保持完整
List<String> chunks = new ArrayList<>();
String fullQa = qaPair.fullText;
int qaTokens = tokenEstimator.estimate(fullQa);
if (qaTokens <= chunkSize) {
chunks.add(fullQa);
return chunks;
}
// 长答案需要切分
log.debug("QA 对超过 Token 限制 ({} > {}),开始智能切分: {}", qaTokens, chunkSize, qaPair.question);
List<String> answerChunks = splitLongAnswer(qaPair.answer, qaPair.question);
for (String answerChunk : answerChunks) {
// 每个片段都包含完整问题
String chunkText = "## " + qaPair.question + "\n" + answerChunk;
chunks.add(chunkText);
}
return chunks;
}
/**
* 切分长答案
*
* @param answer 答案文本
* @param question 问题文本
* @return 切分后的答案片段列表
*/
private List<String> splitLongAnswer(String answer, String question) {
List<String> chunks = new ArrayList<>();
// 预留问题的 Token 空间
String questionHeader = "## " + question + "\n";
int questionTokens = tokenEstimator.estimate(questionHeader);
int availableTokens = chunkSize - questionTokens - 10; // 预留 10 Token 的缓冲
// 先按段落切分
String[] paragraphs = answer.split(PARAGRAPH_SEPARATOR);
StringBuilder currentChunk = new StringBuilder();
int currentTokens = 0;
for (String paragraph : paragraphs) {
if (StrUtil.isEmpty(paragraph)) {
continue;
}
int paragraphTokens = tokenEstimator.estimate(paragraph);
// 如果单个段落就超过限制需要按句子切分
if (paragraphTokens > availableTokens) {
// 先保存当前块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
// 按句子切分长段落
chunks.addAll(splitLongParagraph(paragraph, availableTokens));
continue;
}
// 如果加上这个段落会超过限制
if (currentTokens + paragraphTokens > availableTokens && currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
if (currentChunk.length() > 0) {
currentChunk.append("\n\n");
}
// 添加段落
currentChunk.append(paragraph);
currentTokens += paragraphTokens;
}
// 添加最后一块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
}
return CollUtil.isEmpty(chunks) ? Collections.singletonList(answer) : chunks;
}
/**
* 切分长段落按句子
*
* @param paragraph 段落文本
* @param availableTokens 可用的 Token
* @return 切分后的文本片段列表
*/
private List<String> splitLongParagraph(String paragraph, int availableTokens) {
// 按句子切分
List<String> chunks = new ArrayList<>();
String[] sentences = SENTENCE_PATTERN.split(paragraph);
// 按句子累积切分
StringBuilder currentChunk = new StringBuilder();
int currentTokens = 0;
for (String sentence : sentences) {
if (StrUtil.isEmpty(sentence)) {
continue;
}
int sentenceTokens = tokenEstimator.estimate(sentence);
// 如果单个句子就超过限制强制切分
if (sentenceTokens > availableTokens) {
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
chunks.add(sentence.trim());
continue;
}
// 如果加上这个句子会超过限制
if (currentTokens + sentenceTokens > availableTokens && currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
// 添加句子
currentChunk.append(sentence);
currentTokens += sentenceTokens;
}
// 添加最后一块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
}
return chunks.isEmpty() ? Collections.singletonList(paragraph) : chunks;
}
/**
* 降级切分策略当未识别到 QA 格式时
*
* @param content 文本内容
* @return 切分后的文本片段列表
*/
private List<String> fallbackSplit(String content) {
// 按段落切分
List<String> chunks = new ArrayList<>();
String[] paragraphs = content.split(PARAGRAPH_SEPARATOR);
// 按段落累积切分
StringBuilder currentChunk = new StringBuilder();
int currentTokens = 0;
for (String paragraph : paragraphs) {
if (StrUtil.isEmpty(paragraph)) {
continue;
}
int paragraphTokens = tokenEstimator.estimate(paragraph);
// 如果加上这个段落会超过限制
if (currentTokens + paragraphTokens > chunkSize && currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
// 添加段落
if (currentChunk.length() > 0) {
currentChunk.append("\n\n");
}
currentChunk.append(paragraph);
currentTokens += paragraphTokens;
}
// 添加最后一块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
}
return chunks.isEmpty() ? Collections.singletonList(content) : chunks;
}
/**
* QA 对数据结构
*/
@AllArgsConstructor
private static class QaPair {
String question;
String answer;
String fullText;
}
/**
* Token 估算器接口
*/
public interface TokenEstimator {
int estimate(String text);
}
/**
* 简单的 Token 估算器实现
* 中文1 字符 1 Token
* 英文1 单词 1.3 Token
*/
private static class SimpleTokenEstimator implements TokenEstimator {
@Override
public int estimate(String text) {
if (StrUtil.isEmpty(text)) {
return 0;
}
int chineseChars = 0;
int englishWords = 0;
// 简单统计中英文
for (char c : text.toCharArray()) {
if (c >= 0x4E00 && c <= 0x9FA5) {
chineseChars++;
}
}
// 英文单词估算
String[] words = text.split("\\s+");
for (String word : words) {
if (word.matches(".*[a-zA-Z].*")) {
englishWords++;
}
}
return chineseChars + (int) (englishWords * 1.3);
}
}
}

View File

@ -0,0 +1,301 @@
package cn.iocoder.yudao.module.ai.service.knowledge.splitter;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.transformer.splitter.TextSplitter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 语义化文本切片器
*
* <p>功能特点
* <ul>
* <li>优先在段落边界双换行处切分</li>
* <li>其次在句子边界句号问号感叹号处切分</li>
* <li>避免在句子中间截断保持语义完整性</li>
* <li>支持中英文标点符号识别</li>
* </ul>
*
* @author runzhen
*/
@Slf4j
public class SemanticTextSplitter extends TextSplitter {
/**
* 分段的最大 Token
*/
private final int chunkSize;
/**
* 段落重叠大小用于保持上下文连贯性
*/
private final int chunkOverlap;
/**
* 段落分隔符按优先级排序
*/
private static final List<String> PARAGRAPH_SEPARATORS = Arrays.asList(
"\n\n\n", // 三个换行
"\n\n", // 双换行
"\n" // 单换行
);
/**
* 句子结束标记中英文标点
*/
private static final Pattern SENTENCE_END_PATTERN = Pattern.compile(
"[。!?.!?]+[\\s\"')】\\]]*"
);
/**
* Token 估算器
*/
private final MarkdownQaSplitter.TokenEstimator tokenEstimator;
public SemanticTextSplitter(int chunkSize, int chunkOverlap) {
this.chunkSize = chunkSize;
this.chunkOverlap = Math.min(chunkOverlap, chunkSize / 2); // 重叠不超过一半
this.tokenEstimator = new SimpleTokenEstimator();
}
public SemanticTextSplitter(int chunkSize) {
this(chunkSize, 50); // 默认重叠 50 Token
}
@Override
protected List<String> splitText(String text) {
if (StrUtil.isEmpty(text)) {
return Collections.emptyList();
}
return splitTextRecursive(text);
}
/**
* 切分文本递归策略
*
* @param text 待切分文本
* @return 切分后的文本块列表
*/
private List<String> splitTextRecursive(String text) {
List<String> chunks = new ArrayList<>();
// 如果文本不超过限制直接返回
int textTokens = tokenEstimator.estimate(text);
if (textTokens <= chunkSize) {
chunks.add(text.trim());
return chunks;
}
// 尝试按不同分隔符切分
List<String> splits = null;
String usedSeparator = null;
for (String separator : PARAGRAPH_SEPARATORS) {
if (text.contains(separator)) {
splits = Arrays.asList(text.split(Pattern.quote(separator)));
usedSeparator = separator;
break;
}
}
// 如果没有找到段落分隔符按句子切分
if (splits == null || splits.size() == 1) {
splits = splitBySentences(text);
usedSeparator = ""; // 句子切分不需要分隔符
}
// 合并小片段
chunks = mergeSplits(splits, usedSeparator);
return chunks;
}
/**
* 按句子切分
*
* @param text 待切分文本
* @return 句子列表
*/
private List<String> splitBySentences(String text) {
// 使用正则表达式匹配句子结束位置
List<String> sentences = new ArrayList<>();
int lastEnd = 0;
Matcher matcher = SENTENCE_END_PATTERN.matcher(text);
while (matcher.find()) {
String sentence = text.substring(lastEnd, matcher.end()).trim();
if (StrUtil.isNotEmpty(sentence)) {
sentences.add(sentence);
}
lastEnd = matcher.end();
}
// 添加剩余部分
if (lastEnd < text.length()) {
String remaining = text.substring(lastEnd).trim();
if (StrUtil.isNotEmpty(remaining)) {
sentences.add(remaining);
}
}
return sentences.isEmpty() ? Collections.singletonList(text) : sentences;
}
/**
* 合并切分后的小片段
*
* @param splits 切分后的片段列表
* @param separator 片段间的分隔符
* @return 合并后的文本块列表
*/
private List<String> mergeSplits(List<String> splits, String separator) {
List<String> chunks = new ArrayList<>();
List<String> currentChunks = new ArrayList<>();
int currentLength = 0;
for (String split : splits) {
if (StrUtil.isEmpty(split)) {
continue;
}
int splitTokens = tokenEstimator.estimate(split);
// 如果单个片段就超过限制进一步递归切分
if (splitTokens > chunkSize) {
// 先保存当前累积的块
if (!currentChunks.isEmpty()) {
String chunkText = String.join(separator, currentChunks);
chunks.add(chunkText.trim());
currentChunks.clear();
currentLength = 0;
}
// 递归切分大片段
if (!separator.isEmpty()) {
// 如果是段落分隔符尝试按句子切分
chunks.addAll(splitTextRecursive(split));
} else {
// 如果已经是句子级别强制按字符切分
chunks.addAll(forceSplitLongText(split));
}
continue;
}
// 计算加上分隔符的 Token
int separatorTokens = StrUtil.isEmpty(separator) ? 0 : tokenEstimator.estimate(separator);
// 如果加上这个片段会超过限制
if (!currentChunks.isEmpty() && currentLength + splitTokens + separatorTokens > chunkSize) {
// 保存当前块
String chunkText = String.join(separator, currentChunks);
chunks.add(chunkText.trim());
// 处理重叠保留最后几个片段
currentChunks = getOverlappingChunks(currentChunks, separator);
currentLength = estimateTokens(currentChunks, separator);
}
// 添加当前片段
currentChunks.add(split);
currentLength += splitTokens + separatorTokens;
}
// 添加最后一块
if (!currentChunks.isEmpty()) {
String chunkText = String.join(separator, currentChunks);
chunks.add(chunkText.trim());
}
return chunks;
}
/**
* 获取重叠的片段用于保持上下文
*
* @param chunks 当前片段列表
* @param separator 片段间的分隔符
* @return 重叠的片段列表
*/
private List<String> getOverlappingChunks(List<String> chunks, String separator) {
if (chunkOverlap == 0 || chunks.isEmpty()) {
return new ArrayList<>();
}
// 从后往前取片段直到达到重叠大小
List<String> overlapping = new ArrayList<>();
int tokens = 0;
for (int i = chunks.size() - 1; i >= 0; i--) {
String chunk = chunks.get(i);
int chunkTokens = tokenEstimator.estimate(chunk);
if (tokens + chunkTokens > chunkOverlap) {
break;
}
// 添加到重叠列表前端
overlapping.add(0, chunk);
tokens += chunkTokens + (StrUtil.isEmpty(separator) ? 0 : tokenEstimator.estimate(separator));
}
return overlapping;
}
/**
* 估算片段列表的总 Token
*
* @param chunks 片段列表
* @param separator 片段间的分隔符
* @return Token
*/
private int estimateTokens(List<String> chunks, String separator) {
int total = 0;
for (int i = 0; i < chunks.size(); i++) {
total += tokenEstimator.estimate(chunks.get(i));
if (i < chunks.size() - 1 && StrUtil.isNotEmpty(separator)) {
total += tokenEstimator.estimate(separator);
}
}
return total;
}
/**
* 强制切分长文本当语义切分失败时
*
* @param text 待切分文本
* @return 切分后的文本块列表
*/
private List<String> forceSplitLongText(String text) {
List<String> chunks = new ArrayList<>();
int charsPerChunk = (int) (chunkSize * 0.8); // 保守估计
for (int i = 0; i < text.length(); i += charsPerChunk) {
int end = Math.min(i + charsPerChunk, text.length());
String chunk = text.substring(i, end);
chunks.add(chunk.trim());
}
log.warn("文本过长,已强制按字符切分,可能影响语义完整性");
return chunks;
}
/**
* 简单的 Token 估算器实现
*/
private static class SimpleTokenEstimator implements MarkdownQaSplitter.TokenEstimator {
@Override
public int estimate(String text) {
if (StrUtil.isEmpty(text)) {
return 0;
}
int chineseChars = 0;
int englishWords = 0;
// 简单统计中英文
for (char c : text.toCharArray()) {
if (c >= 0x4E00 && c <= 0x9FA5) {
chineseChars++;
}
}
// 英文单词估算
String[] words = text.split("\\s+");
for (String word : words) {
if (word.matches(".*[a-zA-Z].*")) {
englishWords++;
}
}
return chineseChars + (int) (englishWords * 1.3);
}
}
}

View File

@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.infra.service.file.FileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
@ -44,6 +45,8 @@ public class FileController {
@PostMapping("/upload")
@Operation(summary = "上传文件", description = "模式一:后端上传文件")
@Parameter(name = "file", description = "文件附件", required = true,
schema = @Schema(type = "string", format = "binary"))
public CommonResult<String> uploadFile(@Valid FileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();
byte[] content = IoUtil.readBytes(file.getInputStream());
@ -69,6 +72,14 @@ public class FileController {
return success(fileService.createFile(createReqVO));
}
@GetMapping("/get")
@Operation(summary = "获得文件")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:file:query')")
public CommonResult<FileRespVO> getFile(@RequestParam("id") Long id) {
return success(BeanUtils.toBean(fileService.getFile(id), FileRespVO.class));
}
@DeleteMapping("/delete")
@Operation(summary = "删除文件")
@Parameter(name = "id", description = "编号", required = true)

View File

@ -23,7 +23,14 @@ public class FileUploadReqVO {
@AssertTrue(message = "文件目录不正确")
@JsonIgnore
public boolean isDirectoryValid() {
return !StrUtil.containsAny(directory, "..", "/", "\\");
return isDirectoryValid(directory);
}
public static boolean isDirectoryValid(String directory) {
// 1. 不能包含 .. 防止目录穿越
// 2. 不能以 / \ 开头防止上传到根目录
return !StrUtil.contains(directory, "..")
&& !StrUtil.startWithAny(directory, "/", "\\");
}
}

View File

@ -49,7 +49,7 @@ public class JobLogRespVO {
@Schema(description = "任务状态,参见 JobLogStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty(value = "任务状态", converter = DictConvert.class)
@DictFormat(DictTypeConstants.JOB_STATUS)
@DictFormat(DictTypeConstants.JOB_LOG_STATUS)
private Integer status;
@Schema(description = "结果数据", example = "执行成功")

View File

@ -11,11 +11,13 @@ import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.Api
import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO;
import cn.iocoder.yudao.module.infra.service.logger.ApiAccessLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@ -36,6 +38,15 @@ public class ApiAccessLogController {
@Resource
private ApiAccessLogService apiAccessLogService;
@GetMapping("/get")
@Operation(summary = "获得 API 访问日志")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('infra:api-access-log:query')")
public CommonResult<ApiAccessLogRespVO> getApiAccessLog(@RequestParam("id") Long id) {
ApiAccessLogDO apiAccessLog = apiAccessLogService.getApiAccessLog(id);
return success(BeanUtils.toBean(apiAccessLog, ApiAccessLogRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得API 访问日志分页")
@PreAuthorize("@ss.hasPermission('infra:api-access-log:query')")

View File

@ -50,6 +50,15 @@ public class ApiErrorLogController {
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得 API 错误日志")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('infra:api-error-log:query')")
public CommonResult<ApiErrorLogRespVO> getApiErrorLog(@RequestParam("id") Long id) {
ApiErrorLogDO apiErrorLog = apiErrorLogService.getApiErrorLog(id);
return success(BeanUtils.toBean(apiErrorLog, ApiErrorLogRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得 API 错误日志分页")
@PreAuthorize("@ss.hasPermission('infra:api-error-log:query')")

View File

@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.infra.service.file.FileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
@ -33,6 +34,8 @@ public class AppFileController {
@PostMapping("/upload")
@Operation(summary = "上传文件")
@Parameter(name = "file", description = "文件附件", required = true,
schema = @Schema(type = "string", format = "binary"))
@PermitAll
public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();

View File

@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.infra.controller.app.file.vo;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileUploadReqVO;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -23,7 +23,7 @@ public class AppFileUploadReqVO {
@AssertTrue(message = "文件目录不正确")
@JsonIgnore
public boolean isDirectoryValid() {
return !StrUtil.containsAny(directory, "..", "/", "\\");
return FileUploadReqVO.isDirectoryValid(directory);
}
}

View File

@ -23,6 +23,8 @@ public enum CodegenFrontTypeEnum {
VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版
VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版
VUE3_ADMIN_UNIAPP_WOT(60), // Vue3 Admin + Uniapp + WOT 标准模版
;
/**

View File

@ -47,7 +47,9 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
config.setDomain(buildDomain());
}
// 初始化 S3 客户端
Region region = Region.of("us-east-1"); // 必须填但填什么都行常见的值有 "us-east-1"不填会报错
// 优先级配置的 region > endpoint 解析的 region > 默认值 us-east-1
String regionStr = resolveRegion();
Region region = Region.of(regionStr);
AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
URI endpoint = URI.create(buildEndpoint());
@ -114,7 +116,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
public String presignGetUrl(String url, Integer expirationSeconds) {
// 1. url 转换为 path
String path = StrUtil.removePrefix(url, config.getDomain() + "/");
path = HttpUtils.removeUrlQuery(path);
path = HttpUtils.decodeUtf8(HttpUtils.removeUrlQuery(path));
// 2.1 情况一公开访问无需签名
// 考虑到老版本的兼容所以必须是 config.getEnablePublicAccess() false 才进行签名
@ -159,4 +161,73 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
return StrUtil.format("https://{}", config.getEndpoint());
}
/**
* 解析 AWS 区域
* 优先级配置的 region > endpoint 解析的 region > 默认值 us-east-1
*
* @return 区域字符串
*/
private String resolveRegion() {
// 1. 如果配置了 region直接使用
if (StrUtil.isNotEmpty(config.getRegion())) {
return config.getRegion();
}
// 2.1 尝试从 endpoint 中解析 region
String endpoint = config.getEndpoint();
if (StrUtil.isEmpty(endpoint)) {
return "us-east-1";
}
// 2.2 移除协议头http:// https://
String host = endpoint;
if (HttpUtil.isHttp(endpoint) || HttpUtil.isHttps(endpoint)) {
try {
host = URI.create(endpoint).getHost();
} catch (Exception e) {
// 解析失败使用默认值
return "us-east-1";
}
}
if (StrUtil.isEmpty(host)) {
return "us-east-1";
}
// 3.1 AWS S3 格式s3.us-west-2.amazonaws.com s3.amazonaws.com
if (host.contains("amazonaws.com")) {
// 匹配 s3.{region}.amazonaws.com 格式
if (host.startsWith("s3.") && host.contains(".amazonaws.com")) {
String regionPart = host.substring(3, host.indexOf(".amazonaws.com"));
if (StrUtil.isNotEmpty(regionPart) && !regionPart.equals("accelerate")) {
return regionPart;
}
}
// s3.amazonaws.com s3-accelerate.amazonaws.com 使用默认值
return "us-east-1";
}
// 3.2 阿里云 OSS 格式oss-cn-beijing.aliyuncs.com
if (host.contains(S3FileClientConfig.ENDPOINT_ALIYUN)) {
// 匹配 oss-{region}.aliyuncs.com 格式
if (host.startsWith("oss-") && host.contains("." + S3FileClientConfig.ENDPOINT_ALIYUN)) {
String regionPart = host.substring(4, host.indexOf("." + S3FileClientConfig.ENDPOINT_ALIYUN));
if (StrUtil.isNotEmpty(regionPart)) {
return regionPart;
}
}
}
// 3.3 腾讯云 COS 格式cos.ap-shanghai.myqcloud.com
if (host.contains(S3FileClientConfig.ENDPOINT_TENCENT)) {
// 匹配 cos.{region}.myqcloud.com 格式
if (host.startsWith("cos.") && host.contains("." + S3FileClientConfig.ENDPOINT_TENCENT)) {
String regionPart = host.substring(4, host.indexOf("." + S3FileClientConfig.ENDPOINT_TENCENT));
if (StrUtil.isNotEmpty(regionPart)) {
return regionPart;
}
}
}
// 3.4 其他情况MinIO七牛云等使用默认值
return "us-east-1";
}
}

View File

@ -82,6 +82,18 @@ public class S3FileClientConfig implements FileClientConfig {
@NotNull(message = "是否公开访问不能为空")
private Boolean enablePublicAccess;
/**
* 区域
* 1. AWS S3https://docs.aws.amazon.com/general/latest/gr/s3.html 例如说us-east-1us-west-2
* 2. MinIO可以填任意值通常使用 us-east-1
* 3. 阿里云不需要填写会自动识别
* 4. 腾讯云不需要填写会自动识别
* 5. 七牛云不需要填写会自动识别
* 6. 华为云不需要填写会自动识别
* 7. 火山云不需要填写会自动识别
*/
private String region;
@SuppressWarnings("RedundantIfStatement")
@AssertTrue(message = "domain 不能为空")
@JsonIgnore

View File

@ -1,9 +1,108 @@
package cn.iocoder.yudao.module.infra.framework.monitor.config;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import javax.servlet.DispatcherType;
/**
* Spring Boot Admin Server 配置
*
* 包含 Admin Server 的启用配置和安全配置
* 安全配置独立于 {@link cn.iocoder.yudao.framework.security.config.YudaoWebSecurityConfigurerAdapter}
* 使用 HTTP Basic 认证保护 Admin Server 端点不影响现有的 Token 认证机制
*
* @author 芋道源码
*/
@Configuration(proxyBeanMethods = false)
@EnableAdminServer
@ConditionalOnClass(name = "de.codecentric.boot.admin.server.config.AdminServerProperties") // 目的按需启动 spring boot admin 监控服务
public class AdminServerConfiguration {
@Value("${spring.boot.admin.context-path:''}")
private String adminSeverContextPath;
@Value("${spring.boot.admin.client.username:admin}")
private String username;
@Value("${spring.boot.admin.client.password:admin}")
private String password;
/**
* Spring Boot Admin 专用的 InMemoryUserDetailsManager
* 使用内存存储与系统用户隔离
*/
@Bean("adminUserDetailsManager")
public InMemoryUserDetailsManager adminUserDetailsManager(PasswordEncoder passwordEncoder) {
UserDetails adminUser = User.builder()
.username(username)
.password(passwordEncoder.encode(password))
.roles("ADMIN_SERVER")
.build();
return new InMemoryUserDetailsManager(adminUser);
}
/**
* Spring Boot Admin Server SecurityFilterChain
* 使用 @Order(1) 确保优先于默认的 SecurityFilterChain 匹配
*/
@Bean("adminServerSecurityFilterChain")
@Order(1)
public SecurityFilterChain adminServerSecurityFilterChain(HttpSecurity httpSecurity,
InMemoryUserDetailsManager adminUserDetailsManager) throws Exception {
// 登录成功后的处理器
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setTargetUrlParameter("redirectTo");
successHandler.setDefaultTargetUrl(adminSeverContextPath + "/");
// 配置 HttpSecurity 对象
httpSecurity
// 仅匹配 Admin Server 的路径
.securityMatcher(adminSeverContextPath + "/**")
// 使用独立的 UserDetailsManager
.userDetailsService(adminUserDetailsManager)
// 授权配置
.authorizeHttpRequests(auth -> auth
.requestMatchers(adminSeverContextPath + "/assets/**").permitAll() // 静态资源允许匿名访问
.requestMatchers(adminSeverContextPath + "/login").permitAll() // 登录页面允许匿名访问
.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() // 异步请求允许
.anyRequest().authenticated() // 其他请求需要认证
)
// 表单登录配置用于 Admin UI 访问
.formLogin(form -> form
.loginPage(adminSeverContextPath + "/login")
.successHandler(successHandler)
.permitAll()
)
// 登出配置
.logout(logout -> logout
.logoutUrl(adminSeverContextPath + "/logout")
.logoutSuccessUrl(adminSeverContextPath + "/login")
)
// HTTP Basic 认证用于 Admin Client 注册
.httpBasic(Customizer.withDefaults())
// CSRF 配置
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers(
adminSeverContextPath + "/instances", // Admin Client 注册端点忽略 CSRF
adminSeverContextPath + "/actuator/**" // Actuator 端点忽略 CSRF
)
);
return httpSecurity.build();
}
}

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.infra.framework.security.config;
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -13,9 +12,6 @@ import org.springframework.security.config.annotation.web.configurers.AuthorizeH
@Configuration(proxyBeanMethods = false, value = "infraSecurityConfiguration")
public class SecurityConfiguration {
@Value("${spring.boot.admin.context-path:''}")
private String adminSeverContextPath;
@Bean("infraAuthorizeRequestsCustomizer")
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
return new AuthorizeRequestsCustomizer() {
@ -32,9 +28,6 @@ public class SecurityConfiguration {
.requestMatchers("/actuator/**").permitAll();
// Druid 监控
registry.requestMatchers("/druid/**").permitAll();
// Spring Boot Admin Server 的安全配置
registry.requestMatchers(adminSeverContextPath).permitAll()
.requestMatchers(adminSeverContextPath + "/**").permitAll();
// 文件读取
registry.requestMatchers(buildAdminApi("/infra/file/*/get/**")).permitAll();
}

View File

@ -4,12 +4,14 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO;
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.dal.dataobject.db.DataSourceConfigDO;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum;
@ -17,7 +19,9 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine;
import cn.iocoder.yudao.module.infra.service.db.DataSourceConfigService;
import cn.iocoder.yudao.module.infra.service.db.DatabaseTableService;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.config.po.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.google.common.annotations.VisibleForTesting;
@ -45,6 +49,8 @@ public class CodegenServiceImpl implements CodegenService {
@Resource
private DatabaseTableService databaseTableService;
@Resource
private DataSourceConfigService dataSourceConfigService;
@Resource
private CodegenTableMapper codegenTableMapper;
@ -284,8 +290,11 @@ public class CodegenServiceImpl implements CodegenService {
}
}
// 获取数据源对应的数据库类型
DataSourceConfigDO dataSourceConfig = dataSourceConfigService.getDataSourceConfig(table.getDataSourceConfigId());
DbType dbType = JdbcUtils.getDbType(dataSourceConfig.getUrl());
// 执行生成
return codegenEngine.execute(table, columns, subTables, subColumnsList);
return codegenEngine.execute(dbType, table, columns, subTables, subColumnsList);
}
@Override

View File

@ -33,6 +33,7 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum;
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
import com.baomidou.mybatisplus.annotation.DbType;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Maps;
@ -96,7 +97,7 @@ public class CodegenEngine {
.build();
/**
* 端的配置模版
* 端的配置模版
*
* key1UI 模版的类型 {@link CodegenFrontTypeEnum#getType()}
* key2模板在 resources 的地址
@ -137,6 +138,16 @@ public class CodegenEngine {
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("api/api.ts"),
vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("api/api.ts"),
vue3UniappFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/index.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("components/search-form.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/components/search-form.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/form/index.vue"),
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"))
// VUE3_VBEN2_ANTD_SCHEMA
.put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
@ -300,16 +311,17 @@ public class CodegenEngine {
/**
* 生成代码
*
* @param dbType 数据库类型
* @param table 表定义
* @param columns table 的字段定义数组
* @param subTables 子表数组当且仅当主子表时使用
* @param subColumnsList subTables 的字段定义数组
* @return 生成的代码key 是路径value 是对应代码
*/
public Map<String, String> execute(CodegenTableDO table, List<CodegenColumnDO> columns,
public Map<String, String> execute(DbType dbType, CodegenTableDO table, List<CodegenColumnDO> columns,
List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList) {
// 1.1 初始化 bindMap 上下文
Map<String, Object> bindingMap = initBindingMap(table, columns, subTables, subColumnsList);
Map<String, Object> bindingMap = initBindingMap(dbType, table, columns, subTables, subColumnsList);
// 1.2 获得模版
Map<String, String> templates = getTemplates(table.getFrontType());
@ -387,8 +399,8 @@ public class CodegenEngine {
* @return 格式化后的代码
*/
private String prettyCode(String content, String vmPath) {
// Vue 界面去除字段后面多余的 , 逗号解决前端的 Pretty 代码格式检查的报错需要排除 vben5
if (!StrUtil.contains(vmPath, "vben5")) {
// Vue 界面去除字段后面多余的 , 逗号解决前端的 Pretty 代码格式检查的报错需要排除 vben5vue3_admin_uniapp
if (!StrUtil.containsAny(vmPath, "vben5", "vue3_admin_uniapp")) {
content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }");
}
// Vue 界面去除多的 dateFormatter只有一个的情况下说明没使用到
@ -415,10 +427,11 @@ public class CodegenEngine {
return content;
}
private Map<String, Object> initBindingMap(CodegenTableDO table, List<CodegenColumnDO> columns,
private Map<String, Object> initBindingMap(DbType dbType, CodegenTableDO table, List<CodegenColumnDO> columns,
List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList) {
// 创建 bindingMap
Map<String, Object> bindingMap = new HashMap<>(globalBindingMap);
bindingMap.put("dbType", dbType);
bindingMap.put("table", table);
bindingMap.put("columns", columns);
bindingMap.put("primaryColumn", CollectionUtils.findFirst(columns, CodegenColumnDO::getPrimaryKey)); // 主键字段
@ -617,6 +630,15 @@ public class CodegenEngine {
"src/" + path;
}
private static String vue3AdminUniappTemplatePath(String path) {
return "codegen/vue3_admin_uniapp/" + path + ".vm";
}
private static String vue3UniappFilePath(String path) {
return "yudao-ui-${sceneEnum.basePackage}-uniapp/" + // 顶级目录
"src/" + path;
}
private static String vue3VbenFilePath(String path) {
return "yudao-ui-${sceneEnum.basePackage}-vben/" + // 顶级目录
"src/" + path;

View File

@ -61,6 +61,7 @@ public interface FileService {
* @return 编号
*/
Long createFile(FileCreateReqVO createReqVO);
FileDO getFile(Long id);
/**
* 删除文件

View File

@ -152,6 +152,11 @@ public class FileServiceImpl implements FileService {
return file.getId();
}
@Override
public FileDO getFile(Long id) {
return validateFileExists(id);
}
@Override
public void deleteFile(Long id) throws Exception {
// 校验存在

View File

@ -19,6 +19,14 @@ public interface ApiAccessLogService {
*/
void createApiAccessLog(ApiAccessLogCreateReqDTO createReqDTO);
/**
* 获得 API 访问日志
*
* @param id 编号
* @return API 访问日志
*/
ApiAccessLogDO getApiAccessLog(Long id);
/**
* 获得 API 访问日志分页
*

View File

@ -45,6 +45,11 @@ public class ApiAccessLogServiceImpl implements ApiAccessLogService {
}
}
@Override
public ApiAccessLogDO getApiAccessLog(Long id) {
return apiAccessLogMapper.selectById(id);
}
@Override
public PageResult<ApiAccessLogDO> getApiAccessLogPage(ApiAccessLogPageReqVO pageReqVO) {
return apiAccessLogMapper.selectPage(pageReqVO);

View File

@ -19,6 +19,14 @@ public interface ApiErrorLogService {
*/
void createApiErrorLog(ApiErrorLogCreateReqDTO createReqDTO);
/**
* 获得 API 错误日志
*
* @param id 编号
* @return API 错误日志
*/
ApiErrorLogDO getApiErrorLog(Long id);
/**
* 获得 API 错误日志分页
*

View File

@ -58,6 +58,11 @@ public class ApiErrorLogServiceImpl implements ApiErrorLogService {
return apiErrorLogMapper.selectPage(pageReqVO);
}
@Override
public ApiErrorLogDO getApiErrorLog(Long id) {
return apiErrorLogMapper.selectById(id);
}
@Override
public void updateApiErrorLogProcess(Long id, Integer processStatus, Long processUserId) {
ApiErrorLogDO errorLog = apiErrorLogMapper.selectById(id);

View File

@ -1,3 +1,24 @@
## 通用变量定义
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export'])
##
## 宏定义:生成按钮 SQL通用部分
#macro(insertButtonSql $parentIdVar)
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, ${parentIdVar},
'', '', '', 0
);
#end
#end
##
## ======================= MySQL / OceanBase =======================
#if ($dbType.name() == 'MYSQL' || $dbType.name() == 'OCEAN_BASE')
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
@ -9,12 +30,9 @@ VALUES (
);
-- 按钮父菜单ID
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export'])
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
@ -25,4 +43,139 @@ VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId,
'', '', '', 0
);
#end
#end
##
## ======================= Oracle / 达梦 DM =======================
#elseif ($dbType.name() == 'ORACLE' || $dbType.name() == 'DM')
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'${table.classComment}管理', '', 2, 0, ${table.parentMenuId},
'${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}'
);
-- 按钮父菜单ID
-- 说明Oracle/达梦 使用序列获取上一个插入的 ID
DECLARE
v_parent_id NUMBER;
BEGIN
SELECT system_menu_seq.CURRVAL INTO v_parent_id FROM DUAL;
-- 按钮 SQL
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, v_parent_id,
'', '', '', 0
);
#end
END;
/
##
## ======================= PostgreSQL / 人大金仓 KingbaseES =======================
#elseif ($dbType.name() == 'POSTGRE_SQL' || $dbType.name() == 'KINGBASE_ES')
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'${table.classComment}管理', '', 2, 0, ${table.parentMenuId},
'${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}'
);
-- 按钮父菜单ID
-- 说明PostgreSQL/KingbaseES 使用 lastval() 获取上一个插入的序列值
DO $$
DECLARE
v_parent_id BIGINT;
BEGIN
v_parent_id := lastval();
-- 按钮 SQL
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, v_parent_id,
'', '', '', 0
);
#end
END $$;
##
## ======================= SQL Server =======================
#elseif ($dbType.name() == 'SQL_SERVER' || $dbType.name() == 'SQL_SERVER2005')
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'${table.classComment}管理', '', 2, 0, ${table.parentMenuId},
'${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}'
);
-- 按钮父菜单ID
-- 说明SQL Server 使用 SCOPE_IDENTITY() 获取上一个插入的自增 ID
DECLARE @parentId BIGINT;
SET @parentId = SCOPE_IDENTITY();
-- 按钮 SQL
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId,
'', '', '', 0
);
#end
##
## ======================= 不支持的数据库类型 =======================
#else
-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-- 注意:当前数据库类型 ${dbType.name()} 暂不支持自动生成菜单 SQL
-- 请参考以下 MySQL 语法,手动修改为您的数据库对应的语法
-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'${table.classComment}管理', '', 2, 0, ${table.parentMenuId},
'${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}'
);
-- 按钮父菜单ID
-- TODO: 请根据您的数据库类型,修改获取上一个插入 ID 的方式
-- MySQL: SELECT @parentId := LAST_INSERT_ID();
-- Oracle: SELECT system_menu_seq.CURRVAL INTO v_parent_id FROM DUAL;
-- PostgreSQL: SELECT lastval() INTO v_parent_id;
-- SQL Server: SET @parentId = SCOPE_IDENTITY();
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
#foreach ($functionName in $functionNames)
#set ($index = $foreach.count - 1)
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId,
'', '', '', 0
);
#end
#end

View File

@ -0,0 +1,56 @@
import type { PageParam, PageResult } from '@/http/types'
import { http } from '@/http/http'
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${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")
${column.javaField}#if($optional)?#end: number
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}#if($optional)?#end: Date
#elseif(${javaType} == "boolean")
${column.javaField}#if($optional)?#end: boolean
#else
${column.javaField}#if($optional)?#end: string
#end
#end
#end
}
// TODO @AI`/system/operate-log/get?id=${id}` 类似这种
/** 获取${table.classComment}分页列表 */
export function get${simpleClassName}Page(params: PageParam) {
return http.get<PageResult<${simpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/page', params)
}
/** 获取${table.classComment}详情 */
export function get${simpleClassName}(id: ${primaryTsType}) {
return http.get<${simpleClassName}>('/${table.moduleName}/${simpleClassName_strikeCase}/get?id=' + id)
}
/** 创建${table.classComment} */
export function create${simpleClassName}(data: ${simpleClassName}) {
return http.post<number>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data)
}
/** 更新${table.classComment} */
export function update${simpleClassName}(data: ${simpleClassName}) {
return http.put<boolean>('/${table.moduleName}/${simpleClassName_strikeCase}/update', data)
}
// TODO @AI`/system/operate-log/get?id=${id}` 类似这种
/** 删除${table.classComment} */
export function delete${simpleClassName}(id: ${primaryTsType}) {
return http.delete<boolean>('/${table.moduleName}/${simpleClassName_strikeCase}/delete?id=' + id)
}

View File

@ -0,0 +1,297 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `#[[${]]#getNavbarHeight()#[[}]]#px` }">
#set ($hasDict = 0)
#foreach ($column in $columns)
#if ($hasDict == 0 && $column.listOperation && $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#end
#end
#foreach($column in $columns)
#if ($column.listOperation)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
#set ($listOperationCondition = $column.listOperationCondition)
#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 ($column.htmlType == "input")
<view class="yd-search-form-item">
<view class="yd-search-form-label">
${comment}
</view>
<wd-input
v-model="formData.${javaField}"
placeholder="请输入${comment}"
clearable
/>
</view>
#elseif ($column.htmlType == "datetime" && $listOperationCondition == "BETWEEN")
#set ($AttrName = $javaField.substring(0,1).toUpperCase() + ${javaField.substring(1)})
<view class="yd-search-form-item">
<view class="yd-search-form-label">
${comment}
</view>
<view class="yd-search-form-date-range-container">
<view @click="visible${AttrName}[0] = true">
<view class="yd-search-form-date-range-picker">
{{ formatDate(formData.${javaField}?.[0]) || '开始日期' }}
</view>
</view>
-
<view @click="visible${AttrName}[1] = true">
<view class="yd-search-form-date-range-picker">
{{ formatDate(formData.${javaField}?.[1]) || '结束日期' }}
</view>
</view>
</view>
<wd-datetime-picker-view v-if="visible${AttrName}[0]" v-model="temp${AttrName}[0]" type="date" />
<view v-if="visible${AttrName}[0]" class="yd-search-form-date-range-actions">
<wd-button size="small" plain @click="visible${AttrName}[0] = false">
取消
</wd-button>
<wd-button size="small" type="primary" @click="handle${AttrName}0Confirm">
确定
</wd-button>
</view>
<wd-datetime-picker-view v-if="visible${AttrName}[1]" v-model="temp${AttrName}[1]" type="date" />
<view v-if="visible${AttrName}[1]" class="yd-search-form-date-range-actions">
<wd-button size="small" plain @click="visible${AttrName}[1] = false">
取消
</wd-button>
<wd-button size="small" type="primary" @click="handle${AttrName}1Confirm">
确定
</wd-button>
</view>
</view>
#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}" shape="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>
#else
<view class="yd-search-form-item">
<view class="yd-search-form-label">
${comment}
</view>
<wd-input
v-model="formData.${javaField}"
placeholder="请输入${comment}"
clearable
/>
</view>
#end
#end
#end
<view class="yd-search-form-actions">
<wd-button class="flex-1" plain @click="handleReset">
重置
</wd-button>
<wd-button class="flex-1" type="primary" @click="handleSearch">
搜索
</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
#set ($hasDict = 0)
#set ($hasGetDictOptions = 0)
#set ($hasGetIntDictOptions = 0)
#set ($hasGetStrDictOptions = 0)
#set ($hasGetBoolDictOptions = 0)
#set ($hasDateTimeBetween = 0)
#foreach($column in $columns)
#if ($column.listOperation && $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#if ($column.htmlType == "select" || $column.htmlType == "radio")
#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
#set ($hasGetIntDictOptions = 1)
#elseif ($column.javaType == "String")
#set ($hasGetStrDictOptions = 1)
#elseif ($column.javaType == "Boolean")
#set ($hasGetBoolDictOptions = 1)
#else
#set ($hasGetDictOptions = 1)
#end
#end
#end
#if ($hasDateTimeBetween == 0 && $column.listOperation && $column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
#set ($hasDateTimeBetween = 1)
#end
#end
import { computed, reactive, ref } from 'vue'
import { getNavbarHeight } from '@/utils'
#if ($hasDateTimeBetween == 1)
import { formatDate, formatDateRange } from '@/utils/date'
#end
#if ($hasDict == 1)
#set ($dictImportNames = "getDictLabel, ")
#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'
import { DICT_TYPE } from '@/utils/constants'
#end
const emit = defineEmits<{
search: [data: Record<string, any>]
reset: []
}>()
const visible = ref(false)
const formData = reactive({
#foreach($column in $columns)
#if ($column.listOperation)
#set ($javaType = $column.javaType.toLowerCase())
#if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
${column.javaField}: [undefined, undefined] as [number | undefined, number | undefined],
#elseif ($column.dictType && "" != $column.dictType)
#if(${javaType} == "string")
${column.javaField}: -1 as -1 | string, // -1 表示全部
#elseif(${javaType} == "boolean")
${column.javaField}: -1 as -1 | boolean, // -1 表示全部
#else
${column.javaField}: -1, // -1 表示全部
#end
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}: undefined as number | undefined,
#elseif(${javaType} == "boolean")
${column.javaField}: undefined as boolean | undefined,
#else
${column.javaField}: undefined as string | undefined,
#end
#end
#end
})
/** 搜索条件 placeholder 拼接 */
const placeholder = computed(() => {
const conditions: string[] = []
#foreach($column in $columns)
#if ($column.listOperation)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType.toLowerCase())
#set ($comment = $column.columnComment)
#if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
if (formData.${javaField}?.[0] && formData.${javaField}?.[1]) {
conditions.push(`${comment}:#[[${]]#formatDate(formData.${javaField}[0])#[[}]]#~#[[${]]#formatDate(formData.${javaField}[1])#[[}]]#`)
}
#elseif ($dictType && "" != $dictType)
if (formData.${javaField} !== -1) {
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 (formData.${javaField} !== undefined) {
conditions.push(`${comment}:#[[${]]#formData.${javaField}#[[}]]#`)
}
#else
if (formData.${javaField}) {
conditions.push(`${comment}:#[[${]]#formData.${javaField}#[[}]]#`)
}
#end
#end
#end
#end
return conditions.length > 0 ? conditions.join(' | ') : '搜索${table.classComment}'
})
#if ($hasDateTimeBetween == 1)
// 时间范围选择器状态
#foreach($column in $columns)
#if ($column.listOperation && $column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
#set ($javaField = $column.javaField)
#set ($AttrName = $javaField.substring(0,1).toUpperCase() + ${javaField.substring(1)})
const visible${AttrName} = ref<[boolean, boolean]>([false, false])
const temp${AttrName} = ref<[number, number]>([Date.now(), Date.now()])
/** ${column.columnComment}[0]确认 */
function handle${AttrName}0Confirm() {
formData.${javaField} = [temp${AttrName}.value[0], formData.${javaField}?.[1]]
visible${AttrName}.value[0] = false
}
/** ${column.columnComment}[1]确认 */
function handle${AttrName}1Confirm() {
formData.${javaField} = [formData.${javaField}?.[0], temp${AttrName}.value[1]]
visible${AttrName}.value[1] = false
}
#end
#end
#end
/** 搜索 */
function handleSearch() {
visible.value = false
emit('search', {
...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")
${column.javaField}: formatDateRange(formData.${column.javaField}),
#end
#end
#end
})
}
/** 重置 */
function handleReset() {
#foreach($column in $columns)
#if ($column.listOperation)
#if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
formData.${column.javaField} = [undefined, undefined]
#elseif ($column.dictType && "" != $column.dictType)
formData.${column.javaField} = -1
#else
formData.${column.javaField} = undefined
#end
#end
#end
visible.value = false
emit('reset')
}
</script>

View File

@ -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"
/>
<!-- 详情内容 -->
<view>
<wd-cell-group border>
#foreach($column in $columns)
#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
#set ($javaField = $column.javaField)
#set ($comment = $column.columnComment)
#if ($column.dictType && "" != $column.dictType)
<wd-cell title="${comment}">
<dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${javaField}" />
</wd-cell>
#elseif ($column.javaType == "LocalDateTime")
<wd-cell title="${comment}" :value="formatDateTime(formData?.${javaField}) || '-'" />
#else
<wd-cell title="${comment}" :value="formData?.${javaField} ?? '-'" />
#end
#end
#end
</wd-cell-group>
</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="error" :loading="deleting" @click="handleDelete"
>
删除
</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
#set ($hasDict = 0)
#foreach($column in $columns)
#if ($hasDict == 0 && $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#end
#end
#set ($hasDateTime = 0)
#foreach($column in $columns)
#if ($hasDateTime == 0 && $column.javaType == "LocalDateTime")
#set ($hasDateTime = 1)
#end
#end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { delete${simpleClassName}, get${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
import { 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?: number | any
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const toast = useToast()
const formData = ref<${simpleClassName}>()
const deleting = ref(false)
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
}
/** 加载${table.classComment}详情 */
async function getDetail() {
if (!props.id) {
return
}
try {
toast.loading('加载中...')
formData.value = await get${simpleClassName}(props.id)
} finally {
toast.close()
}
}
/** 编辑${table.classComment} */
function handleEdit() {
uni.navigateTo({
url: `/pages-${table.moduleName}/${table.businessName}/form/index?id=#[[${]]#props.id#[[}]]#`,
})
}
/** 删除${table.classComment} */
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('删除成功')
setTimeout(() => {
handleBack()
}, 500)
} finally {
deleting.value = false
}
},
})
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,253 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
:title="getTitle"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 表单区域 -->
<view>
<wd-form ref="formRef" :model="formData" :rules="formRules">
<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
## 优先判断是否有字典,有字典则使用 radio-group
#if (($column.htmlType == "select" || $column.htmlType == "radio") && $dictType && "" != $dictType)
<wd-cell title="${comment}" title-width="180rpx" prop="${javaField}" center>
<wd-radio-group v-model="formData.${javaField}" shape="button">
<wd-radio
v-for="dict in $dictMethod(DICT_TYPE.${dictType.toUpperCase()})"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-cell>
## 数字类型(无字典)
#elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte")
<wd-cell title="${comment}" title-width="180rpx" prop="${javaField}" center>
<wd-input-number
v-model="formData.${javaField}"
:min="0"
/>
</wd-cell>
## 布尔类型
#elseif (${javaType.toLowerCase()} == "boolean")
<wd-cell title="${comment}" title-width="180rpx" prop="${javaField}" center>
<wd-switch v-model="formData.${javaField}" />
</wd-cell>
## 日期时间类型
#elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "date")
#if (${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "datetime")
#end
<wd-datetime-picker
v-model="formData.${javaField}"
type="${pickerType}"
label="${comment}"
label-width="180rpx"
prop="${javaField}"
/>
## 文本域
#elseif ($column.htmlType == "textarea")
<wd-textarea
v-model="formData.${javaField}"
label="${comment}"
label-width="180rpx"
placeholder="请输入${comment}"
:maxlength="200"
show-word-limit
clearable
/>
## 默认:文本输入
#else
<wd-input
v-model="formData.${javaField}"
label="${comment}"
label-width="180rpx"
prop="${javaField}"
clearable
placeholder="请输入${comment}"
/>
#end
#end
#end
</wd-cell-group>
</wd-form>
</view>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button
type="primary"
block
:loading="formLoading"
@click="handleSubmit"
>
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
#set ($hasDict = 0)
#set ($hasGetDictOptions = 0)
#set ($hasGetIntDictOptions = 0)
#set ($hasGetStrDictOptions = 0)
#set ($hasGetBoolDictOptions = 0)
#foreach ($column in $columns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ($column.htmlType == "select" || $column.htmlType == "radio")
&& $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
#set ($hasGetIntDictOptions = 1)
#elseif ($column.javaType == "String")
#set ($hasGetStrDictOptions = 1)
#elseif ($column.javaType == "Boolean")
#set ($hasGetBoolDictOptions = 1)
#else
#set ($hasGetDictOptions = 1)
#end
#end
#end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { computed, onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
#if ($hasDict == 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 { navigateBackPlus } from '@/utils'
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
const props = defineProps<{
id?: ${primaryTsType} | any
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑${table.classComment}' : '新增${table.classComment}')
const formLoading = ref(false)
const formData = ref<${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)
${column.javaField}: undefined,
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}: 0,
#elseif(${javaType} == "boolean")
${column.javaField}: false,
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}: undefined,
#else
${column.javaField}: '',
#end
#end
#end
})
const formRules = {
#foreach($column in $columns)
#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
}
const formRef = ref()
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
}
/** 加载${table.classComment}详情 */
async function getDetail() {
if (!props.id) {
return
}
formData.value = await get${simpleClassName}(props.id)
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
if (props.id) {
await update${simpleClassName}(formData.value)
toast.success('修改成功')
} else {
await create${simpleClassName}(formData.value)
toast.success('新增成功')
}
setTimeout(() => {
handleBack()
}, 500)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,211 @@
<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" />
<!-- ${table.classComment}列表 -->
<view class="p-24rpx">
<view
v-for="item in list"
:key="item.${primaryColumn.javaField}"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
@click="handleDetail(item)"
>
<view class="p-24rpx">
#set ($titleField = "")
#set ($statusField = "")
#set ($statusDictType = "")
#foreach($column in $columns)
#if ($titleField == "" && !$column.primaryKey && $column.listOperationResult)
#set ($titleField = $column.javaField)
#set ($titleComment = $column.columnComment)
#end
#if ($statusField == "" && $column.listOperationResult && $column.dictType && "" != $column.dictType)
#set ($statusField = $column.javaField)
#set ($statusDictType = $column.dictType)
#end
#end
#if ($titleField == "")
#set ($titleField = $primaryColumn.javaField)
#end
<view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold">
{{ item.${titleField} }}
</view>
#if($statusField != "")
<dict-tag :type="DICT_TYPE.${statusDictType.toUpperCase()}" :value="item.${statusField}" />
#end
</view>
#foreach($column in $columns)
#if ($column.listOperationResult && !$column.primaryKey && $column.javaField != $titleField && $column.javaField != $statusField)
#set ($javaField = $column.javaField)
#set ($comment = $column.columnComment)
#set ($dictType = $column.dictType)
#set ($javaType = $column.javaType)
#if ($dictType && "" != $dictType)
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">${comment}</text>
<dict-tag :type="DICT_TYPE.${dictType.toUpperCase()}" :value="item.${javaField}" />
</view>
#elseif ($javaType == "LocalDateTime")
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">${comment}</text>
<text class="line-clamp-1">{{ formatDateTime(item.${javaField}) || '-' }}</text>
</view>
#else
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">${comment}</text>
<text class="line-clamp-1">{{ item.${javaField} }}</text>
</view>
#end
#end
#end
</view>
</view>
<!-- 加载更多 -->
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
<wd-status-tip image="content" tip="暂无${table.classComment}数据" />
</view>
<wd-loadmore
v-if="list.length > 0"
:state="loadMoreState"
@reload="loadMore"
/>
</view>
<!-- 新增按钮 -->
<wd-fab
v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
position="right-bottom"
type="primary"
:expandable="false"
@click="handleAdd"
/>
</view>
</template>
<script lang="ts" setup>
#set ($hasDict = 0)
#foreach($column in $columns)
#if ($hasDict == 0 && $column.listOperationResult && $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#end
#end
#set ($hasDateTime = 0)
#foreach($column in $columns)
#if ($column.listOperationResult)
#if ($hasDateTime == 0 && $column.javaType == "LocalDateTime")
#set ($hasDateTime = 1)
#end
#end
#end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import type { LoadMoreState } from '@/http/types'
import { onReachBottom } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDateTime == 1)
import { formatDateTime } from '@/utils/date'
#end
import SearchForm from './components/search-form.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const total = ref(0)
const list = ref<${simpleClassName}[]>([])
const loadMoreState = ref<LoadMoreState>('loading')
const queryParams = ref({
pageNo: 1,
pageSize: 10,
})
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 查询${table.classComment}列表 */
async function getList() {
loadMoreState.value = 'loading'
try {
const data = await get${simpleClassName}Page(queryParams.value)
list.value = [...list.value, ...data.list]
total.value = data.total
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
} catch {
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
loadMoreState.value = 'error'
}
}
/** 搜索按钮操作 */
function handleQuery(data?: Record<string, any>) {
queryParams.value = {
...data,
pageNo: 1,
pageSize: queryParams.value.pageSize,
}
list.value = []
getList()
}
/** 重置按钮操作 */
function handleReset() {
handleQuery()
}
/** 加载更多 */
function loadMore() {
if (loadMoreState.value === 'finished') {
return
}
queryParams.value.pageNo++
getList()
}
/** 新增${table.classComment} */
function handleAdd() {
uni.navigateTo({
url: '/pages-${table.moduleName}/${table.businessName}/form/index',
})
}
/** 查看详情 */
function handleDetail(item: ${simpleClassName}) {
uni.navigateTo({
url: `/pages-${table.moduleName}/${table.businessName}/detail/index?id=#[[${]]#item.${primaryColumn.javaField}#[[}]]#`,
})
}
/** 触底加载更多 */
onReachBottom(() => {
loadMore()
})
/** 初始化 */
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -85,7 +85,7 @@ public class SocialClientApiImpl implements SocialClientApi {
// 2. 获得社交用户
SocialUserRespDTO socialUser = socialUserService.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(),
SocialTypeEnum.WECHAT_MINI_PROGRAM.getType());
if (StrUtil.isBlankIfStr(socialUser.getOpenid())) {
if (ObjUtil.isNull(socialUser) || StrUtil.isBlankIfStr(socialUser.getOpenid())) {
log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO);
return;
}

View File

@ -64,6 +64,14 @@ public class PostController {
return success(true);
}
@DeleteMapping("delete-list")
@Operation(summary = "批量删除岗位")
@PreAuthorize("@ss.hasPermission('system:post:delete')")
public CommonResult<Boolean> deletePostList(@RequestParam("ids") List<Long> ids) {
postService.deletePostList(ids);
return success(true);
}
@GetMapping(value = "/get")
@Operation(summary = "获得岗位信息")
@Parameter(name = "id", description = "岗位编号", required = true, example = "1024")

View File

@ -36,6 +36,14 @@ public class LoginLogController {
@Resource
private LoginLogService loginLogService;
@GetMapping("/get")
@Operation(summary = "获得登录日志")
@PreAuthorize("@ss.hasPermission('system:login-log:query')")
public CommonResult<LoginLogRespVO> getLoginLog(Long id) {
LoginLogDO loginLog = loginLogService.getLoginLog(id);
return success(BeanUtils.toBean(loginLog, LoginLogRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得登录日志分页列表")
@PreAuthorize("@ss.hasPermission('system:login-log:query')")

View File

@ -13,11 +13,13 @@ import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO;
import cn.iocoder.yudao.module.system.service.logger.OperateLogService;
import com.fhs.core.trans.anno.TransMethodResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@ -38,6 +40,15 @@ public class OperateLogController {
@Resource
private OperateLogService operateLogService;
@GetMapping("/get")
@Operation(summary = "查看操作日志")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:operate-log:query')")
public CommonResult<OperateLogRespVO> getOperateLog(@RequestParam("id") Long id) {
OperateLogDO operateLog = operateLogService.getOperateLog(id);
return success(BeanUtils.toBean(operateLog, OperateLogRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "查看操作日志分页列表")
@PreAuthorize("@ss.hasPermission('system:operate-log:query')")
@ -50,6 +61,7 @@ public class OperateLogController {
@Operation(summary = "导出操作日志")
@GetMapping("/export-excel")
@PreAuthorize("@ss.hasPermission('system:operate-log:export')")
@TransMethodResult
@ApiAccessLog(operateType = EXPORT)
public void exportOperateLog(HttpServletResponse response, @Valid OperateLogPageReqVO exportReqVO) throws IOException {
exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);

View File

@ -1,8 +1,10 @@
package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
import com.fhs.core.trans.anno.Trans;
import com.fhs.core.trans.constant.TransType;
import com.fhs.core.trans.vo.VO;
@ -31,6 +33,11 @@ public class OperateLogRespVO implements VO {
@ExcelProperty("操作人")
private String userName;
@Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1", implementation = Integer.class)
@ExcelProperty("用户类型")
@DictFormat(DictTypeConstants.USER_TYPE)
private Integer userType;
@Schema(description = "操作模块类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单")
@ExcelProperty("操作模块类型")
private String type;

View File

@ -11,10 +11,12 @@ import cn.iocoder.yudao.module.system.controller.admin.sms.vo.log.SmsLogRespVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsLogDO;
import cn.iocoder.yudao.module.system.service.sms.SmsLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -44,6 +46,15 @@ public class SmsLogController {
return success(BeanUtils.toBean(pageResult, SmsLogRespVO.class));
}
@GetMapping("/get")
@Operation(summary = "获得短信日志")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:sms-log:query')")
public CommonResult<SmsLogRespVO> getSmsLog(@RequestParam("id") Long id) {
SmsLogDO smsLog = smsLogService.getSmsLog(id);
return success(BeanUtils.toBean(smsLog, SmsLogRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出短信日志 Excel")
@PreAuthorize("@ss.hasPermission('system:sms-log:export')")

View File

@ -27,9 +27,12 @@ public class SocialClientRespVO {
@Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "peter")
private String clientSecret;
@Schema(description = "授权方的网页应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000045")
@Schema(description = "授权方的网页应用编号", example = "2000045")
private String agentId;
@Schema(description = "publicKey 公钥", example = "2000045")
private String publicKey;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;

View File

@ -45,6 +45,9 @@ public class SocialClientSaveReqVO {
@Schema(description = "授权方的网页应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000045")
private String agentId;
@Schema(description = "publicKey 公钥", example = "2000045")
private String publicKey;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "状态不能为空")
@InEnum(CommonStatusEnum.class)
@ -58,4 +61,12 @@ public class SocialClientSaveReqVO {
|| !StrUtil.isEmpty(agentId);
}
@AssertTrue(message = "publicKey 不能为空")
@JsonIgnore
public boolean isPublicKeyValid() {
// 如果是支付宝必须填写 publicKey 属性
return !Objects.equals(socialType, SocialTypeEnum.ALIPAY_MINI_PROGRAM.getType())
|| !StrUtil.isEmpty(publicKey);
}
}

View File

@ -5,8 +5,10 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSmsSendReqVO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
@ -26,8 +28,6 @@ public interface AuthConvert {
AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class);
AuthLoginRespVO convert(OAuth2AccessTokenDO bean);
default AuthPermissionInfoRespVO convert(AdminUserDO user, List<RoleDO> roleList, List<MenuDO> menuList) {
return AuthPermissionInfoRespVO.builder()
.user(BeanUtils.toBean(user, AuthPermissionInfoRespVO.UserVO.class))

View File

@ -73,4 +73,12 @@ public class SocialClientDO extends TenantBaseDO {
*/
private String agentId;
/**
* publicKey 公钥
*
* 目前只有部分社交类型在使用
* 1. 支付宝支付宝公钥
*/
private String publicKey;
}

View File

@ -53,6 +53,12 @@ public enum SocialTypeEnum implements ArrayValuable<Integer> {
* @see <a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html">接入文档</a>
*/
WECHAT_MINI_PROGRAM(34, "WECHAT_MINI_PROGRAM"),
/**
* 支付宝小程序
*
* @see <a href="https://opendocs.alipay.com/mini/05dxgc?pathHash=1a3ecb13">接入文档</a>
*/
ALIPAY_MINI_PROGRAM(40, "ALIPAY"),
;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(SocialTypeEnum::getType).toArray(Integer[]::new);

View File

@ -91,10 +91,10 @@ public class AliyunSmsClient extends AbstractSmsClient {
@Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
// 1. 执行请求
// 参考链接 https://api.aliyun.com/document/Dysmsapi/2017-05-25/QuerySmsTemplate
// 参考链接 https://api.aliyun.com/document/Dysmsapi/2017-05-25/GetSmsTemplate
TreeMap<String, Object> queryParam = new TreeMap<>();
queryParam.put("TemplateCode", apiTemplateId);
JSONObject response = request("QuerySmsTemplate", queryParam);
JSONObject response = request("GetSmsTemplate", queryParam);
// 2.1 请求失败
String code = response.getStr("Code");

View File

@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
@ -215,13 +216,13 @@ public class AdminAuthServiceImpl implements AdminAuthService {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
// 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenDO);
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
}
@Override
public AuthLoginRespVO refreshToken(String refreshToken) {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
return AuthConvert.INSTANCE.convert(accessTokenDO);
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
}
@Override

View File

@ -12,6 +12,14 @@ import javax.validation.Valid;
*/
public interface LoginLogService {
/**
* 获得登录日志
*
* @param id 编号
* @return 登录日志
*/
LoginLogDO getLoginLog(Long id);
/**
* 获得登录日志分页
*

View File

@ -21,6 +21,11 @@ public class LoginLogServiceImpl implements LoginLogService {
@Resource
private LoginLogMapper loginLogMapper;
@Override
public LoginLogDO getLoginLog(Long id) {
return loginLogMapper.selectById(id);
}
@Override
public PageResult<LoginLogDO> getLoginLogPage(LoginLogPageReqVO pageReqVO) {
return loginLogMapper.selectPage(pageReqVO);

View File

@ -20,6 +20,14 @@ public interface OperateLogService {
*/
void createOperateLog(OperateLogCreateReqDTO createReqDTO);
/**
* 获得操作日志
*
* @param id 编号
* @return 操作日志
*/
OperateLogDO getOperateLog(Long id);
/**
* 获得操作日志分页列表
*

View File

@ -32,6 +32,11 @@ public class OperateLogServiceImpl implements OperateLogService {
operateLogMapper.insert(log);
}
@Override
public OperateLogDO getOperateLog(Long id) {
return operateLogMapper.selectById(id);
}
@Override
public PageResult<OperateLogDO> getOperateLogPage(OperateLogPageReqVO pageReqVO) {
return operateLogMapper.selectPage(pageReqVO);

View File

@ -19,8 +19,10 @@ import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -53,7 +55,7 @@ public class MailTemplateServiceImpl implements MailTemplateService {
// 插入
MailTemplateDO template = BeanUtils.toBean(createReqVO, MailTemplateDO.class)
.setParams(parseTemplateContentParams(createReqVO.getContent()));
.setParams(parseTemplateTitleAndContentParams(createReqVO.getTitle(), createReqVO.getContent()));
mailTemplateMapper.insert(template);
return template.getId();
}
@ -69,7 +71,7 @@ public class MailTemplateServiceImpl implements MailTemplateService {
// 更新
MailTemplateDO updateObj = BeanUtils.toBean(updateReqVO, MailTemplateDO.class)
.setParams(parseTemplateContentParams(updateReqVO.getContent()));
.setParams(parseTemplateTitleAndContentParams(updateReqVO.getTitle(), updateReqVO.getContent()));
mailTemplateMapper.updateById(updateObj);
}
@ -129,7 +131,77 @@ public class MailTemplateServiceImpl implements MailTemplateService {
@Override
public String formatMailTemplateContent(String content, Map<String, Object> params) {
return StrUtil.format(content, params);
// 1. 先替换模板变量
String formattedContent = StrUtil.format(content, params);
// 关联 Pull Requesthttps://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1461 讨论
// 2.1 反转义HTML特殊字符
formattedContent = unescapeHtml(formattedContent);
// 2.2 处理代码块确保<pre><code>标签格式正确
formattedContent = formatHtmlCodeBlocks(formattedContent);
// 2.3 将最外层的 pre 标签替换为 div 标签
formattedContent = replaceOuterPreWithDiv(formattedContent);
return formattedContent;
}
private String replaceOuterPreWithDiv(String content) {
if (StrUtil.isEmpty(content)) {
return content;
}
// 使用正则表达式匹配所有的 <pre> 标签包括嵌套的 <code> 标签
String regex = "(?s)<pre[^>]*>(.*?)</pre>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(content);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
// 提取 <pre> 标签内的内容
String innerContent = matcher.group(1);
// 返回 div 标签包裹的内容
matcher.appendReplacement(sb, "<div>" + innerContent + "</div>");
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 反转义 HTML 特殊字符
*
* @param input 输入字符串
* @return 反转义后的字符串
*/
private String unescapeHtml(String input) {
if (StrUtil.isEmpty(input)) {
return input;
}
return input
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("&#39;", "'")
.replace("&nbsp;", " ");
}
/**
* 格式化 HTML 中的代码块
*
* @param content 邮件内容
* @return 格式化后的邮件内容
*/
private String formatHtmlCodeBlocks(String content) {
// 匹配 <pre><code> 标签的代码块
Pattern codeBlockPattern = Pattern.compile("<pre\\s*.*?><code\\s*.*?>(.*?)</code></pre>", Pattern.DOTALL);
Matcher matcher = codeBlockPattern.matcher(content);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
// 获取代码块内容
String codeBlock = matcher.group(1);
// 为代码块添加样式
String replacement = "<pre style=\"background-color: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto;\"><code>" + codeBlock + "</code></pre>";
matcher.appendReplacement(sb, replacement);
}
matcher.appendTail(sb);
return sb.toString();
}
@Override
@ -137,14 +209,31 @@ public class MailTemplateServiceImpl implements MailTemplateService {
return mailTemplateMapper.selectCountByAccountId(accountId);
}
/**
* 解析标题和内容中的参数
*/
@VisibleForTesting
public List<String> parseTemplateTitleAndContentParams(String title, String content) {
List<String> titleParams = ReUtil.findAllGroup1(PATTERN_PARAMS, title);
List<String> contentParams = ReUtil.findAllGroup1(PATTERN_PARAMS, content);
// 合并参数并去重
List<String> allParams = new ArrayList<>(titleParams);
for (String param : contentParams) {
if (!allParams.contains(param)) {
allParams.add(param);
}
}
return allParams;
}
/**
* 获得邮件模板中的参数形如 {key}
*
* @param content 内容
* @return 参数列表
*/
private List<String> parseTemplateContentParams(String content) {
List<String> parseTemplateContentParams(String content) {
return ReUtil.findAllGroup1(PATTERN_PARAMS, content);
}
}
}

View File

@ -180,7 +180,13 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
.setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
.setRefreshToken(refreshTokenDO.getRefreshToken())
.setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds()));
accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号避免缓存到 Redis 的时候无对应的租户编号
// 优先从 refreshToken 获取租户编号避免 ThreadLocal 被污染时导致 tenantId null
// 可能关联的 issuehttps://t.zsxq.com/JIi5G
Long tenantId = refreshTokenDO.getTenantId();
if (tenantId == null) {
tenantId = TenantContextHolder.getTenantId();
}
accessTokenDO.setTenantId(tenantId);
oauth2AccessTokenMapper.insert(accessTokenDO);
// 记录到 Redis
oauth2AccessTokenRedisDAO.set(accessTokenDO);

View File

@ -255,6 +255,9 @@ public class MenuServiceImpl implements MenuService {
return;
}
// 如果 id 为空说明不用比较是否为相同 id 的菜单
if (id == null) {
throw exception(MENU_NAME_DUPLICATE);
}
if (!menu.getId().equals(id)) {
throw exception(MENU_NAME_DUPLICATE);
}
@ -277,7 +280,7 @@ public class MenuServiceImpl implements MenuService {
}
// 如果 id 为空说明不用比较是否为相同 id 的菜单
if (id == null) {
return;
throw exception(MENU_COMPONENT_NAME_DUPLICATE);
}
if (!menu.getId().equals(id)) {
throw exception(MENU_COMPONENT_NAME_DUPLICATE);

View File

@ -58,6 +58,14 @@ public interface SmsLogService {
void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success,
LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg);
/**
* 获得短信日志
*
* @param id 日志编号
* @return 短信日志
*/
SmsLogDO getSmsLog(Long id);
/**
* 获得短信日志分页
*

View File

@ -78,6 +78,11 @@ public class SmsLogServiceImpl implements SmsLogService {
.receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build());
}
@Override
public SmsLogDO getSmsLog(Long id) {
return smsLogMapper.selectById(id);
}
@Override
public PageResult<SmsLogDO> getSmsLogPage(SmsLogPageReqVO pageReqVO) {
return smsLogMapper.selectPage(pageReqVO);

View File

@ -50,6 +50,7 @@ import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthAlipayRequest;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -168,7 +169,7 @@ public class SocialClientServiceImpl implements SocialClientService {
public AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state) {
// 构建请求
AuthRequest authRequest = buildAuthRequest(socialType, userType);
AuthCallback authCallback = AuthCallback.builder().code(code).state(state).build();
AuthCallback authCallback = AuthCallback.builder().code(code).auth_code(code).state(state).build();
// 执行请求
AuthResponse<?> authResponse = authRequest.login(authCallback);
log.info("[getAuthUser][请求社交平台 type({}) request({}) response({})]", socialType,
@ -204,7 +205,15 @@ public class SocialClientServiceImpl implements SocialClientService {
if (client.getAgentId() != null) { // 如果有 agentId 则修改 agentId
newAuthConfig.setAgentId(client.getAgentId());
}
// 如果是阿里的小程序
if (SocialTypeEnum.ALIPAY_MINI_PROGRAM.getType().equals(socialType)) {
return new AuthAlipayRequest(newAuthConfig, client.getPublicKey());
}
// 2.3 设置会 request 进行后续使用
if (SocialTypeEnum.ALIPAY_MINI_PROGRAM.getType().equals(socialType)) {
// 特殊如果是支付宝的小程序多了 publicKey 属性可见 AuthConfig 里的 alipayPublicKey 字段说明
return new AuthAlipayRequest(newAuthConfig, client.getPublicKey());
}
ReflectUtil.setFieldValue(request, "config", newAuthConfig);
}
return request;

View File

@ -137,6 +137,8 @@ spring:
url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
# Spring Boot Admin Server 服务端的相关配置
context-path: /admin # 配置 Spring
@ -197,6 +199,12 @@ justauth:
client-id: ${wx.mp.app-id}
client-secret: ${wx.mp.secret}
ignore-check-redirect-uri: true
ALIPAY: # 支付宝小程序
client-id: xx
client-secret: xx
alipay-public-key: xx
ignore-check-redirect-uri: true
ignore-check-state: true
cache:
type: REDIS
prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::

View File

@ -158,6 +158,8 @@ spring:
url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
# Spring Boot Admin Server 服务端的相关配置
context-path: /admin # 配置 Spring
@ -262,6 +264,12 @@ justauth:
client-id: ${wx.mp.app-id}
client-secret: ${wx.mp.secret}
ignore-check-redirect-uri: true
ALIPAY: # 支付宝小程序
client-id: xx
client-secret: xx
alipay-public-key: xx
ignore-check-redirect-uri: true
ignore-check-state: true
cache:
type: REDIS
prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::