feat(framework): 添加springboot4版本

This commit is contained in:
2026-02-27 16:10:26 +08:00
parent 1b1bb10089
commit f9a8c58b68
304 changed files with 23942 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
### IDEA ###
.DS_Store
~/*
.idea/*
*.iml
*/target/*
*/*.iml
/.gradle/
/application.pid
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "{}" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright 2019 Zheng Jie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cutejava-springboot4</artifactId>
<groupId>cn.odboy</groupId>
<version>1.5.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cutejava-framework</artifactId>
<name>公共模块</name>
</project>
@@ -0,0 +1,34 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 用于标记匿名访问方法
*/
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {
}
@@ -0,0 +1,77 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Annotation for mapping HTTP {@code DELETE} requests onto specific handler methods. 支持匿名访问 DeleteMapping
*
* @see AnonymousGetMapping
* @see AnonymousPostMapping
* @see AnonymousPutMapping
* @see AnonymousPatchMapping
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.DELETE)
public @interface AnonymousDeleteMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class) String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class) String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class) String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class) String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class) String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class) String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class) String[] produces() default {};
}
@@ -0,0 +1,77 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Annotation for mapping HTTP {@code GET} requests onto specific handler methods.
* <p>
* 支持匿名访问 GetMapping
*
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface AnonymousGetMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class) String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class) String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class) String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class) String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class) String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*
* @since 4.3.5
*/
@AliasFor(annotation = RequestMapping.class) String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class) String[] produces() default {};
}
@@ -0,0 +1,77 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Annotation for mapping HTTP {@code PATCH} requests onto specific handler methods. 支持匿名访问 PatchMapping
*
* @see AnonymousGetMapping
* @see AnonymousPostMapping
* @see AnonymousPutMapping
* @see AnonymousDeleteMapping
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.PATCH)
public @interface AnonymousPatchMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class) String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class) String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class) String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class) String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class) String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class) String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class) String[] produces() default {};
}
@@ -0,0 +1,77 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Annotation for mapping HTTP {@code POST} requests onto specific handler methods. 支持匿名访问 PostMapping
*
* @see AnonymousGetMapping
* @see AnonymousPostMapping
* @see AnonymousPutMapping
* @see AnonymousDeleteMapping
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.POST)
public @interface AnonymousPostMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class) String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class) String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class) String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class) String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class) String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class) String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class) String[] produces() default {};
}
@@ -0,0 +1,77 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Annotation for mapping HTTP {@code PUT} requests onto specific handler methods. 支持匿名访问 PutMapping
*
* @see AnonymousGetMapping
* @see AnonymousPostMapping
* @see AnonymousPutMapping
* @see AnonymousDeleteMapping
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.PUT)
public @interface AnonymousPutMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class) String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class) String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class) String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class) String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class) String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class) String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class) String[] produces() default {};
}
@@ -0,0 +1,45 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
/**
* 通用参数
*
* @author odboy
* @date 2025-07-23
*/
public class KitArgs {
@Getter
@Setter
public static class FindByLongId extends KitObject {
@NotNull(message = "id必填")
private Long id;
}
@Getter
@Setter
public static class DeleteById extends KitObject {
@NotNull(message = "id必填")
private Long id;
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModelProperty;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class KitBaseCreateTimeTb extends KitObject {
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间: yyyy-MM-dd HH:mm:ss", hidden = true)
private Date createTime;
}
@@ -0,0 +1,37 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModelProperty;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class KitBaseTimeTb extends KitObject {
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间: yyyy-MM-dd HH:mm:ss", hidden = true)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新时间: yyyy-MM-dd HH:mm:ss", hidden = true)
private Date updateTime;
}
@@ -0,0 +1,34 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModelProperty;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class KitBaseUpdateTimeTb extends KitObject {
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新时间: yyyy-MM-dd HH:mm:ss", hidden = true)
private Date updateTime;
}
@@ -0,0 +1,67 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModelProperty;
import java.lang.reflect.Field;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.data.annotation.CreatedBy;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class KitBaseUserCreateTimeTb extends KitObject {
@CreatedBy
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建人", hidden = true)
private String
createBy;
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间: yyyy-MM-dd HH:mm:ss", hidden = true)
private Date createTime;
@Override
public String toString() {
ToStringBuilder builder = new ToStringBuilder(this);
Field[] fields = this.getClass().getDeclaredFields();
try {
for (Field f : fields) {
f.setAccessible(true);
builder.append(f.getName(), f.get(this)).append("\n");
}
} catch (Exception e) {
builder.append("toString builder encounter an error");
}
return builder.toString();
}
/* 分组校验 */
public @interface Create {
}
/* 分组校验 */
public @interface Update {
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import io.swagger.annotations.ApiModelProperty;
import java.util.Date;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
/**
* 逻辑删除通用模型
*
* @author odboy
* @date 2025-01-12
*/
@Getter
@Setter
public class KitBaseUserTimeLogicTb extends KitObject {
@CreatedBy
@TableField(value = "create_by", fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建人", hidden = true)
private String createBy;
@LastModifiedBy
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新人", hidden = true)
private String updateBy;
@TableField(value = "create_time", fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间", hidden = true)
private Date createTime;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新时间", hidden = true)
private Date updateTime;
@ApiModelProperty(value = "数据有效性", hidden = true)
@TableLogic
@TableField("available")
private Integer
available;
}
@@ -0,0 +1,75 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModelProperty;
import java.lang.reflect.Field;
import java.util.Date;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class KitBaseUserTimeTb extends KitObject {
@CreatedBy
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建人", hidden = true)
private String
createBy;
@LastModifiedBy
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新人", hidden = true)
private String updateBy;
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间: yyyy-MM-dd HH:mm:ss", hidden = true)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新时间: yyyy-MM-dd HH:mm:ss", hidden = true)
private Date updateTime;
@Override
public String toString() {
ToStringBuilder builder = new ToStringBuilder(this);
Field[] fields = this.getClass().getDeclaredFields();
try {
for (Field f : fields) {
f.setAccessible(true);
builder.append(f.getName(), f.get(this)).append("\n");
}
} catch (Exception e) {
builder.append("toString builder encounter an error");
}
return builder.toString();
}
/* 分组校验 */
public @interface Create {
}
/* 分组校验 */
public @interface Update {
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import java.io.Serializable;
/**
* 基础对象
*
* @author odboy
* @date 2022-01-12
*/
public class KitObject implements Serializable {
private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,89 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.annotations.ApiModelProperty;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.*;
/**
* 数据分页参数
*
* @author odboy
* @date 2025-01-20
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class KitPageArgs<T> extends KitObject {
@NotNull(message = "参数page不能为空")
@Min(value = 1, message = "参数page最小值为1")
@ApiModelProperty(value = "页码", example = "1")
private Integer page;
@NotNull(message = "参数size不能为空")
@Min(value = 1, message = "参数size最小值为1")
@ApiModelProperty(value = "每页数据量", example = "10")
private Integer size;
private T args;
@ApiModelProperty(value = "排序参数")
private OrderBy orderBy;
@Setter
public static class OrderBy extends KitObject {
/**
* 排序列名称
*/
private String column;
/**
* 排序方式
*/
private String orderType;
/**
* 字段属性转下划线
*
* @return /
*/
public String getColumn() {
return StrUtil.toUnderlineCase(this.column);
}
/**
* 是否顺序排序
*
* @return /
*/
public boolean isAsc() {
return "asc".equals(this.orderType);
}
/**
* 绑定 QueryWrapper,应用排序条件
*
* @param wrapper QueryWrapper<T>
*/
public <T> void bindWrapper(QueryWrapper<T> wrapper) {
wrapper.orderBy(true, this.isAsc(), this.getColumn());
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import com.baomidou.mybatisplus.core.metadata.IPage;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 结果封装类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class KitPageResult<T> extends KitObject {
private List<T> content;
private long totalElements;
public static <T> KitPageResult<T> emptyListData() {
KitPageResult<T> result = new KitPageResult<>();
result.setTotalElements(0);
result.setContent(new ArrayList<>());
return result;
}
public static <T> KitPageResult<T> listData(IPage<T> page) {
KitPageResult<T> result = new KitPageResult<>();
result.setTotalElements(page.getTotal());
result.setContent(page.getRecords());
return result;
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.base;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 元数据选项
*
* @author odboy
* @date 2025-01-13
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class KitSelectOptionVo extends KitObject {
private String label;
private String value;
private Map<String, Object> ext;
}
@@ -0,0 +1,42 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.constant;
/**
* 验证码配置枚举
*/
public enum CaptchaCodeEnum {
/**
* 算数
*/
ARITHMETIC,
/**
* 中文
*/
CHINESE,
/**
* 中文闪图
*/
CHINESE_GIF,
/**
* 闪图
*/
GIF,
/**
* 静态
*/
SPEC
}
@@ -0,0 +1,28 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum FileTypeEnum {
/* 文件类型 */
IMAGE("image", "图片"), DOC("doc", "文档"), VOICE("voice", "音频"), VIDEO("video", "视频"), OTHER("other", "其他");
private final String code;
private final String description;
}
@@ -0,0 +1,61 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum RequestMethodEnum {
/**
* 搜寻 @AnonymousGetMapping
*/
GET("GET"),
/**
* 搜寻 @AnonymousPostMapping
*/
POST("POST"),
/**
* 搜寻 @AnonymousPutMapping
*/
PUT("PUT"),
/**
* 搜寻 @AnonymousPatchMapping
*/
PATCH("PATCH"),
/**
* 搜寻 @AnonymousDeleteMapping
*/
DELETE("DELETE"),
/**
* 否则就是所有 Request 接口都放行
*/
ALL("All");
/**
* Request 类型
*/
private final String type;
public static RequestMethodEnum find(String type) {
for (RequestMethodEnum value : RequestMethodEnum.values()) {
if (value.getType().equals(type)) {
return value;
}
}
return ALL;
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.constant;
/**
* 常用静态常量
*/
public interface SystemConst {
/**
* win 系统
*/
String WIN = "win";
/**
* mac 系统
*/
String MAC = "mac";
/**
* 请求头名称
*/
String HEADER_NAME = "Authorization";
/**
* Token前缀
*/
String TOKEN_PREFIX = "Bearer";
/**
* 小数点
*/
String SYMBOL_DOT = ".";
String SYMBOL_EQUAL = "=";
String SYMBOL_ADD = "+";
String SYMBOL_SUBTRACT = "-";
String SYMBOL_AT = "@";
String PROPERTY_OS_NAME = "os.name";
String OS_NAME_WINDOWS = "Windows";
String CURRENT_APP_NAME = "cutejava";
String CURRENT_APP_TITLE = "CuteJava";
}
@@ -0,0 +1,43 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.feature;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface KitDynamicTableColumn {
/**
* 标题
*/
String value() default "";
/**
* 宽度
*/
int width() default 0;
/**
* 是否排序字段
*/
boolean sortable() default false;
}
@@ -0,0 +1,172 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.feature;
import cn.hutool.core.util.ReflectUtil;
import cn.odboy.base.KitObject;
import cn.odboy.base.KitPageArgs;
import cn.odboy.util.KitBeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Getter;
import lombok.Setter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
/**
* 动态表格
*
* @param <E> 数据表对象
* @param <T> 渲染对象
* @author odboy
* @date 2026-02-05
*/
@Getter
public class KitDynamicTableResponse<E, T> extends KitObject {
/**
* 主键
*/
@Setter
private String primaryKey = "id";
/**
* 表格列定义
*/
private List<Column> columns = new ArrayList<>();
/**
* 数据总数
*/
private Long totalElements;
/**
* 数据内容
*/
private List<T> content;
/**
* @param clazz 渲染类型
* @param pageResult 渲染类型分页参数
*/
public KitDynamicTableResponse(Class<T> clazz, IPage<E> pageResult) {
this.renderColumns(clazz);
this.setContent(clazz, pageResult);
}
/**
* @param clazz 渲染类型
* @param pageResult 渲染类型分页参数
* @param primaryKey 主键
*/
public KitDynamicTableResponse(Class<T> clazz, IPage<E> pageResult, String primaryKey) {
this.primaryKey = primaryKey;
this.renderColumns(clazz);
this.setContent(clazz, pageResult);
}
/**
* @param clazz 渲染类型
* @param pageArgs 分页参数
* @param baseMapper 数据表Mapper
* @param wrapper 查询条件
*/
public KitDynamicTableResponse(Class<T> clazz, KitPageArgs<?> pageArgs, BaseMapper<E> baseMapper, LambdaQueryWrapper<E> wrapper) {
this.renderColumns(clazz);
Page<?> pageResult = baseMapper.selectPage(new Page<>(pageArgs.getPage(), pageArgs.getSize()), wrapper);
this.setContent(clazz, pageResult);
}
/**
* @param clazz 渲染类型
* @param pageArgs 分页参数
* @param primaryKey 主键
* @param baseMapper 数据表Mapper
* @param wrapper 查询条件
*/
public KitDynamicTableResponse(Class<T> clazz, KitPageArgs<?> pageArgs, String primaryKey, BaseMapper<E> baseMapper, LambdaQueryWrapper<E> wrapper) {
this.primaryKey = primaryKey;
this.renderColumns(clazz);
Page<?> pageResult = baseMapper.selectPage(new Page<>(pageArgs.getPage(), pageArgs.getSize()), wrapper);
this.setContent(clazz, pageResult);
}
/**
* 渲染表格列
*
* @param clazz 渲染类型
*/
private void renderColumns(Class<T> clazz) {
List<Column> columns = new ArrayList<>();
if (clazz == null) {
this.columns = columns;
return;
}
Field[] fields = ReflectUtil.getFields(clazz);
for (Field field : fields) {
if ("serialVersionUID".equals(field.getName())) {
continue;
}
Column column = new Column();
column.setName(field.getName());
KitDynamicTableColumn annotation = field.getAnnotation(KitDynamicTableColumn.class);
if (annotation == null) {
column.setTitle(field.getName());
column.setWidth(null);
} else {
column.setTitle(annotation.value());
column.setWidth(annotation.width() == 0 ? null : annotation.width());
column.setSortable(annotation.sortable());
}
columns.add(column);
}
this.columns = columns;
}
/**
* @param clazz 渲染类型
* @param pageResult 渲染结果
*/
public void setContent(Class<T> clazz, IPage<?> pageResult) {
if (pageResult == null) {
return;
}
this.totalElements = pageResult.getTotal();
this.content = KitBeanUtil.copyToList(pageResult.getRecords(), clazz);
}
@Getter
@Setter
public static class Column {
/**
* 索引名称
*/
private String name;
/**
* 索引标题
*/
private String title;
/**
* 表格宽度
*/
private Integer width;
/**
* 是否排序列
*/
private Boolean sortable = false;
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.feature;
import cn.odboy.base.KitPageArgs;
import cn.odboy.framework.mybatisplus.core.KitMpQUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.experimental.UtilityClass;
/**
* 动态表格辅助工具
*
* @author odboy
*/
@UtilityClass
public class KitDynamicTableUtil {
/**
* 渲染排序规则
*
* @param pageArgs 分页参数
* @param <M> 数据表对象
* @param <T> 渲染对象
* @return /
*/
public static <M, T> QueryWrapper<M> renderOrder(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = new QueryWrapper<>();
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) {
wrapper.orderBy(true, orderBy.isAsc(), orderBy.getColumn());
}
return wrapper;
}
/**
* 渲染所有规则(需要配合 @KitMpQuery 注解使用)
*
* @param pageArgs 分页参数
* @param <M> 数据表对象
* @param <T> 渲染对象
* @return /
*/
public static <M, T> LambdaQueryWrapper<M> renderAll(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = KitMpQUtil.build(pageArgs.getArgs());
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) {
wrapper.orderBy(true, orderBy.isAsc(), orderBy.getColumn());
}
return wrapper.lambda();
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.context;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
/**
* Bean加载完毕 监听
*
* @author odboy
* @date 2025-01-16
*/
@Slf4j
@Component
public class BeanLoadFinishListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
log.info("所有的Bean都加载完毕了");
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.context;
import cn.hutool.core.util.StrUtil;
import java.net.InetAddress;
import java.net.UnknownHostException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
/**
* 主要目的是输出应用的服务地址
*
* @author odboy
* @date 2024-04-20
*/
@Slf4j
public class BootApplication {
protected static void inited(ConfigurableApplicationContext application) throws UnknownHostException {
Environment env = application.getEnvironment();
String ip = InetAddress.getLocalHost().getHostAddress();
String port = env.getProperty("server.port");
String path = StrUtil.isEmpty(env.getProperty("config.servlet.context-path")) ? ""
: env.getProperty("config.servlet.context-path");
log.info("""
----------------------------------------------------------
\tApplication is running! Access URLs:
\tLocal: \t\thttp://localhost:{}{}
\tExternal: \thttp://{}:{}{}
\tSwagger文档: \thttp://{}:{}{}/doc.html
----------------------------------------------------------""", port, path, ip, port, path, ip, port, path);
}
@Bean
public KitSpringBeanHolder springBeanHolder() {
return new KitSpringBeanHolder();
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.context;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Objects;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 获取 HttpServletRequest
*/
public class KitRequestHolder {
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(
RequestContextHolder.getRequestAttributes())).getRequest();
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.context;
import com.ezylang.evalex.EvaluationException;
import com.ezylang.evalex.Expression;
import com.ezylang.evalex.data.EvaluationValue;
import com.ezylang.evalex.parser.ParseException;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 脚本工具
*
* @author odboy
* @date 2025-09-23
*/
@Slf4j
@Component
public class KitScriptHelper {
public Object evalObj(String script, Map<String, Object> args) throws EvaluationException, ParseException {
Expression expression = new Expression(script);
expression.withValues(args);
EvaluationValue result = expression.evaluate();
return result.getValue();
}
}
@@ -0,0 +1,169 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.context;
import jakarta.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
@Slf4j
public class KitSpringBeanHolder implements ApplicationContextAware, DisposableBean {
private static final List<CallBack> CALL_BACKS = new ArrayList<>();
private static ApplicationContext applicationContext = null;
private static boolean addCallback = true;
/**
* 针对 某些初始化方法, 在SpringContextHolder 未初始化时 提交回调方法。 在SpringContextHolder 初始化后, 进行回调使用
*
* @param callBack 回调函数
*/
public synchronized static void addCallBacks(CallBack callBack) {
if (addCallback) {
CALL_BACKS.add(callBack);
} else {
log.warn("CallBack:{} 已无法添加!立即执行", callBack.getCallBackName());
callBack.executor();
}
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("all")
public static <T> T getBean(String name) {
assertContextInjected();
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(Class<T> requiredType) {
assertContextInjected();
return applicationContext.getBean(requiredType);
}
/**
* 获取SpringBoot 配置信息
*
* @param property 属性key
* @param defaultValue 默认值
* @param requiredType 返回类型
* @return /
*/
public static <T> T getProperties(String property, T defaultValue, Class<T> requiredType) {
T result = defaultValue;
try {
result = getBean(Environment.class).getProperty(property, requiredType);
} catch (Exception ignored) {
}
return result;
}
/**
* 获取SpringBoot 配置信息
*
* @param property 属性key
* @return /
*/
public static String getProperties(String property) {
return getProperties(property, null, String.class);
}
/**
* 获取SpringBoot 配置信息
*
* @param property 属性key
* @param requiredType 返回类型
* @return /
*/
public static <T> T getProperties(String property, Class<T> requiredType) {
return getProperties(property, null, requiredType);
}
/**
* 检查ApplicationContext不为空.
*/
private static void assertContextInjected() {
if (applicationContext == null) {
throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" +
".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder.");
}
}
/**
* 清除SpringContextHolder中的ApplicationContext为Null.
*/
private static void clearHolder() {
log.debug("清除SpringContextHolder中的ApplicationContext:{}", applicationContext);
applicationContext = null;
}
/**
* 获取 @Service 的所有 bean 名称
*
* @return /
*/
public static List<String> getAllServiceBeanName() {
return new ArrayList<>(Arrays.asList(applicationContext.getBeanNamesForAnnotation(Service.class)));
}
@Override
public void destroy() {
KitSpringBeanHolder.clearHolder();
}
@Override
public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
if (KitSpringBeanHolder.applicationContext != null) {
log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:{}", KitSpringBeanHolder.applicationContext);
}
KitSpringBeanHolder.applicationContext = applicationContext;
if (addCallback) {
for (CallBack callBack : KitSpringBeanHolder.CALL_BACKS) {
callBack.executor();
}
CALL_BACKS.clear();
}
KitSpringBeanHolder.addCallback = false;
}
public interface CallBack {
/**
* 回调执行方法
*/
void executor();
/**
* 本回调任务名称
*
* @return /
*/
default String getCallBackName() {
return Thread.currentThread().threadId() + ":" + this.getClass().getName();
}
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
/**
* 统一异常处理
*/
@Getter
public class BadRequestException extends RuntimeException {
private Integer status = BAD_REQUEST.value();
public BadRequestException(String msg) {
super(msg);
}
public BadRequestException(HttpStatus status, String msg) {
super(msg);
this.status = status.value();
}
public BadRequestException(Throwable cause) {
super(cause.getMessage());
this.status = BAD_REQUEST.value();
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
@Getter
public class ServerException extends RuntimeException {
private Integer status = INTERNAL_SERVER_ERROR.value();
public ServerException(String msg) {
super(msg);
}
public ServerException(HttpStatus status, String msg) {
super(msg);
this.status = status.value();
}
public ServerException(Throwable cause) {
super(cause.getMessage());
this.status = INTERNAL_SERVER_ERROR.value();
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.exception.handler;
import lombok.Data;
@Data
public final class ApiError {
private Integer status = 400;
private Long timestamp;
private String message;
private ApiError() {
timestamp = System.currentTimeMillis();
}
public static ApiError error(final String message) {
ApiError apiError = new ApiError();
apiError.setMessage(message);
return apiError;
}
public static ApiError error(final Integer status, final String message) {
ApiError apiError = new ApiError();
apiError.setStatus(status);
apiError.setMessage(message);
return apiError;
}
}
@@ -0,0 +1,102 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.exception.handler;
import cn.odboy.framework.exception.BadRequestException;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理
*
* @author odboy
*/
@Slf4j
@Hidden
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理所有不可知的异常
*/
@ExceptionHandler(Throwable.class)
public ResponseEntity<ApiError> handleThrowable(Throwable e) {
// 打印堆栈信息
log.error("handleThrowable", e);
return this.buildResponseEntity(ApiError.error(e.getMessage()));
}
/**
* 处理所有不可知的异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleException(Exception e) {
// 打印堆栈信息
log.error("handleException", e);
return this.buildResponseEntity(ApiError.error(e.getMessage()));
}
/**
* BadCredentialsException
*/
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiError> handleBadCredentialsException(BadCredentialsException e) {
// 打印堆栈信息
String message = "坏的凭证".equals(e.getMessage()) ? "用户名或密码不正确" : e.getMessage();
log.error(message);
return this.buildResponseEntity(ApiError.error(message));
}
/**
* 处理自定义异常
*/
@ExceptionHandler(value = BadRequestException.class)
public ResponseEntity<ApiError> handleBadRequestException(BadRequestException e) {
// 打印堆栈信息
log.error("handleBadRequestException", e);
return this.buildResponseEntity(ApiError.error(e.getStatus(), e.getMessage()));
}
/**
* 处理所有接口数据验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
// 打印堆栈信息
log.error("handleMethodArgumentNotValidException", e);
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
String message = objectError.getDefaultMessage();
if (objectError instanceof FieldError) {
message = ((FieldError) objectError).getField() + ": " + message;
}
return this.buildResponseEntity(ApiError.error(message));
}
/**
* 统一返回
*/
private ResponseEntity<ApiError> buildResponseEntity(ApiError apiError) {
return new ResponseEntity<>(apiError, HttpStatus.valueOf(apiError.getStatus()));
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.logging;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 统计执行时长(仅对代理方法有效)
*
* @author odboy
*/
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExecutionTimeLog {
/**
* 接口描述
*/
String description();
/**
* 是否展示异常日志
*/
boolean showErrLog() default true;
}
@@ -0,0 +1,54 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.logging;
import cn.hutool.core.date.TimeInterval;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 执行时间统计
*
* @author odboy
* @date 2025-05-07
*/
@Slf4j
@Aspect
@Component
public class ExecutionTimeLogAspect {
@Around("@annotation(executionTimeLog)")
public Object aroundFace(ProceedingJoinPoint joinPoint, ExecutionTimeLog executionTimeLog)
throws Throwable {
TimeInterval timeInterval = new TimeInterval();
try {
Object proceed = joinPoint.proceed();
log.info("[{}] 接口执行成功,耗时: {} ms", executionTimeLog.description(),
timeInterval.intervalMs());
return proceed;
} catch (Exception e) {
if (executionTimeLog.showErrLog()) {
log.error("[{}] 接口执行失败,耗时: {} ms", executionTimeLog.description(),
timeInterval.intervalMs(), e);
}
throw e;
}
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.monitor.controller;
import cn.odboy.annotation.AnonymousGetMapping;
import cn.odboy.framework.monitor.service.KitHealthCheckPointService;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 健康检查接口
*/
@RestController
@RequestMapping("/healthCheck")
@Tag(name = "系统:健康检查接口")
public class HealthCheckController {
@Autowired
private KitHealthCheckPointService kitHealthCheckPointService;
/**
* 就绪检查
*/
@AnonymousGetMapping(value = "/readiness")
public ResponseEntity<?> doReadiness() {
return kitHealthCheckPointService.doReadiness();
}
/**
* 存活检查
*/
@AnonymousGetMapping(value = "/liveness")
public ResponseEntity<?> doLiveness() {
return kitHealthCheckPointService.doLiveness();
}
// /**
// * 访问首页提示
// */
// @AnonymousGetMapping("/")
// public String index() {
// return "success";
// }
}
@@ -0,0 +1,32 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.monitor.service;
import org.springframework.http.ResponseEntity;
public interface KitHealthCheckPointService {
/**
* 就绪检查
*/
ResponseEntity<?> doReadiness();
/**
* 存活检查
*/
ResponseEntity<?> doLiveness();
}
@@ -0,0 +1,120 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.config;
import cn.odboy.framework.exception.BadRequestException;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Getter
@Slf4j
public enum DataTypeEnum {
/**
* mysql
*/
MYSQL("mysql", "mysql", "com.mysql.jdbc.Driver", "`", "`", "'", "'"),
/**
* oracle
*/
ORACLE("oracle", "oracle", "oracle.jdbc.driver.OracleDriver", "\"", "\"", "\"", "\""),
/**
* sql config
*/
SQLSERVER("sqlserver", "sqlserver", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "\"", "\"", "\"", "\""),
/**
* h2
*/
H2("h2", "h2", "org.h2.Driver", "`", "`", "\"", "\""),
/**
* phoenix
*/
PHOENIX("phoenix", "hbase phoenix", "org.apache.phoenix.jdbc.PhoenixDriver", "", "", "\"", "\""),
/**
* mongo
*/
MONGODB("mongo", "mongodb", "mongodb.jdbc.MongoDriver", "`", "`", "\"", "\""),
/**
* sql4es
*/
ELASTICSEARCH("sql4es", "elasticsearch", "nl.anchormen.sql4es.jdbc.ESDriver", "", "", "'", "'"),
/**
* presto
*/
PRESTO("presto", "presto", "com.facebook.presto.jdbc.PrestoDriver", "", "", "\"", "\""),
/**
* moonbox
*/
MOONBOX("moonbox", "moonbox", "moonbox.jdbc.MbDriver", "`", "`", "`", "`"),
/**
* cassandra
*/
CASSANDRA("cassandra", "cassandra", "com.github.adejanovski.cassandra.jdbc.CassandraDriver", "", "", "'", "'"),
/**
* click house
*/
CLICKHOUSE("clickhouse", "clickhouse", "ru.yandex.clickhouse.ClickHouseDriver", "", "", "\"", "\""),
/**
* kylin
*/
KYLIN("kylin", "kylin", "org.apache.kylin.jdbc.Driver", "\"", "\"", "\"", "\""),
/**
* vertica
*/
VERTICA("vertica", "vertica", "com.vertica.jdbc.Driver", "", "", "'", "'"),
/**
* sap
*/
HANA("sap", "sap hana", "com.sap.db.jdbc.Driver", "", "", "'", "'"),
/**
* impala
*/
IMPALA("impala", "impala", "com.cloudera.impala.jdbc41.Driver", "", "", "'", "'");
private static final String JDBC_URL_PREFIX = "jdbc:";
private final String feature;
private final String desc;
private final String driver;
private final String keywordPrefix;
private final String keywordSuffix;
private final String aliasPrefix;
private final String aliasSuffix;
DataTypeEnum(String feature, String desc, String driver, String keywordPrefix, String keywordSuffix,
String aliasPrefix, String aliasSuffix) {
this.feature = feature;
this.desc = desc;
this.driver = driver;
this.keywordPrefix = keywordPrefix;
this.keywordSuffix = keywordSuffix;
this.aliasPrefix = aliasPrefix;
this.aliasSuffix = aliasSuffix;
}
public static DataTypeEnum urlOf(String jdbcUrl) {
String url = jdbcUrl.toLowerCase().trim();
for (DataTypeEnum dataTypeEnum : values()) {
if (url.startsWith(JDBC_URL_PREFIX + dataTypeEnum.feature)) {
try {
Class.forName(dataTypeEnum.getDriver());
} catch (ClassNotFoundException e) {
throw new BadRequestException("Unable to get driver instance: " + jdbcUrl);
}
return dataTypeEnum;
}
}
return null;
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.config;
/**
* 动态查询 枚举
*/
public enum KitMpQTypeEnum {
/**
* 相等
*/
EQUAL
/**
* 大于等于
*/
, GREATER_THAN
/**
* 小于等于
*/
, LESS_THAN
/**
* 中模糊查询
*/
, INNER_LIKE
/**
* 左模糊查询
*/
, LEFT_LIKE
/**
* 右模糊查询
*/
, RIGHT_LIKE
/**
* 小于
*/
, LESS_THAN_NQ
/**
* 包含
*/
, IN
/**
* 不包含
*/
, NOT_IN
/**
* 不等于
*/
, NOT_EQUAL
/**
* between(注:被注解的属性一定得是数组或者集合, 长度大于等于2)
*/
, BETWEEN
/**
* 不为空
*/
, NOT_NULL
/**
* 为空
*/
, IS_NULL
}
@@ -0,0 +1,47 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 查询描述
*
* @date 2023-11-25
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface KitMpQuery {
/**
* 基本对象的属性名
*/
String propName() default "";
/**
* 查询方式
*/
KitMpQTypeEnum type() default KitMpQTypeEnum.EQUAL;
/**
* 多字段模糊搜索, 仅支持String类型字段, 多个用逗号隔开, 如@MpQuery(blurry = "email,username")
*/
String blurry() default "";
}
@@ -0,0 +1,69 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.config;
import cn.hutool.core.util.StrUtil;
import com.p6spy.engine.spy.appender.MessageFormattingStrategy;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义 p6spy sql输出格式
*/
@Slf4j
public class MpCustomP6SpyLogger implements MessageFormattingStrategy {
/**
* 重置颜色
*/
private static final String RESET = "\u001B[0m";
/**
* 红色
*/
private static final String RED = "\u001B[31m";
/**
* 绿色
*/
private static final String GREEN = "\u001B[32m";
/**
* 蓝色
*/
private static final String BLUE = "\u001B[34m";
/**
* 格式化 sql
*
* @param connectionId 连接id
* @param now 当前时间
* @param elapsed 执行时长
* @param category sql分类
* @param prepared 预编译sql
* @param sql 执行sql
* @param url 数据库连接url
* @return 格式化后的sql
*/
@Override
public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared,
String sql, String url) {
// 去掉换行和多余空格
if (StrUtil.isNotBlank(sql)) {
sql = sql.replaceAll("\\s+", " ").trim();
}
// 格式化并加上颜色
return String.format("CuteJava - %s%s%s %s[%s] %sREVIEW_SQL [Interval: %dms]%s -> %s;", RED, now, RESET, GREEN,
Thread.currentThread().getName(), BLUE, elapsed, RESET, sql);
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.config;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.extension.injector.methods.AlwaysUpdateSomeColumnById;
import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;
import java.util.List;
import org.apache.ibatis.session.Configuration;
/**
* 批量插入SQL注入器
*
* @author odboy
* @date 2025-01-11
*/
public class MpInsertBatchSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(configuration, mapperClass, tableInfo);
// 新增自定义方法, InsertBatchSomeColumn这个方法Mp没有开放出来
methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));
// 全量更新, 包括null字段
methodList.add(new AlwaysUpdateSomeColumnById(i -> i.getFieldFill() != FieldFill.INSERT));
return methodList;
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// MySQL的分页拦截器
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInnerInterceptor.setMaxLimit(500L);
/// 这个配置会导致:当分页没数据时, 游标从0开始,会导致无论page为什么值,就算分页超出最大页数,依然能够查询到
/// paginationInnerInterceptor.setOverflow(true);
paginationInnerInterceptor.setOptimizeJoin(true);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
// 乐观锁
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 防全表更新与删除
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
/**
* Mapper支持批量插入
*/
@Bean
public MpInsertBatchSqlInjector insertBatchSqlInjector() {
return new MpInsertBatchSqlInjector();
}
}
@@ -0,0 +1,213 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.core;
import cn.hutool.core.util.StrUtil;
import cn.odboy.base.KitBaseUserTimeTb;
import cn.odboy.framework.exception.BadRequestException;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.IDbQuery;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.querys.MySqlQuery;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.baomidou.mybatisplus.generator.keywords.MySqlKeyWordsHandler;
import java.sql.Types;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import lombok.Data;
import org.apache.ibatis.annotations.Mapper;
/**
* 代码生成器入口
*
* @author odboy
* @date 2025-01-11
*/
@Data
public class KitMpCmdGenUtil {
// 项目根路径。生成结果 C:\Users\Administrator\Documents\GitHub\cutejava\cutejava
private static final String projectRootPath = System.getProperty("user.dir") + "/";
/**
* 数据库类型 MySQL
*/
private static final IDbQuery DB_QUERY = new MySqlQuery();
/**
* 类型转换
*/
private static final String TYPE_TINY_INT = "tinyint";
private static final String TYPE_DATETIME = "datetime";
private static final String TYPE_DATE = "date";
private String databaseUrl;
private String databaseUsername;
private String databasePassword;
private String driverClassName;
private String parentPackageName = "cn.odboy";
private String parentModuleName;
private static Consumer<StrategyConfig.Builder> getEntityConfigConsumer() {
return strategyConfigBuilder -> strategyConfigBuilder.entityBuilder().enableLombok()
// 如果不需要生成注解, 去掉.enableTableFieldAnnotation()
.enableTableFieldAnnotation().enableFileOverride().naming(NamingStrategy.underline_to_camel)
.columnNaming(NamingStrategy.underline_to_camel).idType(IdType.AUTO);
}
private static Consumer<StrategyConfig.Builder> getStrategyConfigConsumer(List<String> tableNames) {
return strategyConfigBuilder -> strategyConfigBuilder.enableCapitalMode().enableSkipView().disableSqlFilter()
.addInclude(tableNames)
// 实体类生成策略
.entityBuilder().superClass(KitBaseUserTimeTb.class)
.addIgnoreColumns("create_time", "update_time", "create_by", "update_by")
// 开启生成实体时生成字段注解。
// 会在实体类的属性前,添加[@TableField("nickname")]
.enableTableFieldAnnotation().disableSerialVersionUID()
// 阶段2:Mapper策略配置
.mapperBuilder()
// 开启 @Mapper 注解。
// 会在mapper接口上添加注解[@Mapper]
// .enableMapperAnnotation()
// 启用 BaseResultMap 生成。
// 会在mapper.xml文件生成[通用查询映射结果]配置。
// .enableBaseResultMap()
// 启用 BaseColumnList。
// 会在mapper.xml文件生成[通用查询结果列 ]配置
// .enableBaseColumnList()
// 阶段4:Controller策略配置
.controllerBuilder()
// 会在控制类中加[@RestController]注解。
.enableRestStyle()
// 开启驼峰转连字符
.enableHyphenStyle().build();
}
private static Consumer<GlobalConfig.Builder> getGlobalConfigConsumer() {
String outputDir = projectRootPath + "CodeGen";
return globalConfigBuilder -> globalConfigBuilder.enableSwagger().outputDir(outputDir).author("codegen")
.commentDate("yyyy-MM-dd").dateType(DateType.TIME_PACK);
}
private static Consumer<StrategyConfig.Builder> getStrategyConfigConsumer() {
return strategyConfigBuilder -> strategyConfigBuilder.mapperBuilder().mapperAnnotation(Mapper.class)
.formatMapperFileName("%sMapper").formatXmlFileName("%sMapper").entityBuilder().formatFileName("%sTb");
}
private static Consumer<DataSourceConfig.Builder> getDataSourceConfigConsumer() {
return builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
int typeCode = metaInfo.getJdbcType().TYPE_CODE;
if (typeCode == Types.SMALLINT) {
return DbColumnType.INTEGER;
}
if (typeCode == Types.DATE) {
return DbColumnType.DATE;
}
if (typeCode == Types.TIME) {
return DbColumnType.DATE;
}
if (typeCode == Types.TIMESTAMP) {
return DbColumnType.DATE;
}
return typeRegistry.getColumnType(metaInfo);
});
}
/**
* 代码生成函数
*
* @param moduleName 模块名称
* @param tablePrefix 表前缀(可空)
* @param tableNames 表名称, 多个用逗号分隔
*/
public void gen(String moduleName, String tablePrefix, List<String> tableNames) {
this.parentModuleName = moduleName;
// 1.数据库配置
DataSourceConfig.Builder dataSourceConfigBuilder =
new DataSourceConfig.Builder(this.databaseUrl, this.databaseUsername,
this.databasePassword).driverClassName(this.driverClassName);
dataSourceConfigBuilder.dbQuery(DB_QUERY);
dataSourceConfigBuilder.keyWordsHandler(new MySqlKeyWordsHandler());
// 1.1.快速生成器
FastAutoGenerator fastAutoGenerator = FastAutoGenerator.create(dataSourceConfigBuilder);
// 2.全局配置
// 自定义类型转换
fastAutoGenerator.dataSourceConfig(getDataSourceConfigConsumer());
// 覆盖已生成文件
// 添加swagger注解
// 设置注释的作者
// 设置注释的日期格式
// 使用java8新的时间类型
fastAutoGenerator.globalConfig(getGlobalConfigConsumer());
// 3.包配置
// 设置父包名
// 设置父包模块名
// 设置MVC下各个模块的包名
// 设置XML资源文件的目录
fastAutoGenerator.packageConfig(getPackageConfigConsumer());
// 4.模板配置
// 使用Freemarker引擎模板, 默认的是Velocity引擎模板
AbstractTemplateEngine templateEngine = new FreemarkerTemplateEngine();
fastAutoGenerator.templateEngine(templateEngine);
// 5.策略配置
// 设置需要生成的表名
// 设置过滤表前缀
fastAutoGenerator.strategyConfig(getStrategyConfigConsumer(tableNames));
if (tablePrefix != null) {
// 表前缀, 可多个
fastAutoGenerator.strategyConfig(
strategyConfigBuilder -> strategyConfigBuilder.addTablePrefix(tablePrefix));
}
// 5.1.Entity策略配置
// 生成实体时生成字段的注解, 包括@TableId注解等
// 数据库表和字段映射到实体的命名策略, 为下划线转驼峰
// 全局主键类型为None
// 实体名称格式化为XXXEntity
fastAutoGenerator.strategyConfig(getEntityConfigConsumer());
// 5.2.Controller策略配置
// 开启生成@RestController控制器
fastAutoGenerator.strategyConfig(
strategyConfigBuilder -> strategyConfigBuilder.controllerBuilder().enableRestStyle());
// 如果不需要生成Controller, fastAutoGenerator.templateConfig(templateConfig -> templateConfig.controller(""))。5.3
// .Service策略配置:格式化service接口和实现类的文件名称, 去掉默认的ServiceName前面的I
fastAutoGenerator.strategyConfig(
strategyConfigBuilder -> strategyConfigBuilder.serviceBuilder().formatServiceFileName("%sService")
.formatServiceImplFileName("%sServiceImpl"));
// 5.4.Mapper策略配置
// 格式化 mapper文件名,格式化xml实现类文件名称
fastAutoGenerator.strategyConfig(getStrategyConfigConsumer());
// 6.生成代码
fastAutoGenerator.execute();
}
private Consumer<PackageConfig.Builder> getPackageConfigConsumer() {
String mapperPath = projectRootPath + "CodeGen/resources/mapper";
if (StrUtil.isBlank(this.parentModuleName)) {
throw new BadRequestException("模块名称必填");
}
return packageConfigBuilder -> packageConfigBuilder.parent(this.parentPackageName)
.moduleName(this.parentModuleName).entity("dal.dataobject").mapper("dal.mysql").service("service")
.serviceImpl("service.impl").controller("controller")
.pathInfo(Collections.singletonMap(OutputFile.xml, mapperPath));
}
}
@@ -0,0 +1,232 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.odboy.base.KitBaseUserTimeTb;
import cn.odboy.framework.exception.BadRequestException;
import cn.odboy.framework.mybatisplus.config.KitMpQuery;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* 查询条件创建工具
*/
@Slf4j
public class KitMpQUtil {
public static <R, Q> QueryWrapper<R> build(Q query) {
QueryWrapper<R> queryWrapper = new QueryWrapper<>();
if (query == null) {
return queryWrapper;
}
Class<?> clazz = query.getClass();
try {
List<Field> fields = getAllFields(clazz, new ArrayList<>());
for (Field field : fields) {
if (!field.canAccess(query)) {
field.setAccessible(true);
KitMpQuery q = field.getAnnotation(KitMpQuery.class);
if (q != null) {
String attributeName = getAttributeName(q, field);
Object fieldVal = field.get(query);
if (ObjectUtil.isNull(fieldVal) || "".equals(fieldVal)) {
continue;
}
if (StrUtil.isNotBlank(q.blurry())) {
handleBlurryQuery(queryWrapper, q.blurry(), fieldVal);
continue;
}
handleWrapper(q, queryWrapper, attributeName, fieldVal);
}
field.setAccessible(false);
}
}
} catch (Exception e) {
log.error("Failed to build query wrapper for class: {}", clazz.getName(), e);
}
return queryWrapper;
}
private static String getAttributeName(KitMpQuery q, Field field) {
String attributeName = StrUtil.isBlank(q.propName()) ? field.getName() : q.propName();
return StrUtil.toUnderlineCase(attributeName);
}
private static <R> void handleWrapper(KitMpQuery q, QueryWrapper<R> queryWrapper, String attributeName,
Object fieldVal) {
switch (q.type()) {
case EQUAL:
queryWrapper.eq(attributeName, fieldVal);
break;
case GREATER_THAN:
queryWrapper.ge(attributeName, fieldVal);
break;
case LESS_THAN:
queryWrapper.le(attributeName, fieldVal);
break;
case LESS_THAN_NQ:
queryWrapper.lt(attributeName, fieldVal);
break;
case INNER_LIKE:
queryWrapper.like(attributeName, fieldVal);
break;
case LEFT_LIKE:
queryWrapper.likeLeft(attributeName, fieldVal);
break;
case RIGHT_LIKE:
queryWrapper.likeRight(attributeName, fieldVal);
break;
case IN:
handleInOrNotQuery(true, queryWrapper, attributeName, fieldVal);
break;
case NOT_IN:
handleInOrNotQuery(false, queryWrapper, attributeName, fieldVal);
break;
case NOT_EQUAL:
queryWrapper.ne(attributeName, fieldVal);
break;
case NOT_NULL:
queryWrapper.isNotNull(attributeName);
break;
case IS_NULL:
queryWrapper.isNull(attributeName);
break;
case BETWEEN:
handleBetweenQuery(queryWrapper, fieldVal, attributeName);
break;
default:
break;
}
}
private static <R> void handleInOrNotQuery(boolean b, QueryWrapper<R> queryWrapper, String attributeName,
Object fieldVal) {
Collection<?> wrapNotInVal = (Collection<?>) fieldVal;
if (CollUtil.isNotEmpty(wrapNotInVal)) {
Optional<?> anyValOptional = wrapNotInVal.stream().findAny();
if (anyValOptional.isPresent()) {
Object o = anyValOptional.get();
if (o instanceof Long) {
if (b) {
queryWrapper.in(attributeName, fieldVal);
} else {
queryWrapper.notIn(attributeName, fieldVal);
}
} else if (o instanceof Integer) {
if (b) {
queryWrapper.in(attributeName, fieldVal);
} else {
queryWrapper.notIn(attributeName, fieldVal);
}
} else {
throw new BadRequestException("InOrNotIn查询参数类型的对象属性值必须是Long/Integer类型集合");
}
}
}
}
private static <R> void handleBetweenQuery(QueryWrapper<R> queryWrapper, Object fieldVal,
String finalAttributeName) {
if (fieldVal instanceof List) {
List<Object> between = new ArrayList<>((List<?>) fieldVal);
int minLength = 2;
if (CollUtil.isNotEmpty(between) && between.size() >= minLength) {
queryWrapper.between(finalAttributeName, between.get(0), between.get(1));
} else {
throw new BadRequestException("BETWEEN类型的对象列表长度必须 >= 2");
}
} else {
throw new BadRequestException("BETWEEN类型的对象必须是一个List子集合");
}
}
/**
* 模糊多字段
*
* @param queryWrapper /
* @param blurry /
* @param fieldVal /
* @param <R> /
*/
private static <R> void handleBlurryQuery(QueryWrapper<R> queryWrapper, String blurry, Object fieldVal) {
List<String> blurryList =
Arrays.stream(blurry.split(",")).filter(StrUtil::isNotBlank).distinct().toList();
queryWrapper.and(wrapper -> {
for (String blurryItem : blurryList) {
String column = StrUtil.toUnderlineCase(blurryItem);
wrapper.or();
wrapper.like(column, fieldVal.toString());
}
});
}
public static List<Field> getAllFields(Class<?> clazz, List<Field> fields) {
if (clazz != null) {
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
getAllFields(clazz.getSuperclass(), fields);
}
return fields;
}
public static void main(String[] args) {
QueryWrapper<TestDomain> query = new QueryWrapper<>();
query.or(wrapper -> wrapper.eq("username", 1).or().eq("nickname", 2));
query.eq("id", 1);
query.orderByDesc("id");
System.err.println("getSqlSelect=================================");
System.err.println(query.getSqlSelect());
System.err.println("getSqlSegment=================================");
System.err.println(query.getSqlSegment());
System.err.println("getSqlComment=================================");
System.err.println(query.getSqlComment());
System.err.println("getSqlSet=================================");
System.err.println(query.getSqlSet());
System.err.println("getTargetSql=================================");
System.err.println(query.getTargetSql());
}
@Data
@TableName("test_domain")
private static class TestDomain {
@NotNull(groups = KitBaseUserTimeTb.Update.class)
@TableId(value = "user_id", type = IdType.AUTO)
private Long id;
@TableField(value = "dept_id")
private Long deptId;
@NotBlank
private String username;
@NotBlank
private String nickName;
}
}
@@ -0,0 +1,124 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.core.interfaces;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.odboy.base.KitPageResult;
import cn.odboy.util.KitBeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 公共抽象Mapper接口类
*
* @author odboy
* @date 2022-04-03
*/
public interface KitMpMapper<E> extends BaseMapper<E> {
/**
* 批量插入(已测试)
*
* @param entityList List<E>
* @return 影响的行数
*/
int insertBatchSomeColumn(List<E> entityList);
/**
* 根据id全量更新,为空的字段也会被更新(已测试)
*
* @param entity E
* @return 影响的行数
*/
int alwaysUpdateSomeColumnById(@Param("et") E entity);
/**
* 获取字典(已测试)
*
* @param wrapper LambdaQueryWrapper<E>
* @param keyName 键对应的列名称(下划线会自动转驼峰)
* @param valueName 值对应的列名称
* @return Map<String, Object>
*/
default Map<String, Object> getMapWithKv(LambdaQueryWrapper<E> wrapper, String keyName, String valueName) {
Map<String, Object> result = new HashMap<>(1);
List<E> list = this.selectList(wrapper);
if (CollUtil.isEmpty(list)) {
return new HashMap<>(1);
}
E tempObj = list.stream().findFirst().orElse(null);
if (tempObj == null) {
return new HashMap<>(1);
}
Field[] fields = ReflectUtil.getFields(tempObj.getClass());
Field keyField = null;
Field valueField = null;
for (Field field : fields) {
String fieldName = field.getName();
if (fieldName.equals(keyName)) {
keyField = field;
} else if (fieldName.equals(valueName)) {
valueField = field;
break;
}
}
for (E e : list) {
String fieldKey = String.valueOf(ReflectUtil.getFieldValue(e, keyField));
if (fieldKey != null) {
// 下划线转驼峰
String camelFieldKey = StrUtil.toCamelCase(fieldKey);
Object fieldValue = ReflectUtil.getFieldValue(e, valueField);
result.putIfAbsent(camelFieldKey, fieldValue);
}
}
return result;
}
/**
* 条件查询列表, 并返回期望的对象(已测试)
*
* @param wrapper LambdaQueryWrapper<E>
* @param clazz 期望的对象类型
* @return List<T>
*/
default <T> List<T> queryFeatureClazz(LambdaQueryWrapper<E> wrapper, Class<T> clazz) {
List<E> list = this.selectList(wrapper);
return KitBeanUtil.copyToList(list, clazz);
}
/**
* 条件查询分页列表, 并返回期望的对象(已测试)
*
* @param wrapper LambdaQueryWrapper<E> wrapper
* @param clazz 期望的对象类型
* @param pageable 分页参数
* @return IPage<T>
*/
default <T> KitPageResult<T> searchFeatureClazz(LambdaQueryWrapper<E> wrapper, Class<T> clazz, Page<E> pageable) {
IPage<E> pageInfo = this.selectPage(pageable, wrapper);
return new KitPageResult<>(KitBeanUtil.copyToList(pageInfo.getRecords(), clazz), pageInfo.getTotal());
}
}
@@ -0,0 +1,139 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.core.interfaces;
import cn.odboy.base.KitPageResult;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.service.IService;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
* 公共抽象Service接口类
*
* @author odboy
* @date 2022-04-03
*/
public interface KitMpService<T> extends IService<T> {
/**
* 保存数据(已测试)
*
* @param resources 非Tb对象
* @return 影响的行数
*/
<G> int saveFeatureClazz(G resources);
/**
* 批量保存数据(已测试)
*
* @param resources 非Tb对象集合
* @return 影响的行数
*/
<G> int batchSaveFeatureClazz(List<G> resources);
/**
* 根据id更新数据(已测试)
*
* @param resources 非Tb对象
* @return 影响的行数
*/
<G> int updateFeatureClazzById(G resources);
/**
* 根据id更新数据(已测试)
*
* @param resources 非Tb对象
* @return 影响的行数
*/
<G> boolean batchUpdateFeatureClazzById(Collection<G> resources, int batchSize);
/**
* 通过id查询(已测试)
*
* @param id 数据id
* @param targetClazz 目标类型
*/
<G> G getFeatureClazzById(Serializable id, Class<G> targetClazz);
/**
* 根据条件查询一条数据(已测试)
*
* @param wrapper 查询条件
* @param orderColumn 排序字段
* @return /
*/
T getClazzByArgs(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> orderColumn);
/**
* 根据条件查询一条目标类型的数据(已测试)
*
* @param wrapper 查询条件
* @param orderColumn 排序字段
* @param targetClazz 目标类型
* @return /
*/
<G> G getFeatureClazzByArgs(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> orderColumn, Class<G> targetClazz);
/**
* 根据id集合查询目标数据(已测试)
*
* @param ids id集合
* @param targetClazz 目标类型
* @return /
*/
<G> List<G> listFeatureClazzByIds(List<Serializable> ids, Class<G> targetClazz);
/**
* 根据条件查询目标数据(已测试)
*
* @param wrapper 查询条件
* @param targetClazz 目标类型
* @return /
*/
<G> List<G> queryFeatureClazzByArgs(LambdaQueryWrapper<T> wrapper, Class<G> targetClazz);
/**
* 根据条件分页查询数据(已测试)
*
* @param wrapper 查询条件
* @param pageable 分页参数
* @return /
*/
KitPageResult<T> searchClazzByArgs(LambdaQueryWrapper<T> wrapper, IPage<T> pageable);
/**
* 根据条件分页查询目标数据(已测试)
*
* @param wrapper 查询条件
* @param targetClazz 目标类型
* @param pageable 分页参数
* @return /
*/
<G> KitPageResult<G> searchFeatureClazzByArgs(LambdaQueryWrapper<T> wrapper, Class<G> targetClazz, IPage<T> pageable);
/**
* 根据条件更新数据(已测试)
*
* @param wrapper 查询条件与值
* @return 影响的行数
*/
int updateClazzByArgs(LambdaQueryWrapper<T> wrapper);
}
@@ -0,0 +1,118 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.mybatisplus.core.interfaces;
import cn.odboy.base.KitPageResult;
import cn.odboy.util.KitBeanUtil;
import cn.odboy.util.KitValidUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
* 公共抽象Mapper接口类
*
* @author odboy
* @date 2022-04-03
*/
public class KitMpServiceImpl<M extends KitMpMapper<T>, T> extends ServiceImpl<M, T> {
public <G> int saveFeatureClazz(G resources) {
return getBaseMapper().insert(KitBeanUtil.copyToClass(resources, this.getEntityClass()));
}
public <G> int batchSaveFeatureClazz(List<G> resources) {
return getBaseMapper().insert(KitBeanUtil.copyToList(resources, this.getEntityClass())).size();
}
public <G> int updateFeatureClazzById(G resources) {
return getBaseMapper().updateById(KitBeanUtil.copyToClass(resources, this.getEntityClass()));
}
public <G> int batchUpdateFeatureClazzById(Collection<G> resources, int batchSize) {
return getBaseMapper().updateById(KitBeanUtil.copyToList(resources, this.getEntityClass()), batchSize).size();
// String sqlStatement = this.getSqlStatement(SqlMethod.UPDATE_BY_ID);
// return this.executeBatch(
// resources, batchSize, (sqlSession, entity) -> {
// MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
// param.put("et", KitBeanUtil.copyToClass(entity, this.getEntityClass()));
// sqlSession.update(sqlStatement, param);
// }
// );
}
public <G> G getFeatureClazzById(Serializable id, Class<G> targetClazz) {
return KitBeanUtil.copyToClass(this.getById(id), targetClazz);
}
public T getClazzByArgs(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> orderColumn) {
KitValidUtil.isNull(orderColumn, "排序列", "orderColumn");
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
wrapper.orderByDesc(orderColumn);
wrapper.last("limit 1");
return baseMapper.selectOne(wrapper);
}
public <G> G getFeatureClazzByArgs(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> orderColumn, Class<G> targetClazz) {
KitValidUtil.isNull(orderColumn, "排序列", "orderColumn");
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
wrapper.orderByDesc(orderColumn);
wrapper.last("limit 1");
return KitBeanUtil.copyToClass(baseMapper.selectOne(wrapper), targetClazz);
}
public <G> List<G> listFeatureClazzByIds(List<Serializable> ids, Class<G> targetClazz) {
return KitBeanUtil.copyToList(this.listByIds(ids), targetClazz);
}
public <G> List<G> queryFeatureClazzByArgs(LambdaQueryWrapper<T> wrapper, Class<G> targetClazz) {
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
return KitBeanUtil.copyToList(baseMapper.selectList(wrapper), targetClazz);
}
public KitPageResult<T> searchClazzByArgs(LambdaQueryWrapper<T> wrapper, IPage<T> pageable) {
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
IPage<T> originPageData = baseMapper.selectPage(pageable, wrapper);
return new KitPageResult<>(originPageData.getRecords(), originPageData.getTotal());
}
public <G> KitPageResult<G> searchFeatureClazzByArgs(LambdaQueryWrapper<T> wrapper, Class<G> targetClazz, IPage<T> pageable) {
if (wrapper == null) {
wrapper = new LambdaQueryWrapper<>();
}
IPage<T> originPageData = baseMapper.selectPage(pageable, wrapper);
return new KitPageResult<>(KitBeanUtil.copyToList(originPageData.getRecords(), targetClazz), originPageData.getTotal());
}
public int updateClazzByArgs(LambdaUpdateWrapper<T> wrapper) {
KitValidUtil.isNull(wrapper, "查询条件与值", "wrapper");
return baseMapper.update(null, wrapper);
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.properties;
import cn.odboy.framework.properties.model.CaptchaModel;
import cn.odboy.framework.properties.model.ContentRsaEncodeSettingModel;
import cn.odboy.framework.properties.model.JwtAuthSettingModel;
import cn.odboy.framework.properties.model.StorageOSSModel;
import cn.odboy.framework.properties.model.SwaggerApiDocSettingModel;
import cn.odboy.framework.properties.model.ThreadPoolSettingModel;
import cn.odboy.framework.properties.model.UserLoginSettingModel;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 应用配置
*
* @author odboy
* @date 2025-04-13
*/
@Data
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private ContentRsaEncodeSettingModel rsa;
private JwtAuthSettingModel jwt;
private UserLoginSettingModel login;
private SwaggerApiDocSettingModel swagger;
private ThreadPoolSettingModel asyncTaskPool;
private CaptchaModel captcha;
private StorageOSSModel oss;
/**
* 自动识别json对象白名单配置(仅允许解析的包名, 范围越小越安全)<br/> 未配置可能导致, 登录失败, 反复登录等问题
*/
private List<String> serialWhiteList;
}
@@ -0,0 +1,30 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.properties.model;
import cn.odboy.base.KitObject;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CaptchaModel extends KitObject {
/**
* 验证码有效时间, 单位: 秒
*/
private Long expireTime;
}
@@ -0,0 +1,30 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.properties.model;
import cn.odboy.base.KitObject;
import lombok.Getter;
import lombok.Setter;
/**
* 密码加密传输, 前端公钥加密,后端私钥解密
*/
@Getter
@Setter
public class ContentRsaEncodeSettingModel extends KitObject {
private String privateKey;
}
@@ -0,0 +1,45 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.properties.model;
import cn.odboy.base.KitObject;
import lombok.Getter;
import lombok.Setter;
/**
* Jwt参数配置
*/
@Getter
@Setter
public class JwtAuthSettingModel extends KitObject {
/**
* 必须使用最少88位的Base64对该令牌进行编码
*/
private String base64Secret;
/**
* 令牌过期时间 此处单位/毫秒
*/
private Long tokenValidityInSeconds;
/**
* token 续期检查
*/
private Long detect;
/**
* 续期时间
*/
private Long renew;
}
@@ -0,0 +1,42 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.properties.model;
import cn.odboy.base.KitObject;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class OSSConfigModel extends KitObject {
/**
* 服务地址
*/
private String endpoint;
/**
* 存储桶名称
*/
private String bucketName;
/**
* Access Key
*/
private String accessKey;
/**
* Secret Key
*/
private String secretKey;
}
@@ -0,0 +1,30 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.properties.model;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class StorageOSSModel {
/**
* 上传大小, 单位: M
*/
private Long maxSize;
private OSSConfigModel minio;
}
@@ -0,0 +1,31 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.properties.model;
import cn.odboy.base.KitObject;
import lombok.Getter;
import lombok.Setter;
/**
* @author odboy
* @date 2025-04-13
*/
@Getter
@Setter
public class SwaggerApiDocSettingModel extends KitObject {
private Boolean enabled;
}
@@ -0,0 +1,48 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.properties.model;
import cn.odboy.base.KitObject;
import lombok.Getter;
import lombok.Setter;
/**
* 线程池 配置
*
* @author odboy
* @date 2025-04-13
*/
@Getter
@Setter
public class ThreadPoolSettingModel extends KitObject {
/**
* 核心线程池大小
*/
private int corePoolSize;
/**
* 最大线程数
*/
private int maxPoolSize;
/**
* 活跃时间
*/
private int keepAliveSeconds;
/**
* 队列容量
*/
private int queueCapacity;
}
@@ -0,0 +1,126 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.properties.model;
import cn.hutool.core.util.StrUtil;
import cn.odboy.base.KitObject;
import cn.odboy.constant.CaptchaCodeEnum;
import cn.odboy.framework.exception.BadRequestException;
import com.wf.captcha.ArithmeticCaptcha;
import com.wf.captcha.ChineseCaptcha;
import com.wf.captcha.ChineseGifCaptcha;
import com.wf.captcha.GifCaptcha;
import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha;
import lombok.Getter;
import lombok.Setter;
import java.awt.*;
/**
* 验证码配置
*
* @author odboy
*/
@Getter
@Setter
public class UserLoginCaptchaSettingModel extends KitObject {
private CaptchaCodeEnum codeType;
/**
* 验证码有效期 分钟
*/
private Long expiration = 5L;
/**
* 验证码内容长度
*/
private int length = 4;
/**
* 验证码宽度
*/
private int width = 111;
/**
* 验证码高度
*/
private int height = 36;
/**
* 验证码字体
*/
private String fontName;
/**
* 字体大小
*/
private int fontSize = 25;
/**
* 依据配置信息生产验证码
*
* @return /
*/
public Captcha getCaptcha() {
Captcha captcha;
switch (codeType) {
case ARITHMETIC:
// 算术类型 https://gitee.com/whvse/EasyCaptcha
captcha = new FixedArithmeticCaptcha(width, height);
// 几位数运算, 默认是两位
captcha.setLen(length);
break;
case CHINESE:
captcha = new ChineseCaptcha(width, height);
captcha.setLen(length);
break;
case CHINESE_GIF:
captcha = new ChineseGifCaptcha(width, height);
captcha.setLen(length);
break;
case GIF:
captcha = new GifCaptcha(width, height);
captcha.setLen(length);
break;
case SPEC:
captcha = new SpecCaptcha(width, height);
captcha.setLen(length);
break;
default:
throw new BadRequestException("验证码配置信息错误!正确配置查看 LoginCodeEnum ");
}
if (StrUtil.isNotBlank(fontName)) {
captcha.setFont(new Font(fontName, Font.PLAIN, fontSize));
}
return captcha;
}
static class FixedArithmeticCaptcha extends ArithmeticCaptcha {
public FixedArithmeticCaptcha(int width, int height) {
super(width, height);
}
@Override
protected char[] alphas() {
// 生成随机数字和运算符
int n1 = num(1, 10), n2 = num(1, 10);
int opt = num(3);
// 计算结果
int res = new int[]{n1 + n2, n1 - n2, n1 * n2}[opt];
// 转换为字符运算符
char optChar = "+-x".charAt(opt);
this.setArithmeticString(String.format("%s%c%s=?", n1, optChar, n2));
this.chars = String.valueOf(res);
return chars.toCharArray();
}
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.properties.model;
import cn.odboy.base.KitObject;
import lombok.Getter;
import lombok.Setter;
/**
* 登录配置
*
* @author odboy
*/
@Getter
@Setter
public class UserLoginSettingModel extends KitObject {
/**
* 账号单用户 登录
*/
private boolean single = false;
private UserLoginCaptchaSettingModel captchaSetting;
}
@@ -0,0 +1,64 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.quartz.config;
import jakarta.annotation.Nonnull;
import lombok.extern.slf4j.Slf4j;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
/**
* 定时任务配置
*/
@Slf4j
@Configuration
@Scope("singleton")
public class QuartzConfig {
/**
* 解决Job中注入Spring Bean为null的问题
*/
@Component("quartzJobFactory")
public static class QuartzJobFactory extends AdaptableJobFactory {
private final AutowireCapableBeanFactory autowireCapableBeanFactory;
@Autowired
public QuartzJobFactory(AutowireCapableBeanFactory autowireCapableBeanFactory) {
this.autowireCapableBeanFactory = autowireCapableBeanFactory;
}
@Nonnull
@Override
protected Object createJobInstance(@Nonnull TriggerFiredBundle triggerFiredBundle) throws Exception {
try {
// 调用父类的方法, 把Job注入Spring中
Object jobInstance = super.createJobInstance(triggerFiredBundle);
autowireCapableBeanFactory.autowireBean(jobInstance);
return jobInstance;
} catch (Exception e) {
log.error("Job注入Spring失败, {}", triggerFiredBundle, e);
throw e;
}
}
}
}
@@ -0,0 +1,59 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.quartz.core;
import cn.odboy.framework.context.KitSpringBeanHolder;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ReflectionUtils;
/**
* 带返回值的线程
*
* @author odboy
*/
@Slf4j
public class QuartzRunnable implements Callable<Object> {
private final Object target;
private final Method method;
private final String params;
public QuartzRunnable(String beanName, String methodName, String params)
throws NoSuchMethodException, SecurityException {
this.target = KitSpringBeanHolder.getBean(beanName);
this.params = params;
if (StringUtils.isNotBlank(params)) {
this.method = target.getClass().getDeclaredMethod(methodName, String.class);
} else {
this.method = target.getClass().getDeclaredMethod(methodName);
}
}
@Override
public Object call() throws Exception {
ReflectionUtils.makeAccessible(method);
if (StringUtils.isNotBlank(params)) {
return method.invoke(target, params);
} else {
return method.invoke(target, null);
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 消息订阅Demo
*
* @author odboy
*/
@Slf4j
@Component
public class DemoMessageSubscriber {
public void onMessage(String message, String channel) {
log.info("收到来自redis消息队列的message={}, channel={}", message, channel);
}
}
@@ -0,0 +1,881 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.redis;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.odboy.framework.exception.BadRequestException;
import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.ConvertingCursor;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
@Component
public class KitRedisHelper {
private static final Logger log = LoggerFactory.getLogger(KitRedisHelper.class);
private final RedisTemplate<Object, Object> redisTemplate;
public KitRedisHelper(RedisTemplate<Object, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.redisTemplate.setKeySerializer(new StringRedisSerializer());
this.redisTemplate.setHashKeySerializer(new StringRedisSerializer());
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒) 注意:这里将会替换原有的时间
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒) 注意:这里将会替换原有的时间
* @param timeUnit 单位
*/
public boolean expire(String key, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
redisTemplate.expire(key, time, timeUnit);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
/**
* 根据 key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public Long getExpire(Object key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 查找匹配key
*
* @param pattern key
* @return /
*/
@SuppressWarnings("all")
public List<String> scan(String pattern) {
ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
List<String> result;
try (Cursor<byte[]> cursor = rc.scan(options)) {
result = new ArrayList<>();
while (cursor.hasNext()) {
result.add(new String(cursor.next()));
}
}
try {
RedisConnectionUtils.releaseConnection(rc, factory);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return result;
}
/**
* 分页查询 key
*
* @param patternKey key
* @param page 页码
* @param size 每页数目
* @return /
*/
@SuppressWarnings("all")
public List<String> findKeysForPage(String patternKey, int page, int size) {
ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor<byte[]> cursor = rc.scan(options);
List<String> result = new ArrayList<>(size);
int tmpIndex = 0;
int fromIndex = page * size;
int toIndex = page * size + size;
while (cursor.hasNext()) {
if (tmpIndex >= fromIndex && tmpIndex < toIndex) {
result.add(new String(cursor.next()));
tmpIndex++;
continue;
}
// 获取到满足条件的数据后,就可以退出了
if (tmpIndex >= toIndex) {
break;
}
tmpIndex++;
cursor.next();
}
try {
RedisConnectionUtils.releaseConnection(rc, factory);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return result;
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 删除缓存
*
* @param keys 可以传一个值 或多个
*/
public void del(String... keys) {
if (keys != null && keys.length > 0) {
if (keys.length == 1) {
boolean result = Boolean.TRUE.equals(redisTemplate.delete(keys[0]));
log.debug("删除缓存:{}, 结果:{}", keys[0], result);
} else {
Set<Object> keySet = new HashSet<>();
for (String key : keys) {
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
keySet.add(key);
}
}
Long count = redisTemplate.delete(keySet);
log.debug("成功删除缓存:{}", keySet);
log.debug("缓存删除数量:{}个", count);
}
}
}
/**
* 批量模糊删除key
*
* @param pattern /
*/
@SuppressWarnings("all")
public void scanDel(String pattern) {
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(100).build();
try (Cursor<byte[]> cursor = redisTemplate.executeWithStickyConnection(connection -> {
try {
return (Cursor<byte[]>) new ConvertingCursor<>(connection.scan(options), redisTemplate.getKeySerializer()::deserialize);
} catch (Exception e) {
throw new RuntimeException("Redis scan operation failed", e);
}
})) {
List<Object> keysToDelete = new ArrayList<>();
while (cursor.hasNext()) {
Object key = cursor.next();
keysToDelete.add(key);
// 批量删除,避免一次性删除过多key
if (keysToDelete.size() >= 100) {
redisTemplate.delete(keysToDelete);
keysToDelete.clear();
}
}
// 删除剩余的key
if (!keysToDelete.isEmpty()) {
redisTemplate.delete(keysToDelete);
}
} catch (Exception e) {
throw new RuntimeException("Error during scan and delete operation", e);
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public <T> T get(String key, Class<T> clazz) {
Object value = key == null ? null : redisTemplate.opsForValue().get(key);
if (value == null) {
return null;
}
// 如果 value 不是目标类型, 则尝试将其反序列化为 clazz 类型
boolean isInstance = clazz.isInstance(value);
if (isInstance) {
return clazz.cast(value);
}
return JSON.parseObject(value.toString(), clazz);
}
/**
* 普通缓存获取
*
* @param key 键
* @param clazz 列表中元素的类型
* @return 值
*/
public <T> List<T> getList(String key, Class<T> clazz) {
Object value = key == null ? null : redisTemplate.opsForValue().get(key);
if (value == null) {
return null;
}
if (value instanceof List<?> list) {
// 检查每个元素是否为指定类型
if (list.stream().allMatch(clazz::isInstance)) {
return list.stream().map(clazz::cast).collect(Collectors.toList());
}
}
return null;
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public String getStr(String key) {
if (StrUtil.isBlank(key)) {
return null;
}
Object value = redisTemplate.opsForValue().get(key);
if (value == null) {
return null;
} else {
return String.valueOf(value);
}
}
/**
* 批量获取
*
* @param keys
* @return
*/
public List<Object> multiGet(List<String> keys) {
List<Object> list = redisTemplate.opsForValue().multiGet(Sets.newHashSet(keys));
List<Object> resultList = Lists.newArrayList();
Optional.ofNullable(list)
.ifPresent(e -> list.forEach(ele -> Optional.ofNullable(ele).ifPresent(resultList::add)));
return resultList;
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
int attempt = 0;
while (attempt < 3) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
attempt++;
log.error("Attempt {} failed: {}", attempt, e.getMessage(), e);
}
}
return false;
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期, 注意:这里将会替换原有的时间
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(time));
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间,注意:这里将会替换原有的时间
* @param timeUnit 类型
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
if (TimeUnit.SECONDS.equals(timeUnit)) {
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(time));
} else if (TimeUnit.MINUTES.equals(timeUnit)) {
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(time));
} else if (TimeUnit.HOURS.equals(timeUnit)) {
redisTemplate.opsForValue().set(key, value, Duration.ofHours(time));
} else {
throw new BadRequestException("不支持的类型");
}
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
// ================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, value));
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
Long add = redisTemplate.opsForSet().add(key, values);
return add == null ? 0L : add;
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0L;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒) 注意:这里将会替换原有的时间
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count == null ? 0 : count;
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
Long size = redisTemplate.opsForSet().size(key);
return size == null ? 0 : size;
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count == null ? 0 : count;
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒) 注意:这里将会替换原有的时间
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒) 注意:这里将会替换原有的时间
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return /
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
return redisTemplate.opsForList().remove(key, count, value);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* @param prefix 前缀
* @param ids id
*/
public void delByKeys(String prefix, Set<Long> ids) {
Set<Object> keys = new HashSet<>();
for (Long id : ids) {
keys.addAll(redisTemplate.keys(prefix + id));
}
long count = redisTemplate.delete(keys);
}
// ============================incr=============================
/**
* 递增
*
* @param key
* @return
*/
public Long increment(String key) {
return redisTemplate.opsForValue().increment(key);
}
/**
* 递减
*
* @param key
* @return
*/
public Long decrement(String key) {
return redisTemplate.opsForValue().decrement(key);
}
// ============================校验=============================
public boolean isHealth() {
try {
Objects.requireNonNull(redisTemplate.getConnectionFactory()).getConnection().ping();
return true;
} catch (Exception e) {
log.error("Redis health check failed: {}", e.getMessage(), e);
return false;
}
}
/**
* 仅当键不存在时设置值(原子操作)
*
* @param key 键
* @param value 值
* @param time 过期时间
* @param timeUnit 时间单位
* @return 如果设置成功返回true,否则返回false
*/
public boolean setIfAbsent(String key, Object value, long time, TimeUnit timeUnit) {
try {
String stringValue = ObjUtil.toString(value);
// 使用Redis的SETNX命令实现原子性操作
Boolean success;
if (TimeUnit.SECONDS.equals(timeUnit)) {
success = redisTemplate.opsForValue().setIfAbsent(key, stringValue, Duration.ofSeconds(time));
} else if (TimeUnit.MINUTES.equals(timeUnit)) {
success = redisTemplate.opsForValue().setIfAbsent(key, stringValue, Duration.ofMinutes(time));
} else if (TimeUnit.HOURS.equals(timeUnit)) {
success = redisTemplate.opsForValue().setIfAbsent(key, stringValue, Duration.ofHours(time));
} else {
throw new BadRequestException("不支持的类型");
}
return success != null && success;
} catch (Exception e) {
log.error("Redis setIfAbsent操作失败, key: {}", key, e);
return false;
}
}
}
@@ -0,0 +1,194 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.redis;
import cn.odboy.framework.properties.AppProperties;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONFactory;
import com.alibaba.fastjson2.JSONWriter;
import jakarta.annotation.Nonnull;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.MurmurHash3;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleCacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Slf4j
@Configuration
@EnableCaching
public class RedisConfiguration {
@Autowired
private AppProperties properties;
/**
* 设置 redis 数据默认过期时间,默认2小时 设置@cacheable 序列化方式
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer))
.entryTtl(Duration.ofHours(2));
return configuration;
}
@Bean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 指定 key 和 value 的序列化方案
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// 设置fastJson的序列化白名单
for (String pack : properties.getSerialWhiteList()) {
JSONFactory.getDefaultObjectReaderProvider().addAutoTypeAccept(pack);
log.warn("设置fastJson的序列化白名单:{}", pack);
}
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
/**
* 缓存管理器
*
* @param redisConnectionFactory /
* @return 缓存管理器
*/
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration config = redisCacheConfiguration();
return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();
}
/**
* 自定义缓存key生成策略
*/
@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
Map<String, Object> container = new HashMap<>(8);
Class<?> targetClassClass = target.getClass();
// 类地址
container.put("class", targetClassClass.toGenericString());
// 方法名称
container.put("methodName", method.getName());
// 包名称
container.put("package", targetClassClass.getPackage());
// 参数列表
for (int i = 0; i < params.length; i++) {
container.put(String.valueOf(i), params[i]);
}
// 转为JSON字符串
String jsonString = JSON.toJSONString(container);
// 使用 MurmurHash 生成 hash
return Integer.toHexString(MurmurHash3.hash32x86(jsonString.getBytes()));
};
}
@Bean
public CacheErrorHandler errorHandler() {
return new SimpleCacheErrorHandler() {
@Override
public void handleCacheGetError(@Nonnull RuntimeException exception, @Nonnull Cache cache,
@Nonnull Object key) {
// 处理缓存读取错误
log.error("Cache Get Error: {}", exception.getMessage());
}
@Override
public void handleCachePutError(@Nonnull RuntimeException exception, @Nonnull Cache cache,
@Nonnull Object key, Object value) {
// 处理缓存写入错误
log.error("Cache Put Error: {}", exception.getMessage());
}
@Override
public void handleCacheEvictError(@Nonnull RuntimeException exception, @Nonnull Cache cache,
@Nonnull Object key) {
// 处理缓存删除错误
log.error("Cache Evict Error: {}", exception.getMessage());
}
@Override
public void handleCacheClearError(@Nonnull RuntimeException exception, @Nonnull Cache cache) {
// 处理缓存清除错误
log.error("Cache Clear Error: {}", exception.getMessage());
}
};
}
/**
* Value 序列化
*
* @param <T>
*/
static class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
private final Class<T> clazz;
FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
/**
* 序列化
*/
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(StandardCharsets.UTF_8);
}
/**
* 反序列化
*/
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length == 0) {
return null;
}
String str = new String(bytes, StandardCharsets.UTF_8);
// 移除对 SupportAutoType 的使用
return JSON.parseObject(str, clazz);
}
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.redis;
import cn.odboy.framework.websocket.WsMessageSubscriber;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
/**
* Redis消息队列配置
*
* @author odboy
*/
@Configuration
public class RedisMessageConfig {
/**
* 消息监听容器
*
* @param connectionFactory /
* @param listenerWsMessageAdapter /
* @return /
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerWsMessageAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 指定监听的主题
container.addMessageListener(listenerWsMessageAdapter, new ChannelTopic("DemoMessage"));
container.addMessageListener(listenerWsMessageAdapter, new ChannelTopic("WebSocketMessage"));
return container;
}
/**
* 将订阅者与消息处理方法绑定
*
* @param subscriber /
* @return /
*/
@Bean
public MessageListenerAdapter listenerDemoMessageAdapter(DemoMessageSubscriber subscriber) {
return new MessageListenerAdapter(subscriber, "onMessage");
}
@Bean
public MessageListenerAdapter listenerWsMessageAdapter(WsMessageSubscriber subscriber) {
return new MessageListenerAdapter(subscriber, "onMessage");
}
}
@@ -0,0 +1,101 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.server.config;
import cn.odboy.framework.properties.AppProperties;
import cn.odboy.framework.properties.model.ThreadPoolSettingModel;
import com.alibaba.ttl.TtlRunnable;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* 创建自定义的线程池
*/
@EnableAsync
@Configuration
public class AsyncTheadPoolConfig implements AsyncConfigurer {
@Autowired
private AppProperties properties;
/**
* 自定义线程池, 用法 @Async
*
* @return Executor
*/
@Override
public Executor getAsyncExecutor() {
ThreadPoolSettingModel asyncTaskPool = properties.getAsyncTaskPool();
// 自定义工厂
ThreadFactory factory = r -> new Thread(r, "DefaultAsync-" + new AtomicInteger(1).getAndIncrement());
// 自定义线程池
return new ThreadPoolExecutor(asyncTaskPool.getCorePoolSize(), asyncTaskPool.getMaxPoolSize(),
asyncTaskPool.getKeepAliveSeconds(), TimeUnit.SECONDS,
new ArrayBlockingQueue<>(asyncTaskPool.getQueueCapacity()), factory,
new ThreadPoolExecutor.CallerRunsPolicy());
}
/**
* 自定义线程池, 用法, 注入到类中使用
*/
@Bean("taskAsync")
public ThreadPoolTaskExecutor taskAsync() {
ThreadPoolSettingModel asyncTaskPool = properties.getAsyncTaskPool();
// 用法 private ThreadPoolTaskExecutor taskExecutor
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(asyncTaskPool.getCorePoolSize());
executor.setMaxPoolSize(asyncTaskPool.getMaxPoolSize());
executor.setQueueCapacity(asyncTaskPool.getQueueCapacity());
executor.setThreadNamePrefix("TaskAsync-");
executor.setKeepAliveSeconds(asyncTaskPool.getKeepAliveSeconds());
// DiscardOldestPolicy,抛弃最早的任务,将新任务加入队列。
// AbortPolicy,拒绝执行新任务,并抛出异常。
// CallerRunsPolicy,交由调用者线程执行新任务,如果调用者线程已关闭,则抛弃任务。
// DiscardPolicy,直接抛弃新任务。
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// 设置包装器
executor.setTaskDecorator(TtlRunnable::get);
executor.initialize();
return executor;
}
@Bean("longTimeTaskAsync")
public ThreadPoolTaskExecutor longTimeTaskAsync() {
ThreadPoolSettingModel asyncTaskPool = properties.getAsyncTaskPool();
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(asyncTaskPool.getCorePoolSize());
executor.setMaxPoolSize(asyncTaskPool.getMaxPoolSize());
executor.setQueueCapacity(asyncTaskPool.getQueueCapacity());
executor.setThreadNamePrefix("LongTimeTaskAsync-");
executor.setKeepAliveSeconds(asyncTaskPool.getKeepAliveSeconds());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// 设置包装器
executor.setTaskDecorator(TtlRunnable::get);
executor.initialize();
return executor;
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.server.converters;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
@Component
public class DateStringArraysToListDateConverter implements Converter<String[], List<Date>> {
private static final ThreadLocal<SimpleDateFormat> SDF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
@Override
public List<Date> convert(String[] source) {
if (source.length == 0) {
return Collections.emptyList();
}
return Arrays.stream(source).map(s -> {
try {
return SDF.get().parse(s);
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid date string: " + s);
}
}).toList();
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.server.converters;
import cn.hutool.core.util.StrUtil;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
@Component
public class DateStringToDateConverter implements Converter<String, Date> {
private static final ThreadLocal<SimpleDateFormat> SDF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
@Override
public Date convert(String source) {
if (StrUtil.isBlank(source)) {
return null;
}
try {
return SDF.get().parse(source);
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid date string: " + source);
}
}
}
@@ -0,0 +1,147 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.server.core;
import cn.odboy.framework.server.converters.DateStringArraysToListDateConverter;
import cn.odboy.framework.server.converters.DateStringToDateConverter;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.support.config.FastJsonConfig;
import com.alibaba.fastjson2.support.spring6.http.converter.FastJsonHttpMessageConverter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverters.ServerBuilder;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨越、静态资源映射、Http消息转换
*
* @author odboy
* @date 2025-03-21
*/
@Configuration
@EnableWebMvc
public class ConfigurerAdapter implements WebMvcConfigurer {
@Autowired
private DateStringArraysToListDateConverter dateStringArraysToListDateConverter;
@Autowired
private DateStringToDateConverter dateStringToDateConverter;
@Autowired
private KitFileLocalUploadHelper fileUploadPathHelper;
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String path = fileUploadPathHelper.getPath();
String pathUtl = "file:" + path.replace("\\", "/");
registry.addResourceHandler("/avatar/**").addResourceLocations(pathUtl).setCachePeriod(0);
registry.addResourceHandler("/file/**").addResourceLocations(pathUtl).setCachePeriod(0);
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/").setCachePeriod(0);
}
// /**
// * Http消息转换器
// */
// @Override
// public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// // 添加默认的 StringHttpMessageConverter
// converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
// // 配置 FastJsonHttpMessageConverter
// FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();
// List<MediaType> supportMediaTypeList = new ArrayList<>();
// supportMediaTypeList.add(MediaType.APPLICATION_JSON);
// FastJsonConfig config = this.getFastJsonConfig();
// fastJsonConverter.setFastJsonConfig(config);
// fastJsonConverter.setSupportedMediaTypes(supportMediaTypeList);
// fastJsonConverter.setDefaultCharset(StandardCharsets.UTF_8);
// // 将 FastJsonHttpMessageConverter 添加到列表末尾
// converters.add(fastJsonConverter);
// }
/**
* Http消息转换器
*/
@Override
public void configureMessageConverters(ServerBuilder builder) {
// 添加默认的 StringHttpMessageConverter
builder.addCustomConverter(new StringHttpMessageConverter(StandardCharsets.UTF_8));
// 配置 FastJsonHttpMessageConverter
FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();
List<MediaType> supportMediaTypeList = new ArrayList<>();
supportMediaTypeList.add(MediaType.APPLICATION_JSON);
FastJsonConfig config = this.getFastJsonConfig();
fastJsonConverter.setFastJsonConfig(config);
fastJsonConverter.setSupportedMediaTypes(supportMediaTypeList);
fastJsonConverter.setDefaultCharset(StandardCharsets.UTF_8);
// 将 FastJsonHttpMessageConverter 添加到列表末尾
builder.addCustomConverter(fastJsonConverter);
WebMvcConfigurer.super.configureMessageConverters(builder);
}
private FastJsonConfig getFastJsonConfig() {
FastJsonConfig config = new FastJsonConfig();
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
// 开启引用检测, 枚举支持
/// 是否输出值为null的字段
// config.setWriterFeatures(JSONWriter.Feature.WriteMapNullValue);
/// 字段如果为null,输出为false,而非null
// config.setWriterFeatures(JSONWriter.Feature.WriteNullBooleanAsFalse);
/// 字段如果为null,输出为[],而非null
// config.setWriterFeatures(JSONWriter.Feature.WriteNullListAsEmpty);
/// 字符类型字段如果为null,输出为"",而非null
// config.setWriterFeatures(JSONWriter.Feature.WriteNullStringAsEmpty);
/// 太具体的数值会直接影响逻辑本身, 所以不要这个
// config.setWriterFeatures(JSONWriter.Feature.WriteNullNumberAsZero);
/// 全局Long转字符串,会导致基础框架本身异常, 所以不要这个
// config.setWriterFeatures(JSONWriter.Feature.WriteLongAsString);
config.setWriterFeatures(JSONWriter.Feature.WriteEnumUsingToString);
config.setWriterFeatures(JSONWriter.Feature.ReferenceDetection);
return config;
}
/**
* 自定义时间转换器
*/
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(dateStringArraysToListDateConverter);
registry.addConverter(dateStringToDateConverter);
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.server.core;
import cn.hutool.core.util.StrUtil;
import cn.odboy.constant.SystemConst;
import cn.odboy.util.KitSystemUtil;
import org.springframework.stereotype.Component;
/**
* 简化文件上传路径配置
*
* @author odboy
*/
@Component
public class KitFileLocalUploadHelper {
/**
* 单位MB
*/
private static final int AVATAR_MAX_SIZE = 5;
private static final String PATH_WINDOWS = "C:\\${appName}\\file\\";
private static final String PATH_MAC = "/Users${username}/${appName}/file/";
private static final String PATH_LINUX = "/home/${appName}/file/";
public String getPath() {
String os = System.getProperty("os.name");
if (os == null) {
// 默认使用 Linux 路径作为兜底方案
return PATH_LINUX.replace("${appName}", SystemConst.CURRENT_APP_NAME);
}
String lowerOs = os.toLowerCase();
if (lowerOs.startsWith(SystemConst.WIN)) {
return PATH_WINDOWS.replace("${appName}", SystemConst.CURRENT_APP_NAME);
} else if (lowerOs.startsWith(SystemConst.MAC)) {
String currentUserName = KitSystemUtil.getCurrentUserName();
if (StrUtil.isBlank(currentUserName)) {
return PATH_MAC.replace("${appName}", SystemConst.CURRENT_APP_NAME).replace("${username}", "");
}
return PATH_MAC.replace("${appName}", SystemConst.CURRENT_APP_NAME).replace("${username}", "/" + currentUserName);
}
return PATH_LINUX.replace("${appName}", SystemConst.CURRENT_APP_NAME);
}
public int getAvatarMaxSize() {
return AVATAR_MAX_SIZE;
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket;
/**
* WebSocket业务代码常量
*
* @author odboy
*/
public interface KitWsBizCodeConst {
// 主动推送
String AutomaticPush = "100";
// 实时聊天
String IM = "101";
}
@@ -0,0 +1,55 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket;
import cn.odboy.framework.websocket.context.KitWsMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class KitWsMessagePublisher {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
private KitWsMessage innerBuild(String bizCode, String fromUser, String toUser, String message) {
KitWsMessage wsMessage = new KitWsMessage();
wsMessage.setBizCode(bizCode);
KitWsMessage.Data data = new KitWsMessage.Data();
data.setFormUser(fromUser);
data.setToUser(toUser);
data.setMessage(message);
wsMessage.setData(data);
return wsMessage;
}
public void automaticPush(String fromUser, String toUser, String message) {
KitWsMessage wsMessage = this.innerBuild(KitWsBizCodeConst.AutomaticPush, fromUser, toUser, message);
redisTemplate.convertAndSend("WebSocketMessage", wsMessage);
log.info("发送AutomaticPush消息");
}
public void imPush(String fromUser, String toUser, String message) {
KitWsMessage wsMessage = this.innerBuild(KitWsBizCodeConst.IM, fromUser, toUser, message);
redisTemplate.convertAndSend("WebSocketMessage", wsMessage);
log.info("发送IM消息");
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket;
import cn.odboy.framework.websocket.context.KitWsMessage;
import cn.odboy.framework.websocket.context.KitWsServer;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Ws消息订阅
*
* @author odboy
*/
@Slf4j
@Component
public class WsMessageSubscriber {
@Autowired
private KitWsServer kitWsServer;
// /**
// * 无效写法
// */
// public void onMessage(KitWsMessage message) {
// log.info("收到来自redis消息队列的数据, {}", JSON.toJSONString(message));
// KitWsMessage.Data data = message.getData();
// kitWsServer.sendMessage(data.getMessage(), data.getToUsername());
// }
public void onMessage(String message, String channel) {
log.info("收到来自redis消息队列的message={}, channel={}", message, channel);
KitWsMessage wsMessage = JSON.parseObject(message, KitWsMessage.class);
kitWsServer.sendMessage(wsMessage.getData().getMessage(), wsMessage.getData().getToUser());
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* 自动注册使用了 @ServerEndpoint 注解声明的 WebSocket 端点对象
*
* @author odboy
* @date 2025-10-30
*/
@Configuration
public class WebSocketEndpointConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket.context;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
/**
* WebSocket客户端管理
*
* @author odboy
* @date 2025-07-24
*/
@Slf4j
public class KitWsClientManager {
/**
* concurrent包的线程安全Map, 用来存放每个客户端对应的MyWebSocket对象(已通过redis pub/sub解耦)
*/
private static final Map<String, KitWsServer> clientMap = new ConcurrentHashMap<>();
public static void addClient(String sid, KitWsServer wsServer) {
// 如果存在就先删除一个, 防止重复推送消息
removeClient(sid);
clientMap.put(sid, wsServer);
}
public static void removeClient(String sid) {
KitWsServer wsServer = clientMap.get(sid);
if (wsServer != null) {
try {
log.info("关闭session, sid={}", sid);
wsServer.getSession().close();
} catch (Exception e) {
log.error("Close session failed, sid={}", sid, e);
}
clientMap.remove(sid);
}
}
public static Collection<KitWsServer> getAllClient() {
return clientMap.values();
}
public static KitWsServer getClientBySid(String sid) {
return clientMap.get(sid);
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket.context;
import cn.odboy.base.KitObject;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class KitWsMessage extends KitObject {
private String bizCode;
private Data data;
@Getter
@Setter
public static class Data extends KitObject {
private String formUser;
private String toUser;
private String message;
}
}
@@ -0,0 +1,159 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket.context;
import cn.hutool.core.util.StrUtil;
import cn.odboy.framework.context.KitSpringBeanHolder;
import cn.odboy.framework.websocket.KitWsMessagePublisher;
import com.alibaba.fastjson2.JSON;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Objects;
@Slf4j
@Component
@ServerEndpoint("/websocket/{sid}")
@Getter
public class KitWsServer {
/**
* 与某个客户端的连接会话, 需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收sid
*/
private String sid = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
this.sid = sid;
KitWsClientManager.addClient(sid, this);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
if ("[{}]".equals(message)){
try {
KitWsClientManager.removeClient(this.sid);
} catch (Exception e) {
log.error("WebSocket onClose error, sid={}", this.sid, e);
}
return;
}
KitWsMessage wsMessage = JSON.parseObject(message, KitWsMessage.class);
// String bizCode = wsMessage.getBizCode();
// Object data = wsMessage.getData();
log.info("收到来 sid={} 的信息: message={}", sid, message);
// session.getBasicRemote().sendText(message);
KitWsMessagePublisher messagePublisher = KitSpringBeanHolder.getBean(KitWsMessagePublisher.class);
messagePublisher.imPush(wsMessage.getData().getFormUser(), wsMessage.getData().getToUser(), wsMessage.getData().getMessage());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
try {
KitWsClientManager.removeClient(this.sid);
} catch (Exception e) {
log.error("WebSocket onClose error, sid={}", this.sid, e);
}
}
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocket sid={} 发生错误", this.sid, error);
try {
KitWsClientManager.removeClient(this.sid);
} catch (Exception e) {
log.error("WebSocket onClose error, sid={}", this.sid, e);
}
}
/**
* 实现服务器主动推送
*/
private void innerSendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 精准推送消息
*/
public void sendMessage(String message, @PathParam("sid") String sid) {
log.info("推送消息到{}, 推送内容:{}", sid, message);
try {
if (StrUtil.isBlank(sid)) {
sendToAll(message);
} else {
KitWsServer wsClient = KitWsClientManager.getClientBySid(sid);
if (wsClient != null) {
wsClient.innerSendMessage(message);
}
}
} catch (IOException e) {
log.warn("推送消息到客户端失败", e);
}
}
/**
* 群发消息
*/
private void sendToAll(String message) {
for (KitWsServer item : KitWsClientManager.getAllClient()) {
try {
item.innerSendMessage(message);
} catch (IOException e) {
log.error("发送消息给 sid={} 失败", item.sid, e);
KitWsClientManager.removeClient(item.sid);
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
KitWsServer that = (KitWsServer) o;
return Objects.equals(session, that.session) && Objects.equals(sid, that.sid);
}
@Override
public int hashCode() {
return Objects.hash(session, sid);
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket.model;
import cn.odboy.base.KitObject;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class WsSidVo extends KitObject {
private String username;
private String bizCode;
private String param;
}
@@ -0,0 +1,44 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket.util;
import cn.odboy.framework.exception.BadRequestException;
import cn.odboy.framework.websocket.model.WsSidVo;
import lombok.experimental.UtilityClass;
/**
* WebSocket消息解析工具
*
* @author odboy
* @date 2025-07-25
*/
@UtilityClass
public class WsMessageUtil {
public static WsSidVo parseSid(String sid) {
// sid {username}_{bizCode}_{param}
String[] sids = sid.split("_");
if (sids.length != 3) {
throw new BadRequestException("sid格式异常");
}
WsSidVo sidVo = new WsSidVo();
sidVo.setUsername(sids[0]);
sidVo.setBizCode(sids[1]);
sidVo.setParam(sids[2]);
return sidVo;
}
}
@@ -0,0 +1,112 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.odboy.annotation.AnonymousAccess;
import cn.odboy.constant.RequestMethodEnum;
import lombok.experimental.UtilityClass;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* 匿名标记工具
*/
@UtilityClass
public class KitAnonTagUtil {
/**
* 获取匿名标记的URL
*
* @param applicationContext /
* @return /
*/
public static Map<String, Set<String>> getAnonymousUrl(final ApplicationContext applicationContext) {
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
Map<String, Set<String>> anonymousUrls = new HashMap<>(8);
// 获取匿名标记
Set<String> get = new HashSet<>();
Set<String> post = new HashSet<>();
Set<String> put = new HashSet<>();
Set<String> patch = new HashSet<>();
Set<String> delete = new HashSet<>();
Set<String> all = new HashSet<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
if (null != anonymousAccess && infoEntry.getKey() != null) {
List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
RequestMethodEnum request = RequestMethodEnum.find(requestMethods.isEmpty() ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
PathPatternsRequestCondition patternsCondition = infoEntry.getKey().getPathPatternsCondition();
if (patternsCondition != null) {
switch (Objects.requireNonNull(request)) {
case GET:
get.addAll(patternsCondition.getPatternValues());
break;
case POST:
post.addAll(patternsCondition.getPatternValues());
break;
case PUT:
put.addAll(patternsCondition.getPatternValues());
break;
case PATCH:
patch.addAll(patternsCondition.getPatternValues());
break;
case DELETE:
delete.addAll(patternsCondition.getPatternValues());
break;
default:
all.addAll(patternsCondition.getPatternValues());
break;
}
}
}
}
anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
return anonymousUrls;
}
/**
* 获取所有匿名标记的URL
*
* @param applicationContext /
* @return /
*/
public static Set<String> getAllAnonymousUrl(final ApplicationContext applicationContext) {
Set<String> allUrl = new HashSet<>();
Map<String, Set<String>> anonymousUrls = getAnonymousUrl(applicationContext);
for (Map.Entry<String, Set<String>> entry : anonymousUrls.entrySet()) {
allUrl.addAll(anonymousUrls.get(entry.getKey()));
}
return allUrl;
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.core.util.IdUtil;
import lombok.experimental.UtilityClass;
import java.nio.charset.StandardCharsets;
/**
* 加密令牌生成工具
*
* @author odboy
* @date 2022-11-16
*/
@UtilityClass
public final class KitBase64SecretUtil {
public static String build() {
// 128位uuid
return Base64Encoder.encode(getLongUuid(4).getBytes(StandardCharsets.UTF_8));
}
public static String getLongUuid(int bit) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < bit; i++) {
result.append(IdUtil.simpleUUID());
}
return result.toString();
}
public static void main(String[] args) {
System.out.println(build());
}
}
@@ -0,0 +1,58 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import lombok.experimental.UtilityClass;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Bean拷贝相关
*
* @author odboy
* @date 2026-01-06
*/
@UtilityClass
public class KitBeanUtil {
public static <T> T copyToClass(Object source, Class<T> tClass) {
if (source == null) {
return null;
}
return BeanUtil.copyProperties(source, tClass);
}
public static void copyToTarget(Object source, Object target) {
if (source == null || target == null) {
return;
}
BeanUtil.copyProperties(source, target, CopyOptions.create().setIgnoreNullValue(true));
}
public static <T> List<T> copyToList(Collection<?> collection, Class<T> targetType) {
if (collection == null) {
return new ArrayList<>(0);
}
if (collection.isEmpty()) {
return new ArrayList<>(0);
}
return BeanUtil.copyToList(collection, targetType);
}
}
@@ -0,0 +1,183 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import lombok.experimental.UtilityClass;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
/**
* 计算类
*/
@UtilityClass
public final class KitBigDecimalUtil {
/**
* 将对象转换为 BigDecimal
*
* @param obj 输入对象
* @return 转换后的 BigDecimal
*/
private static BigDecimal toBigDecimal(Object obj) {
return switch (obj) {
case BigDecimal result -> result;
case Long result -> BigDecimal.valueOf(result);
case Integer result -> BigDecimal.valueOf(result.longValue());
case Short result -> BigDecimal.valueOf(result.longValue());
case Byte result -> BigDecimal.valueOf(result.longValue());
case Double result -> BigDecimal.valueOf(result);
case Float result -> BigDecimal.valueOf(result.doubleValue());
case String result -> new BigDecimal(result);
case Number result -> BigDecimal.valueOf(result.doubleValue());
case null, default -> {
assert obj != null;
throw new IllegalArgumentException("Unsupported type: " + obj.getClass().getName());
}
};
}
/**
* 加法
*
* @param a 加数
* @param b 加数
* @return 两个加数的和,
*/
public static BigDecimal add(Object a, Object b) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.add(bdB);
}
/**
* 减法
*
* @param a 被减数
* @param b 减数
* @return 两数的差,
*/
public static BigDecimal subtract(Object a, Object b) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.subtract(bdB);
}
/**
* 乘法
*
* @param a 乘数
* @param b 乘数
* @return 两个乘数的积, 保留6位小数
*/
public static BigDecimal multiply(Object a, Object b) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.multiply(bdB);
}
/**
* 除法
*
* @param a 被除数
* @param b 除数
* @return 两数的商
*/
public static BigDecimal divide(Object a, Object b) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.divide(bdB, 6, RoundingMode.DOWN);
}
/**
* 分转元
*
* @param obj 分的金额
* @return 转换后的元
*/
public static BigDecimal centsToYuan(Object obj) {
BigDecimal cents = toBigDecimal(obj);
return cents.divide(BigDecimal.valueOf(100), 6, RoundingMode.DOWN);
}
/**
* 元转分
*
* @param obj 元的金额
* @return 转换后的分
*/
public static long yuanToCents(Object obj) {
BigDecimal yuan = toBigDecimal(obj);
return yuan.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_EVEN).longValue();
}
/**
* 取整,采用银行家算法
*
* @param obj 金额
* @return /
*/
public static BigDecimal toRound(Object obj) {
return toBigDecimal(obj).setScale(0, RoundingMode.HALF_EVEN);
}
/**
* 取整,采用银行家算法
*
* @param obj 金额
* @return /
*/
public static long toRoundLong(Object obj) {
return toBigDecimal(obj).setScale(0, RoundingMode.HALF_EVEN).longValue();
}
/**
* 格式化为 xxx,xxx,xxx 格式
*
* @param obj 金额
* @return /
*/
public static String formatGroupingAmount(Object obj) {
if (obj == null) {
return "0";
}
BigDecimal bigDecimal = toBigDecimal(obj);
NumberFormat nf = NumberFormat.getInstance();
// 启用千位分隔符
nf.setGroupingUsed(true);
return nf.format(bigDecimal.setScale(0, RoundingMode.HALF_UP));
}
public static void main(String[] args) {
BigDecimal num1 = new BigDecimal("10.123");
BigDecimal num2 = new BigDecimal("2.456");
System.out.println("加法结果: " + add(num1, num2));
System.out.println("减法结果: " + subtract(num1, num2));
System.out.println("乘法结果: " + multiply(num1, num2));
System.out.println("除法结果: " + divide(num1, num2));
Long cents = 12345L;
System.out.println("分转元结果: " + centsToYuan(cents));
BigDecimal yuan = new BigDecimal("123.45");
System.out.println("元转分结果: " + yuanToCents(yuan));
BigDecimal toRo = new BigDecimal("123.1548456");
System.out.println("取整: " + toRound(toRo));
BigDecimal toRoLong = new BigDecimal("123.1548456");
System.out.println("取整转long: " + toRoundLong(toRoLong));
BigDecimal formatGa = new BigDecimal("1231548456123123");
System.out.println("格式化千分位: " + formatGroupingAmount(formatGa));
}
}
@@ -0,0 +1,73 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import jakarta.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 浏览器 相关
*/
@Slf4j
@UtilityClass
public final class KitBrowserUtil {
private static final String UNKNOWN = "unknown";
/**
* 获取ip地址
*/
public static String getIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
String comma = ",";
String localhost = "127.0.0.1";
if (ip.contains(comma)) {
ip = ip.split(",")[0];
}
if (localhost.equals(ip)) {
// 获取本机真正的ip地址
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
log.error("获取本机地址失败", e);
}
}
return ip;
}
/**
* 获取浏览器
*/
public static String getVersion(HttpServletRequest request) {
UserAgent ua = UserAgentUtil.parse(request.getHeader("User-Agent"));
String browser = ua.getBrowser().toString() + " " + ua.getVersion();
return browser.replace(".0.0.0", "");
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import lombok.experimental.UtilityClass;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
/**
* 类 相关
*/
@UtilityClass
public final class KitClassUtil {
public static List<Field> getAllFields(Class<?> clazz, List<Field> fields) {
if (clazz != null) {
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
getAllFields(clazz.getSuperclass(), fields);
}
return fields;
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import lombok.experimental.UtilityClass;
import java.io.Closeable;
/**
* 用于关闭各种连接, 缺啥补啥
*/
@UtilityClass
public final class KitCloseUtil {
public static void close(Closeable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
// 静默关闭
}
}
}
public static void close(AutoCloseable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
// 静默关闭
}
}
}
}
@@ -0,0 +1,223 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.odboy.base.KitSelectOptionVo;
import com.alibaba.fastjson2.JSON;
import lombok.experimental.UtilityClass;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* 集合工具
*
* @author odboy
* @date 2022-11-09
*/
@UtilityClass
public final class KitCollUtil extends cn.hutool.core.collection.CollUtil {
private static <T, K> Predicate<T> distinctPredicate(Function<? super T, ? extends K> keyMapper) {
Map<Object, Boolean> map = new HashMap<>(3);
return (t) -> null == map.putIfAbsent(keyMapper.apply(t), true);
}
/**
* 流根据对象属性去重
* <p>
* 使用方式: list = CollUtil.streamDistinct(list, Object::getXXX)
*/
public static <T, K> List<T> streamDistinct(List<T> data, Function<? super T, ? extends K> keyMapper) {
return data.stream().filter(distinctPredicate(keyMapper)).collect(Collectors.toList());
}
/**
* 流根据对象属性自然排序
* <p>
* 使用方式: list = CollUtil.streamAscSort(list, Object::getXXX)
*
* @param data /
* @param keyExtractor /
* @param <T> /
* @param <U> /
* @return /
*/
public static <T, U extends Comparable<? super U>> List<T> streamAscSort(List<T> data,
Function<? super T, ? extends U> keyExtractor) {
return data.stream().sorted(Comparator.comparing(keyExtractor)).collect(Collectors.toList());
}
/**
* 流根据对象属性倒转排序
* <p>
* 使用方式: list = CollUtil.streamDescSort(list, Object::getXXX)
*
* @param data /
* @param keyExtractor /
* @param <T> /
* @param <U> /
* @return /
*/
public static <T, U extends Comparable<? super U>> List<T> streamDescSort(List<T> data,
Function<? super T, ? extends U> keyExtractor) {
return data.stream().sorted(Comparator.comparing(keyExtractor).reversed()).collect(Collectors.toList());
}
/**
* 流拆分对象为键值对
* <p>
* 使用方式: map = CollUtil.streamToMap(list, Object::getXXKey, Object::getXXValue)
*
* @param data /
* @param keyMapper /
* @param valueMapper /
* @param <T> /
* @param <K> /
* @param <U> /
* @return /
*/
public static <T, K, U> Map<K, U> streamToMap(List<T> data, Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
// 根据key去重再并流
return streamDistinct(data, keyMapper).stream().collect(Collectors.toMap(keyMapper, valueMapper));
}
/**
* 流对List<Double>求和
* <p>
* 使用方式: total = CollUtil.streamDoubleAdd(doubleList)
*
* @param data /
* @return /
*/
public static Double streamDoubleAdd(List<Double> data) {
if (KitCollUtil.isNotEmpty(data)) {
return data.stream().filter(Objects::nonNull).reduce(Double::sum).orElse(0.0);
}
return 0.0;
}
/**
* 流对List<BigDecimal>求和
* <p>
* 使用方式: total = CollUtil.streamBigDecimalAdd(doubleList)
*
* @param data /
* @return /
*/
public static BigDecimal streamBigDecimalAdd(List<BigDecimal> data) {
if (KitCollUtil.isNotEmpty(data)) {
return data.stream().filter(Objects::nonNull).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
}
return BigDecimal.ZERO;
}
/**
* 流对List<Object>中的某一BigDecimal属性求和
* <p>
* 使用方式: total = CollUtil.streamBigDecimalAdd(list, XXX)
*
* @param data /
* @return /
*/
public static <T> BigDecimal streamBigDecimalAdd(List<T> data, String elementName) {
return data.stream().filter(Objects::nonNull).map(m -> {
// 反射, 取集合中各元素的某属性
BigDecimal that;
try {
Field field = m.getClass().getDeclaredField(elementName);
field.setAccessible(true);
that = (BigDecimal) field.get(m);
} catch (Exception e) {
// 忽略异常
that = BigDecimal.ZERO;
}
return that;
}).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
}
/**
* 流对List<BigDecimal>取最大值
* <p>
* 使用方式: max = CollUtil.streamBigDecimalMax(list)
*
* @param data /
* @return /
*/
public static BigDecimal streamBigDecimalMax(List<BigDecimal> data) {
return data.stream().filter(Objects::nonNull).reduce(BigDecimal::max).orElse(null);
}
/**
* 流对List<BigDecimal>取最小值
* <p>
* 使用方式: max = CollUtil.streamBigDecimalMin(list)
*
* @param data /
* @return /
*/
public static BigDecimal streamBigDecimalMin(List<BigDecimal> data) {
return data.stream().filter(Objects::nonNull).reduce(BigDecimal::min).orElse(null);
}
/**
* 流对List<BigDecimal>取平均值
* <p>
* 使用方式: avg = CollUtil.streamBigDecimalAvg(list)
*
* @param data /
* @return /
*/
public static BigDecimal streamBigDecimalAvg(List<BigDecimal> data) {
KitValidUtil.isEmpty(data, "data");
return data.stream().filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(BigDecimal.valueOf(data.size()), 6, RoundingMode.FLOOR);
}
public static void main(String[] args) {
List<KitSelectOptionVo> testData = new ArrayList<>();
KitSelectOptionVo selectOption1 = KitSelectOptionVo.builder().label("苹果").value("apple").build();
KitSelectOptionVo selectOption2 = KitSelectOptionVo.builder().label("苹果").value("apple").build();
KitSelectOptionVo selectOption3 = KitSelectOptionVo.builder().label("香蕉").value("banana").build();
KitSelectOptionVo selectOption4 = KitSelectOptionVo.builder().label("猫").value("cat").build();
testData.add(selectOption1);
testData.add(selectOption2);
testData.add(selectOption3);
testData.add(selectOption4);
List<KitSelectOptionVo> selectOptions1 = KitCollUtil.streamAscSort(testData, KitSelectOptionVo::getValue);
System.out.println(JSON.toJSONString(selectOptions1));
List<KitSelectOptionVo> selectOptions2 = KitCollUtil.streamDescSort(testData, KitSelectOptionVo::getValue);
System.out.println(JSON.toJSONString(selectOptions2));
List<KitSelectOptionVo> selectOptions3 = KitCollUtil.streamDistinct(testData, KitSelectOptionVo::getValue);
System.out.println(JSON.toJSONString(selectOptions3));
Map<String, String> selectMap1 =
KitCollUtil.streamToMap(testData, KitSelectOptionVo::getValue, KitSelectOptionVo::getLabel);
System.out.println(JSON.toJSONString(selectMap1));
// 这只是一个示范
BigDecimal total1 = KitCollUtil.streamBigDecimalAdd(testData, "value");
System.out.println(total1);
}
}
@@ -0,0 +1,166 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import java.util.Calendar;
import java.util.Date;
import lombok.experimental.UtilityClass;
/**
* 时间相关工具
*
* @author odboy
* @date 2025-10-01
*/
@UtilityClass
public class KitDateUtil {
/**
* 获取当前时间毫秒数 yyyyMMddHHmmssSSS
*
* @return /
*/
public static String getNowDateTimeMsStr() {
return DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN);
}
/**
* 获取当前时间,但不包括毫秒数 yyyyMMddHHmmss
*
* @return /
*/
public static String getNowDateTimeStr() {
return DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN);
}
/**
* 获取当前时间,但不包括毫秒数 yyyyMMdd
*
* @return /
*/
public static String getNowDateStr() {
return DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN);
}
/**
* 格式化到毫秒
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return 格式化后的时间,X天X时X分X秒
*/
public static String formatMillsDuration(Date startTime, Date endTime) {
if (startTime == null || endTime == null) {
// 什么都不返回
return "";
}
long betweenSeconds = DateUtil.between(startTime, endTime, cn.hutool.core.date.DateUnit.SECOND);
long days = betweenSeconds / (24 * 60 * 60);
long hours = (betweenSeconds % (24 * 60 * 60)) / (60 * 60);
long minutes = (betweenSeconds % (60 * 60)) / 60;
long seconds = betweenSeconds % 60;
StringBuilder result = new StringBuilder();
if (days > 0) {
result.append(days).append("天");
}
if (hours > 0) {
result.append(hours).append("时");
}
if (minutes > 0) {
result.append(minutes).append("分");
}
if (seconds > 0 || result.length() == 0) {
result.append(seconds).append("秒");
}
return result.toString();
}
/**
* 格式化到秒
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return 格式化后的时间,X天X时X分X秒
*/
public static String formatSecondsDuration(Date startTime, Date endTime) {
if (startTime == null || endTime == null) {
// 什么都不返回
return "";
}
long betweenMs = DateUtil.betweenMs(startTime, endTime);
long days = betweenMs / (24 * 60 * 60 * 1000);
long hours = (betweenMs % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000);
long minutes = (betweenMs % (60 * 60 * 1000)) / (60 * 1000);
long seconds = (betweenMs % (60 * 1000)) / 1000;
long milliseconds = betweenMs % 1000;
StringBuilder result = new StringBuilder();
if (days > 0) {
result.append(days).append("天");
}
if (hours > 0) {
result.append(hours).append("时");
}
if (minutes > 0) {
result.append(minutes).append("分");
}
if (seconds > 0) {
result.append(seconds).append("秒");
}
if (milliseconds > 0) {
result.append(milliseconds).append("毫秒");
}
// 如果所有单位都是0,则显示0毫秒
if (result.length() == 0) {
result.append("0毫秒");
}
return result.toString();
}
/**
* 获得当天是周几
*/
public static String getWeekDay() {
String[] weekDays = {"星期天", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"};
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
int w = cal.get(Calendar.DAY_OF_WEEK) - 1;
if (w < 0) {
w = 0;
}
return weekDays[w];
}
/**
* 格式化毫秒时间戳字符串为'yyyy-MM-dd'格式
*
* @param timestamp 例如:1767196800000
* @return 例如:2026-01-01
*/
public static String formatMsTimestamp(String timestamp) {
String formatDate;
if (timestamp.matches("\\d{13}")) {
long ts = Long.parseLong(timestamp);
formatDate = DateUtil.format(new Date(ts), DatePattern.NORM_DATE_PATTERN);
} else {
DateTime dateTime = DateTime.of(timestamp, DatePattern.NORM_DATETIME_PATTERN);
formatDate = DateUtil.format(dateTime, DatePattern.NORM_DATE_PATTERN);
}
return formatDate;
}
}
@@ -0,0 +1,94 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import lombok.experimental.UtilityClass;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.nio.charset.StandardCharsets;
/**
* DES加密
*/
@UtilityClass
public final class KitDesEncryptUtil {
private static final String STR_PARAM = "Passw0rd";
private static final IvParameterSpec IV = new IvParameterSpec(STR_PARAM.getBytes(StandardCharsets.UTF_8));
private static DESKeySpec getDesKeySpec(String source) throws Exception {
if (source == null || source.isEmpty()) {
return null;
}
return new DESKeySpec(STR_PARAM.getBytes(StandardCharsets.UTF_8));
}
/**
* 对称加密
*/
public static String desEncrypt(String source) throws Exception {
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
DESKeySpec desKeySpec = getDesKeySpec(source);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey secretKey = keyFactory.generateSecret(desKeySpec);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IV);
return byte2hex(cipher.doFinal(source.getBytes(StandardCharsets.UTF_8))).toUpperCase();
}
/**
* 对称解密
*/
public static String desDecrypt(String source) throws Exception {
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
byte[] src = hex2byte(source.getBytes(StandardCharsets.UTF_8));
DESKeySpec desKeySpec = getDesKeySpec(source);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey secretKey = keyFactory.generateSecret(desKeySpec);
cipher.init(Cipher.DECRYPT_MODE, secretKey, IV);
byte[] retByte = cipher.doFinal(src);
return new String(retByte);
}
private static String byte2hex(byte[] inStr) {
String stmp;
StringBuilder out = new StringBuilder(inStr.length * 2);
for (byte b : inStr) {
stmp = Integer.toHexString(b & 0xFF);
if (stmp.length() == 1) {
out.append("0").append(stmp);
} else {
out.append(stmp);
}
}
return out.toString();
}
private static byte[] hex2byte(byte[] b) {
int size = 2;
if ((b.length % size) != 0) {
throw new IllegalArgumentException("长度不是偶数");
}
byte[] b2 = new byte[b.length / 2];
for (int n = 0; n < b.length; n += size) {
String item = new String(b, n, 2);
b2[n / 2] = (byte) Integer.parseInt(item, 16);
}
return b2;
}
}
@@ -0,0 +1,196 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.odboy.framework.exception.BadRequestException;
import java.util.concurrent.atomic.AtomicLong;
/**
* 分布式id生成工具(不建议用,仅供参考)
*
* @author odboy
* @date 2025-04-05
*/
@Deprecated
public final class KitDistributedIdUtil {
/**
* 起始毫秒时间戳, 2020-12-30
*/
private static final long START_TIMESTAMP = 1609314517000L;
/**
* 数据中心 ID 所占位数
*/
private final long dataCenterIdBits;
/**
* 机器 ID 所占位数
*/
private final long workerIdBits;
/**
* 序列号所占位数
*/
private final long sequenceBits;
/**
* 数据中心 ID 最大值
*/
private final long maxDataCenterId;
/**
* 机器 ID 最大值
*/
private final long maxWorkerId;
/**
* 序列号最大值
*/
private final long sequenceMask;
/**
* 机器 ID 向左移位数
*/
private final long workerIdShift;
/**
* 数据中心 ID 向左移位数
*/
private final long dataCenterIdShift;
/**
* 时间戳向左移位数
*/
private final long timestampLeftShift;
/**
* 数据中心 ID
*/
private final long dataCenterId;
/**
* 机器 ID
*/
private final long workerId;
/**
* 序列号
*/
private final AtomicLong sequence = new AtomicLong(0L);
/**
* 上一次生成 ID 的时间戳
*/
private final AtomicLong lastTimestamp = new AtomicLong(-1L);
/**
* 构造函数, 初始化数据中心 ID 和机器 ID
*
* @param dataCenterId 数据中心 ID
* @param workerId 机器 ID
* @param dataCenterIdBits 数据中心 ID 所占位数
* @param workerIdBits 机器 ID 所占位数
* @param sequenceBits 序列号所占位数
*/
public KitDistributedIdUtil(long dataCenterId, long workerId, long dataCenterIdBits, long workerIdBits,
long sequenceBits) {
this.dataCenterIdBits = dataCenterIdBits;
this.workerIdBits = workerIdBits;
this.sequenceBits = sequenceBits;
this.maxDataCenterId = ~(-1L << dataCenterIdBits);
this.maxWorkerId = ~(-1L << workerIdBits);
this.sequenceMask = ~(-1L << sequenceBits);
this.workerIdShift = sequenceBits;
this.dataCenterIdShift = sequenceBits + workerIdBits;
this.timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException(
"Data center ID can't be greater than " + maxDataCenterId + " or less than 0");
}
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("Worker ID can't be greater than " + maxWorkerId + " or less than 0");
}
this.dataCenterId = dataCenterId;
this.workerId = workerId;
}
/**
* 获取单例实例的下一个分布式 ID
*
* @return 生成的分布式 ID
*/
public static long fastNextId() {
return InstanceHolder.INSTANCE.nextId();
}
/**
* 主方法,用于测试分布式 ID 生成
*
* @param args 命令行参数
*/
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
long id = Thread.currentThread().getId();
for (int j = 0; j < 20; j++) {
System.out.println(id + " - " + KitDistributedIdUtil.fastNextId());
}
}).start();
}
}
/**
* 生成下一个分布式 ID
*
* @return 生成的分布式 ID
*/
public long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 如果当前时间戳小于上一次生成 ID 的时间戳, 说明时钟回拨,抛出异常
if (currentTimestamp < lastTimestamp.get()) {
throw new BadRequestException(
"Clock moved backwards. Refusing to generate id for " + (lastTimestamp.get() - currentTimestamp) +
" milliseconds");
}
long lastTimestampValue = lastTimestamp.get();
if (currentTimestamp == lastTimestampValue) {
// 同一毫秒内,序列号自增
long seq = sequence.incrementAndGet() & sequenceMask;
if (seq == 0) {
// 序列号溢出,等待下一毫秒
currentTimestamp = waitNextMillis(lastTimestampValue);
}
return ((currentTimestamp - START_TIMESTAMP) << timestampLeftShift) | (dataCenterId << dataCenterIdShift) |
(workerId << workerIdShift) | seq;
} else {
// 时间戳改变,重置序列号
sequence.set(0L);
lastTimestamp.set(currentTimestamp);
return ((currentTimestamp - START_TIMESTAMP) << timestampLeftShift) | (dataCenterId << dataCenterIdShift) |
(workerId << workerIdShift);
}
}
/**
* 等待下一毫秒
*
* @param lastTimestamp 上一次生成 ID 的时间戳
* @return 当前时间戳
*/
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
/**
* 单例实例持有者类
*/
private static class InstanceHolder {
private static final KitDistributedIdUtil INSTANCE = new KitDistributedIdUtil(20, 12, 5, 5, 12);
}
}
@@ -0,0 +1,400 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.BigExcelWriter;
import cn.hutool.poi.excel.ExcelUtil;
import cn.odboy.constant.FileTypeEnum;
import cn.odboy.constant.SystemConst;
import cn.odboy.framework.exception.BadRequestException;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.odboy.constant.SystemConst.SYMBOL_ADD;
import static cn.odboy.constant.SystemConst.SYMBOL_AT;
import static cn.odboy.constant.SystemConst.SYMBOL_EQUAL;
import static cn.odboy.constant.SystemConst.SYMBOL_SUBTRACT;
/**
* File工具类, 扩展 hutool 工具包
*/
@Slf4j
@UtilityClass
public final class KitFileUtil extends FileUtil {
/**
* 系统临时目录
* <br>
* windows 包含路径分割符, 但Linux 不包含, 在windows \\==\ 前提下, 为安全起见 同意拼装 路径分割符,
* <pre>
* java.io.tmpdir
* windows : C:\Users/xxx\AppData\Local\Temp\
* linux: /temp
* </pre>
*/
public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator;
/**
* 定义GB的计算常量
*/
private static final int GB = 1024 * 1024 * 1024;
/**
* 定义MB的计算常量
*/
private static final int MB = 1024 * 1024;
/**
* 定义KB的计算常量
*/
private static final int KB = 1024;
/**
* MultipartFile转File
*/
public static File toFile(MultipartFile multipartFile) {
// 获取文件名
String fileName = multipartFile.getOriginalFilename();
// 获取文件后缀
String prefix = "." + getSuffix(fileName);
File file = null;
try {
// 用uuid作为文件名, 防止生成的临时文件重复
file = new File(SYS_TEM_DIR + IdUtil.simpleUUID() + prefix);
// MultipartFile to File
multipartFile.transferTo(file);
} catch (IOException e) {
log.error("保存临时文件失败", e);
}
return file;
}
/**
* 文件大小转换
*/
public static String getSize(long size) {
String resultSize;
if (size / GB >= 1) {
//如果当前Byte的值大于等于1GB
resultSize = new DecimalFormat("0.00").format(size / (float) GB) + " GB";
} else if (size / MB >= 1) {
//如果当前Byte的值大于等于1MB
resultSize = new DecimalFormat("0.00").format(size / (float) MB) + " MB";
} else if (size / KB >= 1) {
//如果当前Byte的值大于等于1KB
resultSize = new DecimalFormat("0.00").format(size / (float) KB) + " KB";
} else {
resultSize = size + " B";
}
return resultSize;
}
/**
* inputStream 转 File
*/
public static File inputStreamToFile(InputStream ins, String name) {
File file = new File(SYS_TEM_DIR + name);
if (file.exists()) {
return file;
}
OutputStream os = null;
try {
os = Files.newOutputStream(file.toPath());
int bytesRead;
int len = 8192;
byte[] buffer = new byte[len];
while ((bytesRead = ins.read(buffer, 0, len)) != -1) {
os.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
KitCloseUtil.close(os);
KitCloseUtil.close(ins);
}
return file;
}
/**
* 将文件名解析成文件的上传路径
*/
public static File upload(MultipartFile file, String filePath) {
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssS");
// 过滤非法文件名
String name = getPrefix(verifyFilename(file.getOriginalFilename()));
String suffix = getSuffix(file.getOriginalFilename());
String nowStr = "-" + format.format(date);
try {
String fileName = name + nowStr + "." + suffix;
String path = filePath + fileName;
// getCanonicalFile 可解析正确各种路径
File dest = new File(path).getCanonicalFile();
// 检测是否存在目录
if (!dest.getParentFile().exists()) {
if (!dest.getParentFile().mkdirs()) {
log.error("创建目录失败, {}", dest.getParentFile().getAbsolutePath());
}
}
// 文件写入
file.transferTo(dest);
return dest;
} catch (Exception e) {
log.error("文件上传失败", e);
}
return null;
}
/**
* 导出excel
*/
@Deprecated
public static void downloadExcel(List<Map<String, Object>> list, HttpServletResponse response) throws IOException {
String tempPath = SYS_TEM_DIR + IdUtil.fastSimpleUUID() + ".xlsx";
File file = new File(tempPath);
BigExcelWriter writer = ExcelUtil.getBigWriter(file);
// 处理数据以防止CSV注入
List<Map<String, Object>> sanitizedList = list.parallelStream().map(map -> {
Map<String, Object> sanitizedMap = new LinkedHashMap<>();
map.forEach((key, value) -> {
if (value instanceof String) {
String strValue = (String) value;
// 检查并处理以特殊字符开头的值
if (strValue.startsWith(SYMBOL_EQUAL) || strValue.startsWith(SYMBOL_ADD) ||
strValue.startsWith(SYMBOL_SUBTRACT) || strValue.startsWith(SYMBOL_AT)) {
// 添加单引号前缀
strValue = "'" + strValue;
}
sanitizedMap.put(key, strValue);
} else {
sanitizedMap.put(key, value);
}
});
return sanitizedMap;
}).collect(Collectors.toList());
// 一次性写出内容, 使用默认样式,强制输出标题
writer.write(sanitizedList, true);
SXSSFSheet sheet = (SXSSFSheet) writer.getSheet();
//上面需要强转SXSSFSheet 不然没有trackAllColumnsForAutoSizing方法
sheet.trackAllColumnsForAutoSizing();
//列宽自适应
writer.autoSizeColumnAll();
//response为HttpServletResponse对象
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
//test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码
response.setHeader("Content-Disposition", "attachment;filename=file.xlsx");
ServletOutputStream out = response.getOutputStream();
// 终止后删除临时文件
file.deleteOnExit();
writer.flush(out, true);
//此处记得关闭输出Servlet流
IoUtil.close(out);
}
/**
* 获取文件类型
*/
public static String getFileType(String type) {
String documents = "txt doc pdf ppt pps xlsx xls docx";
String voice = "mp3 wav wma mpa ram ra aac aif m4a";
String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg";
String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg";
if (image.contains(type)) {
return FileTypeEnum.IMAGE.getCode();
} else if (documents.contains(type)) {
return FileTypeEnum.DOC.getCode();
} else if (voice.contains(type)) {
return FileTypeEnum.VOICE.getCode();
} else if (video.contains(type)) {
return FileTypeEnum.VIDEO.getCode();
} else {
return FileTypeEnum.OTHER.getCode();
}
}
/**
* 检查文件大小是否超出最大值
*/
public static void checkSize(long maxSize, long size) {
// 1M
int len = 1024 * 1024;
if (size > (maxSize * len)) {
throw new BadRequestException("文件超出规定大小:" + maxSize + "MB");
}
}
/**
* 判断两个文件是否相同
*/
public static boolean check(File file1, File file2) {
String img1Md5 = getMd5(file1);
String img2Md5 = getMd5(file2);
if (img1Md5 != null) {
return img1Md5.equals(img2Md5);
}
return false;
}
/**
* 判断两个文件是否相同
*/
public static boolean check(String file1Md5, String file2Md5) {
return file1Md5.equals(file2Md5);
}
/**
* 获取文件字节数组
*/
private static byte[] getByte(File file) {
byte[] b = new byte[(int) file.length()];
InputStream in = null;
try {
in = Files.newInputStream(file.toPath());
// 忽略read值
int read = in.read(b);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
} finally {
KitCloseUtil.close(in);
}
return b;
}
/**
* 获取文件MD5值
*/
private static String getMd5(byte[] bytes) {
// 16进制字符
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("MD5");
mdTemp.update(bytes);
byte[] md = mdTemp.digest();
int j = md.length;
char[] str = new char[j * 2];
int k = 0;
// 移位 输出字符串
for (byte byte0 : md) {
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
log.error("获取文件MD5值失败", e);
}
return null;
}
/**
* 下载文件
*
* @param request /
* @param response /
* @param file /
*/
public static void downloadFile(HttpServletRequest request, HttpServletResponse response, File file,
boolean deleteOnExit) {
response.setCharacterEncoding(request.getCharacterEncoding());
response.setContentType("application/octet-stream");
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName());
IOUtils.copy(fis, response.getOutputStream());
response.flushBuffer();
} catch (Exception e) {
log.error("下载文件失败", e);
} finally {
if (fis != null) {
try {
fis.close();
if (deleteOnExit) {
file.deleteOnExit();
}
} catch (IOException e) {
log.error("关闭IO流失败", e);
}
}
}
}
/**
* 验证并过滤非法的文件名
*
* @param fileName 文件名
* @return 文件名
*/
public static String verifyFilename(String fileName) {
// 过滤掉特殊字符
fileName = fileName.replaceAll("[\\\\/:*?\"<>|~\\s]", "");
// 去掉文件名开头和结尾的空格和点
fileName = fileName.trim().replaceAll("^[. ]+|[. ]+$", "");
// 不允许文件名超过255(在Mac和Linux中)或260(在Windows中)个字符
int maxFileNameLength = 255;
if (System.getProperty(SystemConst.PROPERTY_OS_NAME).startsWith(SystemConst.OS_NAME_WINDOWS)) {
maxFileNameLength = 260;
}
if (fileName.length() > maxFileNameLength) {
fileName = fileName.substring(0, maxFileNameLength);
}
// 过滤掉控制字符
fileName = fileName.replaceAll("[\\p{Cntrl}]", "");
// 过滤掉 ".." 路径
fileName = fileName.replaceAll("\\.{2,}", "");
// 去掉文件名开头的 ".."
fileName = fileName.replaceAll("^\\.+/", "");
// 保留文件名中最后一个 "." 字符,过滤掉其他 "."
fileName = fileName.replaceAll("^(.*)(\\.[^.]*)$", "$1").replaceAll("\\.", "") +
fileName.replaceAll("^(.*)(\\.[^.]*)$", "$2");
return fileName;
}
public static String getMd5(File file) {
return getMd5(getByte(file));
}
/**
* 获取文件名前缀
*
* @param originalFilename 原始文件名
* @param firstName 首选文件名,如果不为空,则返回
* @return /
*/
public static String getPrefix(String originalFilename, String firstName) {
return StrUtil.isBlank(firstName) ? getPrefix(originalFilename) : firstName;
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.odboy.framework.context.KitSpringBeanHolder;
import lombok.experimental.UtilityClass;
import net.dreamlu.mica.ip2region.core.Ip2regionSearcher;
import net.dreamlu.mica.ip2region.core.IpInfo;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
/**
* IP地址 相关
*/
@UtilityClass
public final class KitIPUtil {
/**
* 注入bean
*/
private final static Ip2regionSearcher IP_SEARCHER = KitSpringBeanHolder.getBean(Ip2regionSearcher.class);
/**
* 根据ip获取详细地址
*/
public static String getCityInfo(String ip) {
IpInfo ipInfo = IP_SEARCHER.memorySearch(ip);
if (ipInfo != null) {
return ipInfo.getAddress();
}
return null;
}
/**
* 获取当前机器的IP
*
* @return /
*/
public static String getLocalIp() {
try {
InetAddress candidateAddress = null;
// 遍历所有的网络接口
for (Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
interfaces.hasMoreElements(); ) {
NetworkInterface anInterface = interfaces.nextElement();
// 在所有的接口下再遍历IP
for (Enumeration<InetAddress> inetAddresses = anInterface.getInetAddresses();
inetAddresses.hasMoreElements(); ) {
InetAddress inetAddr = inetAddresses.nextElement();
// 排除loopback类型地址
if (!inetAddr.isLoopbackAddress()) {
if (inetAddr.isSiteLocalAddress()) {
// 如果是site-local地址, 就是它了
return inetAddr.getHostAddress();
} else if (candidateAddress == null) {
// site-local类型的地址未被发现, 先记录候选地址
candidateAddress = inetAddr;
}
}
}
}
if (candidateAddress != null) {
return candidateAddress.getHostAddress();
}
// 如果没有发现 non-loopback地址.只能用最次选的方案
InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();
if (jdkSuppliedAddress == null) {
return "";
}
return jdkSuppliedAddress.getHostAddress();
} catch (Exception e) {
return "";
}
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import lombok.experimental.UtilityClass;
import java.util.List;
import java.util.Map;
/**
* Json工具
*
* @author odboy
* @date 2024-10-01
*/
@UtilityClass
public final class KitJsonUtil {
public static <K, V> Map<K, V> toMap(String jsonStr, Class<K> kClass, Class<V> vClass) {
return JSON.parseObject(jsonStr, new TypeReference<Map<K, V>>() {
});
}
public static <V> List<V> toList(String jsonStr, Class<V> vClass) {
return JSON.parseObject(jsonStr, new TypeReference<List<V>>() {
});
}
public static void main(String[] args) {
String mapStr = "{\"key1\":\"value1\", \"key2\":\"value2\"}";
System.err.println(KitJsonUtil.toMap(mapStr, String.class, String.class));
String listStr = "[\"value1\", \"value2\"]";
System.err.println(KitJsonUtil.toList(listStr, String.class));
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.hutool.core.date.DateTime;
import cn.odboy.base.KitBaseUserTimeTb;
import lombok.experimental.UtilityClass;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* List 工具
*
* @author odboy
* @date 2024-10-01
*/
@UtilityClass
public final class KitListUtil {
/**
* 根据对象T的属性去重
*
* @param items 对象T集合
* @param classifier 对象T属性表达式, 比如: User::getName
*/
public static <T, K> List<T> distinctByArgs(List<T> items, Function<? super T, ? extends K> classifier) {
return items.stream().collect(Collectors.groupingBy(classifier)).values().stream()
.map(List::getFirst).collect(Collectors.toList());
}
/**
* list转map, 保留新值 -> Map<String, User> userMap = items.stream().collect(Collectors.toMap(User::getName, user -> user)) <br> list转map, 保留旧值 -> Map<String,
* User> userMap = users.stream().collect(Collectors.toMap(User::getName, user -> user, (existing, replacement) -> existing));
*/
public static void main(String[] args) {
List<KitBaseUserTimeTb> entities = new ArrayList<>();
int total = 50;
for (int i = 0; i < total; i++) {
KitBaseUserTimeTb entity = new KitBaseUserTimeTb();
if (i % 2 == 0) {
entity.setCreateBy("odboy");
} else {
entity.setCreateBy("admin");
}
entity.setUpdateBy("admin");
entity.setCreateTime(DateTime.now().toTimestamp());
entity.setUpdateTime(DateTime.now().toTimestamp());
entities.add(entity);
}
List<KitBaseUserTimeTb> entities1 = KitListUtil.distinctByArgs(entities, KitBaseUserTimeTb::getCreateBy);
System.err.println(entities1);
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import lombok.experimental.UtilityClass;
/**
* 适配 logger 占位符
*
* @author odboy
* @date 2025-01-13
*/
@UtilityClass
public final class KitLogFmtUtil {
/**
* 替换字符串中的占位符 {} 为传入的参数
*
* @param message 消息模板
* @param args 替换占位符的参数
* @return 替换后的消息
*/
public static String format(String message, Object... args) {
if (message == null || args == null || args.length == 0) {
return message;
}
// 使用 String.format 替换占位符
return String.format(message.replace("{}", "%s"), args);
}
public static void main(String[] args) {
// 示例用法
String messageTemplate = "获取dingtalk accessToken失败, code={}, message={}";
String formattedMessage = format(messageTemplate, 400, "Bad Request");
// 输出: 获取dingtalk accessToken失败, code=400, message=Bad Request
System.out.println(formattedMessage);
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.odboy.base.KitPageResult;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.experimental.UtilityClass;
import java.util.Collections;
import java.util.List;
/**
* 分页工具
*
* @author odboy
*/
@UtilityClass
public final class KitPageUtil extends cn.hutool.core.util.PageUtil {
/**
* List 分页
*/
public static <T> List<T> softPaging(long page, long size, List<T> list) {
int pageIndex = Math.toIntExact(page - 1);
int fromIndex = Math.toIntExact(pageIndex * size);
int toIndex = Math.toIntExact(pageIndex * size + size);
if (fromIndex > list.size()) {
return Collections.emptyList();
} else if (toIndex >= list.size()) {
return list.subList(fromIndex, list.size());
} else {
return list.subList(fromIndex, toIndex);
}
}
/**
* Page 数据处理
*/
public static <T> KitPageResult<T> toPage(IPage<T> page) {
return new KitPageResult<>(page.getRecords(), page.getTotal());
}
/**
* 自定义分页
*/
public static <T> KitPageResult<T> toPage(List<T> list) {
return new KitPageResult<>(list, list.size());
}
/**
* 返回空数据
*/
public static <T> KitPageResult<T> emptyData() {
return new KitPageResult<>(null, 0);
}
/**
* 自定义分页
*/
public static <T> KitPageResult<T> toPage(List<T> list, long totalElements) {
return new KitPageResult<>(list, totalElements);
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import lombok.experimental.UtilityClass;
/**
* Resources模版工具
*
* @author odboy
* @date 2025-06-22
*/
@UtilityClass
public class KitResourceTemplateUtil {
public static String render(String moduleName, String filename, Dict params) {
String path = StrUtil.isBlank(moduleName) ? "template" : "template/" + moduleName;
TemplateEngine engine =
TemplateUtil.createEngine(new TemplateConfig(path, TemplateConfig.ResourceMode.CLASSPATH));
Template template = engine.getTemplate(filename);
return template.render(params);
}
}
@@ -0,0 +1,71 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.util;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* 返回值处理 工具
*
* @author odboy
* @date 2025-05-08
*/
public class KitReturnValueHandleUtil {
public static Object getDefaultValue(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取目标方法的返回值类型
Class<?> returnType = method.getReturnType();
if (returnType.isArray()) {
return getEmptyArray(returnType);
} else if (List.class.isAssignableFrom(returnType)) {
return Collections.emptyList();
} else if (Set.class.isAssignableFrom(returnType)) {
return Collections.emptySet();
} else if (Collection.class.isAssignableFrom(returnType)) {
// 默认返回空 List
return Collections.emptyList();
} else {
// 普通对象返回 null
return null;
}
}
/**
* 根据数组类型返回空数组
*/
private static Object getEmptyArray(Class<?> arrayType) {
if (arrayType == int[].class) {
return new int[0];
} else if (arrayType == long[].class) {
return new long[0];
} else if (arrayType == double[].class) {
return new double[0];
} else if (arrayType == String[].class) {
return new String[0];
} else {
// 其他引用类型数组
return java.lang.reflect.Array.newInstance(arrayType.getComponentType(), 0);
}
}
}

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